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)
def empty_dialog(self): """ Is triggered if course selection is empty """ d = QDialog() b1 = QPushButton("Ok", d) lbl1 = QLabel("Your selection of courses is empty") vbox = QVBoxLayout() vbox.addWidget(lbl1) vbox.addStretch() vbox.addWidget(b1) vbox.addStretch() d.setWindowTitle("Selection empty") d.setLayout(vbox) b1.clicked.connect(d.accept) d.setWindowIcon(QIcon("res/logo.ico")) d.exec_()
class DialogSteamapiKey: steamapi_window: QDialog def is_valid_steampi_key(self, key): if len(key) == 32: return True return False def steamapi_key_dialog(self): self.steamapi_window = QDialog() self.steamapi_window.setWindowTitle(_("Set steamapi key")) self.steamapi_window.setWindowIcon(self.switcher_logo) layout = QVBoxLayout() self.steamapi_window.setLayout(layout) text_label = QLabel( _("Used for getting avatars. Get yours from <a href='https://steamcommunity.com/dev/apikey'>steam</a>" )) apikey_edit = QLineEdit() save_button = QPushButton(_("Save")) text_label.setOpenExternalLinks(True) apikey_edit.setText(self.switcher.settings.get("steam_api_key")) layout.addWidget(text_label) layout.addWidget(apikey_edit) layout.addWidget(save_button) def save_enabled(): save_button.setEnabled( self.is_valid_steampi_key(apikey_edit.text())) def save(): self.switcher.settings["steam_api_key"] = apikey_edit.text() self.switcher.settings_write() self.steamapi_window.hide() if self.switcher.first_run: self.import_accounts_dialog() save_enabled() apikey_edit.textChanged.connect(lambda: save_enabled()) save_button.clicked.connect(lambda: save()) self.steamapi_window.show()
def success_dialog(self, desc): """ Is triggered if schedules were created """ d = QDialog() b1 = QPushButton("Ok", d) lbl1 = QLabel(f"Results successfully saved as result{desc}.txt") vbox = QVBoxLayout() vbox.addWidget(lbl1) vbox.addStretch() vbox.addWidget(b1) vbox.addStretch() d.setWindowTitle("Success") d.setLayout(vbox) b1.clicked.connect(d.accept) d.setWindowIcon(QIcon("res/logo.ico")) self.get_finallistsize() d.exec_()
def fail_dialog(self): """ Is triggered if schedules cannot be created """ d = QDialog() b1 = QPushButton("Ok", d) lbl1 = QLabel("Cannot create a schedule with these subjects") vbox = QVBoxLayout() vbox.addWidget(lbl1) vbox.addStretch() vbox.addWidget(b1) vbox.addStretch() d.setWindowTitle("Failed") d.setLayout(vbox) b1.clicked.connect(d.accept) d.setWindowIcon(QIcon("res/logo.ico")) self.get_finallistsize() d.exec_()
def on_about_clicked(self): """ About pop-up """ d = QDialog() l1 = QLabel( "nu-schedule\n\nA course schedule generator for the Nazarbayev University\nHomepage: https://github.com/ac130kz/nu-schedule\nApache 2.0 License\n\n© Mikhail Krassavin, 2020" ) b1 = QPushButton("Ok", d) vbox = QVBoxLayout() vbox.addWidget(l1) hbox = QHBoxLayout() hbox.addStretch() hbox.addWidget(b1) hbox.addStretch() vbox.addItem(hbox) d.setWindowIcon(QIcon("res/logo.ico")) d.setWindowTitle("About") d.setLayout(vbox) b1.clicked.connect(d.accept) d.exec_()
def on_help_clicked(self): """ Help pop-up """ d = QDialog() l1 = QLabel( "1. Press |Load| to download the latest data for the semester.\n\n3. With |Edit| button access the selection menu,\nadded courses will appear on the Main window.\n\n4. Use |Generate| button to generate and\n save your schedule as result<unixtimestamp>.txt" ) b1 = QPushButton("Ok", d) vbox = QVBoxLayout() vbox.addWidget(l1) hbox = QHBoxLayout() hbox.addStretch() hbox.addWidget(b1) hbox.addStretch() vbox.addItem(hbox) d.setWindowIcon(QIcon("res/logo.ico")) d.setWindowTitle("Help") d.setLayout(vbox) b1.clicked.connect(d.accept) d.exec_()
def on_edit_clicked(self): """ Editing course selection """ if self.coursesconnector: d = QDialog() add = QPushButton("Add", d) delete = QPushButton("Delete", d) cmb1 = QComboBox() cmb1.addItems(sorted([t[0] for t in self.coursesconnector])) hbox = QHBoxLayout() hbox.addWidget(cmb1) hbox.addStretch() hbox.addWidget(add) hbox.addWidget(delete) hbox.addStretch() d.setWindowIcon(QIcon("res/logo.ico")) d.setWindowTitle("Adding courses") d.setLayout(hbox) add.clicked.connect( lambda: self.on_add_clicked(cmb1.currentText())) delete.clicked.connect( lambda: self.on_delete_clicked(cmb1.currentText())) d.exec_()
class InsertLinkDialog(object): def __init__(self, parent, dp=None, signal_server_address=''): self._dialog = QDialog(parent) self._dp = dp self._parent = parent self._link = '' self._password = '' self._is_shared = True self._password_mode = False self._signal_server_address = signal_server_address self._dialog.setWindowIcon(QIcon(':/images/icon.png')) self._ui = Ui_insert_link_dialog() self._ui.setupUi(self._dialog) self._init_ui() self._cant_validate = tr("Cannot validate share link") def _init_ui(self): self._dialog.setAttribute(Qt.WA_MacFrameworkScaled) self._hide_error() self._ok_button = self._ui.ok_button self._ui.cancel_button.clicked.connect(self._dialog.reject) self._ok_button.clicked.connect(self._ok_clicked) self._ui.link_line_edit.textChanged.connect(self._text_changed) self._set_fonts() def _set_fonts(self): controls = [] controls.extend([c for c in self._dialog.findChildren(QLabel)]) controls.extend( [c for c in self._dialog.findChildren(QLineEdit)]) controls.extend( [c for c in self._dialog.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 _ok_clicked(self): validated = self._validate() if validated is None: self._change_mode() elif validated: if self._password_mode: self._password = self._ui.link_line_edit.text() else: self._link = self._ui.link_line_edit.text() self._dialog.accept() def _text_changed(self, *args, **kwargs): self._hide_error() def _validate(self): if self._is_shared: return self._validate_shared() else: return self._validate_network_file() def _validate_shared(self): if self._password_mode: return self._validate_password() if not self._validate_scheme(): return False return self._check_share_link() def _validate_scheme(self): share_url = self._ui.link_line_edit.text() pr = urlparse(share_url) success = pr.scheme in ('http', 'https', 'pvtbox') and pr.path share_hash = pr.path.split('/')[-1] success = success and share_hash and len(share_hash) == 32 if not success: self._show_error() return success def _check_share_link(self): self._lock_screen() share_url = self._link if self._password_mode \ else self._ui.link_line_edit.text() pr = urlparse(share_url) share_hash = pr.path.split('/')[-1] param_str = '' if self._password_mode: password = self._ui.link_line_edit.text() password = base64.b64encode( bytes(password, 'utf-8')).decode('utf-8') params = {"passwd": password} query = urlencode(params, encoding='utf-8') param_str = '?{}'.format(query) url = 'https://{}/ws/webshare/{}{}'.format( self._signal_server_address, share_hash, param_str) logger.debug("url %s", url) error = '' try: response = urlopen(url, timeout=1) status = response.status except HTTPError as e: logger.warning("Request to signal server returned error %s", e) status = e.code response = str(e.read(), encoding='utf-8') except URLError as e: logger.warning("Request to signal server returned url error %s", e) self._show_error(self._cant_validate) self._unlock_screen() return False logger.debug("request status %s", status) if status == 400: if self._password_mode: self._link += param_str success = True else: success, error = self._parse_response(response) if success is False: self._show_error(error) self._unlock_screen() return success def _parse_response(self, response): try: data = json.loads(response) err_code = data.get("errcode", '') info = data.get("info", '') if err_code == 'SHARE_WRONG_PASSWORD': success = None if not self._password_mode else False error = '' elif err_code == 'LOCKED_CAUSE_TOO_MANY_BAD_LOGIN': success = False error = tr('Locked after too many incorrect attempts') elif err_code == 'SHARE_NOT_FOUND': success = False error = '' else: success = False error = info if info else self._cant_validate except Exception as e: logger.warning("Can't parse response (%s). reason: %s", response, e) success = False error = self._cant_validate return success, error def _validate_password(self): if not self._ui.link_line_edit.text(): self._show_error() return False return self._check_share_link() def _validate_network_file(self): # place code to validate network file link here return False def _change_mode(self): assert not self._password_mode, \ "Must not be in password mode in changing mode" logger.debug("Changing to password mode") self._password_mode = True self._dialog.setWindowTitle(tr("Insert password")) self._link = self._ui.link_line_edit.text() self._ui.link_line_edit.setText('') self._ui.link_line_edit.setPlaceholderText(tr("Insert password here")) self._ui.link_line_edit.setEchoMode(QLineEdit.Password) self._hide_error() def _show_error(self, error_text=''): if not error_text: link_text = self._ui.link_line_edit.text() error_text = tr("Please insert share link") \ if not self._password_mode and not link_text \ else tr("Invalid link") if not self._password_mode \ else tr("Password can not be empty") if not link_text \ else tr("Wrong password") self._ui.error_label.setText(error_text) self._ui.link_line_edit.setFocus() def _hide_error(self): self._ui.error_label.setText('') self._ui.link_line_edit.setFocus() def _lock_screen(self): self._ok_button.setText(tr("Processing...")) self._dialog.setEnabled(False) self._dialog.repaint() def _unlock_screen(self): self._ok_button.setText(tr("Ok")) self._dialog.setEnabled(True) self._dialog.repaint() def show(self): logger.debug("Opening insert link dialog") if self._dialog.exec_() == QDialog.Rejected: self._link = '' self._password = '' self._is_shared = True logger.verbose("link (%s), password (%s)", self._link, self._password) return self._link, self._is_shared
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
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 View(QMainWindow): def __init__(self, model, controller): super().__init__() self._model = model self._controller = controller self.segmentcursor = False self.togglecolors = {"#1f77b4": "m", "m": "#1f77b4"} ################################################################# # define GUI layout and connect input widgets to external slots # ################################################################# self.setWindowTitle("biopeaks") self.setGeometry(50, 50, 1750, 750) self.setWindowIcon(QIcon(":/python_icon.png")) # figure0 for signal self.figure0 = Figure() self.canvas0 = FigureCanvas(self.figure0) # Enforce minimum height, otherwise resizing with self.splitter causes # mpl to throw an error because figure is resized to height 0. The # widget can still be fully collapsed with self.splitter- self.canvas0.setMinimumHeight(1) # in pixels self.ax00 = self.figure0.add_subplot(1, 1, 1) self.ax00.set_frame_on(False) self.figure0.subplots_adjust(left=0.04, right=0.98, bottom=0.25) self.line00 = None self.scat = None self.segmentspan = None # figure1 for marker self.figure1 = Figure() self.canvas1 = FigureCanvas(self.figure1) self.canvas1.setMinimumHeight(1) self.ax10 = self.figure1.add_subplot(1, 1, 1, sharex=self.ax00) self.ax10.get_xaxis().set_visible(False) self.ax10.set_frame_on(False) self.figure1.subplots_adjust(left=0.04, right=0.98) self.line10 = None # figure2 for statistics self.figure2 = Figure() self.canvas2 = FigureCanvas(self.figure2) self.canvas2.setMinimumHeight(1) self.ax20 = self.figure2.add_subplot(3, 1, 1, sharex=self.ax00) self.ax20.get_xaxis().set_visible(False) self.ax20.set_frame_on(False) self.line20 = None self.ax21 = self.figure2.add_subplot(3, 1, 2, sharex=self.ax00) self.ax21.get_xaxis().set_visible(False) self.ax21.set_frame_on(False) self.line21 = None self.ax22 = self.figure2.add_subplot(3, 1, 3, sharex=self.ax00) self.ax22.get_xaxis().set_visible(False) self.ax22.set_frame_on(False) self.line22 = None self.figure2.subplots_adjust(left=0.04, right=0.98) # navigation bar self.navitools = CustomNavigationToolbar(self.canvas0, self) # peak editing self.editcheckbox = QCheckBox("editable", self) self.editcheckbox.stateChanged.connect(self._model.set_peakseditable) # peak saving batch self.savecheckbox = QCheckBox("save during batch processing", self) self.savecheckbox.stateChanged.connect(self._model.set_savebatchpeaks) # peak auto-correction batch self.correctcheckbox = QCheckBox("correct during batch processing", self) self.correctcheckbox.stateChanged.connect( self._model.set_correctbatchpeaks) # selecting stats for saving self.periodcheckbox = QCheckBox("period", self) self.periodcheckbox.stateChanged.connect( lambda: self.select_stats("period")) self.ratecheckbox = QCheckBox("rate", self) self.ratecheckbox.stateChanged.connect( lambda: self.select_stats("rate")) self.tidalampcheckbox = QCheckBox("tidal amplitude", self) self.tidalampcheckbox.stateChanged.connect( lambda: self.select_stats("tidalamp")) # channel selection self.sigchanmenulabel = QLabel("biosignal") self.sigchanmenu = QComboBox(self) self.sigchanmenu.addItem("A1") self.sigchanmenu.addItem("A2") self.sigchanmenu.addItem("A3") self.sigchanmenu.addItem("A4") self.sigchanmenu.addItem("A5") self.sigchanmenu.addItem("A6") self.sigchanmenu.currentTextChanged.connect(self._model.set_signalchan) # initialize with default value self._model.set_signalchan(self.sigchanmenu.currentText()) self.markerchanmenulabel = QLabel("marker") self.markerchanmenu = QComboBox(self) self.markerchanmenu.addItem("none") self.markerchanmenu.addItem("I1") self.markerchanmenu.addItem("I2") self.markerchanmenu.addItem("A1") self.markerchanmenu.addItem("A2") self.markerchanmenu.addItem("A3") self.markerchanmenu.addItem("A4") self.markerchanmenu.addItem("A5") self.markerchanmenu.addItem("A6") self.markerchanmenu.currentTextChanged.connect( self._model.set_markerchan) # initialize with default value self._model.set_markerchan(self.markerchanmenu.currentText()) # processing mode (batch or single file) self.batchmenulabel = QLabel("mode") self.batchmenu = QComboBox(self) self.batchmenu.addItem("single file") self.batchmenu.addItem("multiple files") self.batchmenu.currentTextChanged.connect(self._model.set_batchmode) self.batchmenu.currentTextChanged.connect(self.toggle_options) # initialize with default value self._model.set_batchmode(self.batchmenu.currentText()) self.toggle_options(self.batchmenu.currentText()) # modality selection self.modmenulabel = QLabel("modality") self.modmenu = QComboBox(self) self.modmenu.addItem("ECG") self.modmenu.addItem("PPG") self.modmenu.addItem("RESP") self.modmenu.currentTextChanged.connect(self._model.set_modality) self.modmenu.currentTextChanged.connect(self.toggle_options) # initialize with default value self._model.set_modality(self.modmenu.currentText()) self.toggle_options(self.modmenu.currentText()) # segment selection; this widget can be openend / set visible from # the menu and closed from within itself (see mapping of segmentermap); # it provides utilities to select a segment from the signal self.segmentermap = QSignalMapper(self) self.segmenter = QDockWidget("select a segment", self) # disable closing such that widget can only be closed by confirming # selection or custom button self.segmenter.setFeatures(QDockWidget.NoDockWidgetFeatures) # Limit number of decimals to four. regex = QRegExp("[0-9]*\.?[0-9]{4}") validator = QRegExpValidator(regex) self.startlabel = QLabel("start") self.startedit = QLineEdit() self.startedit.setValidator(validator) self.endlabel = QLabel("end") self.endedit = QLineEdit() self.endedit.setValidator(validator) segmentfromcursor = QAction(QIcon(":/mouse_icon.png"), "select with mouse", self) segmentfromcursor.triggered.connect(self.enable_segmentedit) self.startedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.endedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.previewedit = QPushButton("preview segment") lambdafn = lambda: self._model.set_segment( [self.startedit.text(), self.endedit.text()]) self.previewedit.clicked.connect(lambdafn) self.confirmedit = QPushButton("confirm segment") self.confirmedit.clicked.connect(self._controller.segment_signal) self.confirmedit.clicked.connect(self.segmentermap.map) self.segmentermap.setMapping(self.confirmedit, 0) self.abortedit = QPushButton("abort segmentation") self.abortedit.clicked.connect(self.segmentermap.map) # reset the segment to None self.segmentermap.setMapping(self.abortedit, 2) self.segmenterlayout = QFormLayout() self.segmenterlayout.addRow(self.startlabel, self.startedit) self.segmenterlayout.addRow(self.endlabel, self.endedit) self.segmenterlayout.addRow(self.previewedit) self.segmenterlayout.addRow(self.confirmedit) self.segmenterlayout.addRow(self.abortedit) self.segmenterwidget = QWidget() self.segmenterwidget.setLayout(self.segmenterlayout) self.segmenter.setWidget(self.segmenterwidget) self.segmenter.setVisible(False) self.segmenter.setAllowedAreas(Qt.RightDockWidgetArea) self.addDockWidget(Qt.RightDockWidgetArea, self.segmenter) # Set up dialog to gather user input for custom files. regex = QRegExp("[1-9][0-9]") validator = QRegExpValidator(regex) self.signallabel = QLabel("biosignal column") self.signaledit = QLineEdit() self.signaledit.setValidator(validator) self.markerlabel = QLabel("marker column") self.markeredit = QLineEdit() self.markeredit.setValidator(validator) regex = QRegExp("[0-9]{2}") validator = QRegExpValidator(regex) self.headerrowslabel = QLabel("number of header rows") self.headerrowsedit = QLineEdit() self.headerrowsedit.setValidator(validator) regex = QRegExp("[0-9]{5}") validator = QRegExpValidator(regex) self.sfreqlabel = QLabel("sampling rate") self.sfreqedit = QLineEdit() self.sfreqedit.setValidator(validator) self.separatorlabel = QLabel("column separator") self.separatormenu = QComboBox(self) self.separatormenu.addItem("comma") self.separatormenu.addItem("tab") self.separatormenu.addItem("colon") self.separatormenu.addItem("space") self.continuecustomfile = QPushButton("continue loading file") self.continuecustomfile.clicked.connect(self.set_customheader) self.customfiledialog = QDialog() self.customfiledialog.setWindowTitle("custom file info") self.customfiledialog.setWindowIcon(QIcon(":/file_icon.png")) self.customfiledialog.setWindowFlags( Qt.WindowCloseButtonHint ) # remove help button by only setting close button self.customfilelayout = QFormLayout() self.customfilelayout.addRow(self.signallabel, self.signaledit) self.customfilelayout.addRow(self.markerlabel, self.markeredit) self.customfilelayout.addRow(self.separatorlabel, self.separatormenu) self.customfilelayout.addRow(self.headerrowslabel, self.headerrowsedit) self.customfilelayout.addRow(self.sfreqlabel, self.sfreqedit) self.customfilelayout.addRow(self.continuecustomfile) self.customfiledialog.setLayout(self.customfilelayout) # set up menubar menubar = self.menuBar() # signal menu signalmenu = menubar.addMenu("biosignal") openSignal = signalmenu.addMenu("load") openEDF = QAction("EDF", self) openEDF.triggered.connect(lambda: self._model.set_filetype("EDF")) openEDF.triggered.connect(self._controller.get_fpaths) openSignal.addAction(openEDF) openOpenSignals = QAction("OpenSignals", self) openOpenSignals.triggered.connect( lambda: self._model.set_filetype("OpenSignals")) openOpenSignals.triggered.connect(self._controller.get_fpaths) openSignal.addAction(openOpenSignals) openCustom = QAction("Custom", self) openCustom.triggered.connect( lambda: self._model.set_filetype("Custom")) openCustom.triggered.connect(lambda: self.customfiledialog.exec_()) openSignal.addAction(openCustom) segmentSignal = QAction("select segment", self) segmentSignal.triggered.connect(self.segmentermap.map) self.segmentermap.setMapping(segmentSignal, 1) signalmenu.addAction(segmentSignal) self.segmentermap.mapped.connect(self.toggle_segmenter) saveSignal = QAction("save", self) saveSignal.triggered.connect(self._controller.get_wpathsignal) signalmenu.addAction(saveSignal) # peak menu peakmenu = menubar.addMenu("peaks") findPeaks = QAction("find", self) findPeaks.triggered.connect(self._controller.find_peaks) peakmenu.addAction(findPeaks) autocorrectPeaks = QAction("autocorrect", self) autocorrectPeaks.triggered.connect(self._controller.autocorrect_peaks) peakmenu.addAction(autocorrectPeaks) savePeaks = QAction("save", self) savePeaks.triggered.connect(self._controller.get_wpathpeaks) peakmenu.addAction(savePeaks) loadPeaks = QAction("load", self) loadPeaks.triggered.connect(self._controller.get_rpathpeaks) peakmenu.addAction(loadPeaks) # stats menu statsmenu = menubar.addMenu("statistics") calculateStats = QAction("calculate", self) calculateStats.triggered.connect(self._controller.calculate_stats) statsmenu.addAction(calculateStats) saveStats = QAction("save", self) saveStats.triggered.connect(self._controller.get_wpathstats) statsmenu.addAction(saveStats) # set up status bar to display error messages and current file path self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.progressBar = QProgressBar(self) self.progressBar.setRange(0, 1) self.statusBar.addPermanentWidget(self.progressBar) self.currentFile = QLabel() self.statusBar.addPermanentWidget(self.currentFile) # set up the central widget containing the plot and navigationtoolbar self.centwidget = QWidget() self.setCentralWidget(self.centwidget) # connect canvas0 to keyboard and mouse input for peak editing; # only widgets (e.g. canvas) that currently have focus capture # keyboard input: "You must enable keyboard focus for a widget if # it processes keyboard events." self.canvas0.setFocusPolicy(Qt.ClickFocus) self.canvas0.setFocus() self.canvas0.mpl_connect("key_press_event", self._controller.edit_peaks) self.canvas0.mpl_connect("button_press_event", self.get_xcursor) # arrange the three figure canvases in splitter object self.splitter = QSplitter(Qt.Vertical) # setting opaque resizing to false is important, since resizing gets # very slow otherwise once axes are populated self.splitter.setOpaqueResize(False) self.splitter.addWidget(self.canvas0) self.splitter.addWidget(self.canvas1) self.splitter.addWidget(self.canvas2) self.splitter.setChildrenCollapsible(False) # define GUI layout self.vlayout0 = QVBoxLayout(self.centwidget) self.vlayout1 = QVBoxLayout() self.vlayoutA = QFormLayout() self.vlayoutB = QFormLayout() self.vlayoutC = QVBoxLayout() self.vlayoutD = QVBoxLayout() self.hlayout0 = QHBoxLayout() self.optionsgroupA = QGroupBox("processing options") self.vlayoutA.addRow(self.modmenulabel, self.modmenu) self.vlayoutA.addRow(self.batchmenulabel, self.batchmenu) self.optionsgroupA.setLayout(self.vlayoutA) self.optionsgroupB = QGroupBox("channels") self.vlayoutB.addRow(self.sigchanmenulabel, self.sigchanmenu) self.vlayoutB.addRow(self.markerchanmenulabel, self.markerchanmenu) self.optionsgroupB.setLayout(self.vlayoutB) self.optionsgroupC = QGroupBox("peaks") self.vlayoutC.addWidget(self.editcheckbox) self.vlayoutC.addWidget(self.savecheckbox) self.vlayoutC.addWidget(self.correctcheckbox) self.optionsgroupC.setLayout(self.vlayoutC) self.optionsgroupD = QGroupBox("select statistics for saving") self.vlayoutD.addWidget(self.periodcheckbox) self.vlayoutD.addWidget(self.ratecheckbox) self.vlayoutD.addWidget(self.tidalampcheckbox) self.optionsgroupD.setLayout(self.vlayoutD) self.vlayout1.addWidget(self.optionsgroupA) self.vlayout1.addWidget(self.optionsgroupB) self.vlayout1.addWidget(self.optionsgroupC) self.vlayout1.addWidget(self.optionsgroupD) self.optionsgroupwidget = QWidget() self.optionsgroupwidget.setLayout(self.vlayout1) self.optionsgroup = QDockWidget("configurations", self) self.optionsgroup.setAllowedAreas(Qt.LeftDockWidgetArea) self.toggleoptionsgroup = self.optionsgroup.toggleViewAction() self.toggleoptionsgroup.setText("show/hide configurations") menubar.addAction(self.toggleoptionsgroup) self.optionsgroup.setWidget(self.optionsgroupwidget) self.addDockWidget(Qt.LeftDockWidgetArea, self.optionsgroup) self.vlayout0.addWidget(self.splitter) self.hlayout0.addWidget(self.navitools) self.vlayout0.addLayout(self.hlayout0) ############################################## # connect output widgets to external signals # ############################################## self._model.signal_changed.connect(self.plot_signal) self._model.marker_changed.connect(self.plot_marker) self._model.peaks_changed.connect(self.plot_peaks) self._model.period_changed.connect(self.plot_period) self._model.rate_changed.connect(self.plot_rate) self._model.tidalamp_changed.connect(self.plot_tidalamp) self._model.path_changed.connect(self.display_path) self._model.segment_changed.connect(self.plot_segment) self._model.status_changed.connect(self.display_status) self._model.progress_changed.connect(self.display_progress) self._model.model_reset.connect(self.reset_plot) ########### # methods # ########### def plot_signal(self, value): self.ax00.clear() self.ax00.relim() # reset navitools history self.navitools.update() self.line00 = self.ax00.plot(self._model.sec, value, zorder=1) self.ax00.set_xlabel("seconds", fontsize="large", fontweight="heavy") self.canvas0.draw() # print("plot_signal listening") # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_peaks(self, value): # self.scat is listed in ax.collections if self.ax00.collections: self.ax00.collections[0].remove() self.scat = self.ax00.scatter(self._model.sec[value], self._model.signal[value], c="m", zorder=2) self.canvas0.draw() # print("plot_peaks listening") # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_segment(self, value): # If an invalid signal has been selected reset the segmenter interface. if value is None: self.toggle_segmenter(1) return if self.ax00.patches: # self.segementspan is listed in ax.patches self.ax00.patches[0].remove() self.segmentspan = self.ax00.axvspan(value[0], value[1], color="m", alpha=0.25) self.canvas0.draw() self.confirmedit.setEnabled(True) # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_marker(self, value): self.ax10.clear() self.ax10.relim() self.line10 = self.ax10.plot(value[0], value[1]) self.canvas1.draw() # print("plot_marker listening") def plot_period(self, value): self.ax20.clear() self.ax20.relim() self.navitools.home() if self._model.savestats["period"]: self.line20 = self.ax20.plot(self._model.sec, value, c="m") else: self.line20 = self.ax20.plot(self._model.sec, value) self.ax20.set_ylim(bottom=min(value), top=max(value)) self.ax20.set_title("period", pad=0, fontweight="heavy") self.ax20.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_period listening") def plot_rate(self, value): self.ax21.clear() self.ax21.relim() self.navitools.home() if self._model.savestats["rate"]: self.line21 = self.ax21.plot(self._model.sec, value, c="m") else: self.line21 = self.ax21.plot(self._model.sec, value) self.ax21.set_ylim(bottom=min(value), top=max(value)) self.ax21.set_title("rate", pad=0, fontweight="heavy") self.ax21.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_rate listening") def plot_tidalamp(self, value): self.ax22.clear() self.ax22.relim() self.navitools.home() if self._model.savestats["tidalamp"]: self.line22 = self.ax22.plot(self._model.sec, value, c="m") else: self.line22 = self.ax22.plot(self._model.sec, value) self.ax22.set_ylim(bottom=min(value), top=max(value)) self.ax22.set_title("amplitude", pad=0, fontweight="heavy") self.ax22.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_tidalamp listening") def display_path(self, value): self.currentFile.setText(value) def display_status(self, status): # display status until new status is set self.statusBar.showMessage(status) def display_progress(self, value): # if value is 0, the progressbar indicates a busy state self.progressBar.setRange(0, value) def toggle_segmenter(self, value): if not self._model.loaded: return # Open segmenter when called from signalmenu or clear segmenter # upon selection of invalid segment. if value == 1: self.segmenter.setVisible(True) self.confirmedit.setEnabled(False) self.startedit.clear() self.endedit.clear() if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() # Close segmenter after segment has been confirmed. elif value == 0: self.segmenter.setVisible(False) if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() # Close segmenter after segmentation has been aborted (reset # segment). elif value == 2: self._model.set_segment([0, 0]) # This will reset the model to None self.segmenter.setVisible(False) if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() def enable_segmentedit(self): # disable peak editing to avoid interference self.editcheckbox.setChecked(False) if self.startedit.hasFocus(): self.segmentcursor = "start" elif self.endedit.hasFocus(): self.segmentcursor = "end" def set_customheader(self): """Populate the customheader with inputs from the customfiledialog""" # Check if one of the mandatory fields is missing. mandatoryfields = self.signaledit.text() and self.headerrowsedit.text( ) and self.sfreqedit.text() if not mandatoryfields: self._model.status = ( "Please provide values for 'biosignal column'" ", 'number of header rows' and 'sampling" " rate'.") return seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} self._model.customheader = dict.fromkeys( self._model.customheader, None ) # reset header here since it cannot be reset in controller.get_fpaths() self._model.customheader["signalidx"] = int(self.signaledit.text()) self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) self._model.customheader["sfreq"] = int(self.sfreqedit.text()) self._model.customheader["separator"] = seps[ self.separatormenu.currentText()] if self.markeredit.text(): # not mandatory self._model.customheader["markeridx"] = int(self.markeredit.text()) self.customfiledialog.done(QDialog.Accepted) # close the dialog window self._controller.get_fpaths() # move on to file selection def get_xcursor(self, event): # event.button 1 corresponds to left mouse button if event.button != 1: return # limit number of decimal places to two if self.segmentcursor == "start": self.startedit.selectAll() self.startedit.insert("{:.2f}".format(event.xdata)) elif self.segmentcursor == "end": self.endedit.selectAll() self.endedit.insert("{:.2f}".format(event.xdata)) # disable segment cursor again after value has been set self.segmentcursor = False def select_stats(self, event): """ select or deselect statistics to be saved; toggle boolean with xor operator ^=, toggle color with dictionary """ self._model.savestats[event] ^= True line = None if event == "period": if self.line20: line = self.line20[0] elif event == "rate": if self.line21: line = self.line21[0] elif event == "tidalamp": if self.line22: line = self.line22[0] if line: line.set_color(self.togglecolors[line.get_color()]) self.canvas2.draw() def toggle_options(self, event): if event in ["ECG", "PPG"]: self.tidalampcheckbox.setEnabled(False) self.tidalampcheckbox.setChecked(False) self.ax22.set_visible(False) self.canvas2.draw() elif event == "RESP": self.tidalampcheckbox.setEnabled(True) self.ax22.set_visible(True) self.canvas2.draw() elif event == "multiple files": self.editcheckbox.setEnabled(False) self.editcheckbox.setChecked(False) self.savecheckbox.setEnabled(True) self.correctcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(False) elif event == "single file": self.editcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(True) self.savecheckbox.setEnabled(False) self.savecheckbox.setChecked(False) self.correctcheckbox.setEnabled(False) self.correctcheckbox.setChecked(False) def reset_plot(self): self.ax00.clear() self.ax00.relim() self.line00 = None self.scat = None self.segmentspan = None self.ax10.clear() self.ax10.relim() self.line10 = None self.ax20.clear() self.ax20.relim() self.line20 = None self.ax21.clear() self.ax21.relim() self.line21 = None self.ax22.clear() self.ax22.relim() self.line22 = None self.canvas0.draw() self.canvas1.draw() self.canvas2.draw() self.navitools.update() self.currentFile.clear()
def executeProject(self, mainWindow, project_name): if project_name == "SmartGroceries": self.referenceItemsLabels = {} self.itemInHand = None self.itemsInCart = list() self.currentOrderReferenceItems = list() self.currentOrderUserId = 0 self.recommendedItemsButtons = list() self.previousRecommendations = list() self.numberOfGoodRecommendations = 0 self.supportedUsers = range(1, 1001) products = pd.read_csv( os.path.join( os.path.dirname(os.path.dirname( os.path.abspath(__file__))), "projects", "SmartGroceries", "data", "products.csv")) aisles = pd.read_csv( os.path.join( os.path.dirname(os.path.dirname( os.path.abspath(__file__))), "projects", "SmartGroceries", "data", "aisles.csv")) test_data = pd.read_csv( os.path.join( os.path.dirname(os.path.dirname( os.path.abspath(__file__))), "projects", "SmartGroceries", "data", "test_set.csv")) # print(products) # print(aisles) # print(test_data) aisle_name_to_id = { k: v for k, v in zip(aisles.aisle, aisles.aisle_id) } product_name_to_id = { k: v for k, v in zip(products.product_name, products.product_id) } product_id_to_name = { k: v for k, v in zip(products.product_id, products.product_name) } def changeCurrentitem(itemName): if itemName == "": self.itemInHand = None currentItemLabel.setText( f"<b>Select an item from the list or from the recommendations</b>s" ) addToCartButton.setEnabled(False) else: self.itemInHand = product_name_to_id[itemName] currentItemLabel.setText(f"Add <b>{itemName}</b> to Cart") addToCartButton.setEnabled(True) addToCartButton.setFocus() def handleNewOrderButtonClicked(): grouped = test_data.groupby('order_id') while True: order_number = random.sample(grouped.indices.keys(), 1)[0] currentOrder = grouped.get_group(order_number) self.currentOrderReferenceItems = currentOrder.product_id.tolist( ) self.currentOrderUserId = currentOrder.user_id.iloc[0] if len( self.currentOrderReferenceItems ) > 1 and self.currentOrderUserId in self.supportedUsers: break print(self.currentOrderReferenceItems) orderInfo = f"<b>Order ID: </b>{currentOrder.order_id.iloc[0]}<br>" orderInfo += f"<b>User ID: </b>{self.currentOrderUserId} | <b>DOW: </b>{calendar.day_name[currentOrder.order_dow.iloc[0]]} | <b>Hour of Day: </b>{currentOrder.order_hour_of_day.iloc[0]} | <b>Number of Items: </b>{len(self.currentOrderReferenceItems)}" orderInfo += "<br><b>Items in the Reference Order:</b>" for widget in self.referenceItemsLabels.values(): item = referenceItemsLayout.itemAt(0) widget.setVisible(False) referenceItemsLayout.removeItem(item) del item self.referenceItemsLabels.clear() currentCartItems.clear() self.itemsInCart.clear() self.previousRecommendations.clear() self.numberOfGoodRecommendations = 0 updateCurrentRecommendations(list()) for product in self.currentOrderReferenceItems: refItemName = product_id_to_name[product] refItemLabel = QPushButton(refItemName) refItemLabel.setContentsMargins(QMargins(0, 0, 0, 0)) refItemLabel.setStyleSheet("Text-align:left") refItemLabel.setFlat(False) refItemLabel.clicked.connect( partial(changeCurrentitem, refItemName)) self.referenceItemsLabels[product] = refItemLabel orderInfoLabel.setText( f"<b>Order Information</b><br>{orderInfo}") for referenceItemLabel in self.referenceItemsLabels.values(): referenceItemsLayout.addWidget(referenceItemLabel) runAutoButton.setFocus() def handleRunAutomatically(): for referenceItemLabel in self.referenceItemsLabels.values(): referenceItemLabel.click() addToCartButton.click() def updateCurrentRecommendations(recommendations): for widget in self.recommendedItemsButtons: item = recommendationsLayout.itemAt(0) widget.setVisible(False) recommendationsLayout.removeItem(item) del item self.recommendedItemsButtons.clear() for product in recommendations: recItemName = product_id_to_name[product] recItemButton = QPushButton(recItemName) recItemButton.setContentsMargins(QMargins(0, 0, 0, 0)) recItemButton.setStyleSheet("Text-align:left;") if product not in self.currentOrderReferenceItems: recItemButton.setFlat(True) recItemButton.clicked.connect( partial(changeCurrentitem, recItemName)) self.recommendedItemsButtons.append(recItemButton) for recItemButton in self.recommendedItemsButtons: recommendationsLayout.addWidget(recItemButton) if len(recommendations) > 0: currentRecommendationsLabel.setVisible(True) else: currentRecommendationsLabel.setVisible(False) self.previousRecommendations += recommendations def handleAddToCartButtonClicked(): print(self.currentOrderReferenceItems) print(self.itemInHand) if self.itemInHand not in self.currentOrderReferenceItems: QMessageBox( QMessageBox.Critical, "Error adding item to cart", "You can only add items that exists in the reference order" ).exec_() return elif self.itemInHand in self.itemsInCart: QMessageBox(QMessageBox.Critical, "Error adding item to cart", "This item is already in the cart").exec_() return self.referenceItemsLabels[self.itemInHand].setFlat(True) self.itemsInCart.append(self.itemInHand) currentCartItems.addItem(product_id_to_name[self.itemInHand]) if self.itemInHand in self.previousRecommendations: self.numberOfGoodRecommendations += 1 self.referenceItemsLabels[self.itemInHand].setStyleSheet( "Text-align:left; background-color:green;") self.referenceItemsLabels[self.itemInHand].setFlat(False) #update recommendations result = self._sendCommand( "PROCESS_PROJECT_GROUP_2", ";".join([ str(self.currentOrderUserId), ",".join([str(x) for x in self.itemsInCart]), ",".join([ str(x) for x in set(self.previousRecommendations) ]) ])) if result == FAILURE_CODE: self.log( "Processing Failed, error getting recommendations from the RPi" ) return else: try: recommendations = [int(id) for id in result.split(',')] except: recommendations = [] updateCurrentRecommendations(recommendations) if len(self.itemsInCart) == len( self.currentOrderReferenceItems): completionMessage = QMessageBox( QMessageBox.Information, "Order Completed", f"Order Completed with {self.numberOfGoodRecommendations} Good Recommendation(s)\nPress New Order to start a new order" ) if self.numberOfGoodRecommendations == 0: completionMessage.setIconPixmap( QPixmap('images/this_is_fine.jpg')) completionMessage.setWindowIcon(appIcon) completionMessage.exec_() newOrderButton.setFocus() def aisleChanged(): aisle_number = aisle_name_to_id[ selectAisleCombobox.currentText()] products_in_aisle = products[ products.aisle_id == aisle_number].product_name.tolist() selectproductCombobox.clear() selectproductCombobox.addItem("") selectproductCombobox.addItems(products_in_aisle) def itemChanged(): current_item = selectproductCombobox.currentText() changeCurrentitem(current_item) dialog = QDialog(mainWindow) appIcon = QIcon("images/this_is_fine.jpg") dialog.setWindowIcon(appIcon) dialog.setMinimumWidth(600) dialog.setWindowTitle("Smart Groceries Demo") layout = QVBoxLayout() newOrderButton = QPushButton("New Order") orderInfoLabel = QLabel() orderInfoLabel.setTextFormat(Qt.RichText) chooseItemLayout = QHBoxLayout() verticalSpacer = QSpacerItem(20, 20) currentCartItems = QListWidget() layoutWidget = QWidget() referenceItemsLayout = QVBoxLayout(layoutWidget) referenceItemsLayout.setSpacing(0) referenceItemsLayout.setMargin(0) scroll = QScrollArea(dialog) scroll.setWidgetResizable(True) scroll.setMinimumHeight(150) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll.setWidget(layoutWidget) selectAisleLabel = QLabel("Aisle: ") selectProductLabel = QLabel("Product: ") selectAisleCombobox = QComboBox() selectproductCombobox = QComboBox() chooseItemLayout.addWidget(selectAisleLabel, 0, Qt.AlignLeft) chooseItemLayout.addWidget(selectAisleCombobox, 0, Qt.AlignLeft) chooseItemLayout.addWidget(selectProductLabel, 0, Qt.AlignLeft) chooseItemLayout.addWidget(selectproductCombobox, 0, Qt.AlignLeft) addToCartButton = QPushButton("Add to Cart") currentItemLabel = QLabel() currentItemLabel.setTextFormat(Qt.RichText) if self.itemInHand is None: currentItemLabel.setText( f"<b>Select an item from the list or from the recommendations</b>" ) addToCartButton.setDisabled(True) currentItemLayout = QHBoxLayout() currentItemLayout.addWidget(currentItemLabel) currentItemLayout.addWidget(addToCartButton) recommendationsLayout = QVBoxLayout() recommendationsLayout.setSpacing(0) recommendationsLayout.setMargin(0) newOrderButton.clicked.connect(handleNewOrderButtonClicked) addToCartButton.clicked.connect(handleAddToCartButtonClicked) selectproductCombobox.currentIndexChanged.connect(itemChanged) selectAisleCombobox.currentIndexChanged.connect(aisleChanged) selectAisleCombobox.addItems(aisles.aisle.tolist()) layout.addWidget(newOrderButton) layout.addSpacerItem(verticalSpacer) layout.addWidget(orderInfoLabel) layout.addWidget(scroll) layout.addSpacerItem(verticalSpacer) layout.addLayout(chooseItemLayout) layout.addSpacerItem(verticalSpacer) itemsInTheCartLabel = QLabel("<b>Items in the Cart<b>") layout.addWidget(itemsInTheCartLabel) itemsInTheCartLabel.setTextFormat(Qt.RichText) layout.addWidget(currentCartItems) layout.addSpacerItem(verticalSpacer) currentRecommendationsLabel = QLabel( "<b>Current Recommendations<b>") layout.addWidget(currentRecommendationsLabel) currentRecommendationsLabel.setTextFormat(Qt.RichText) currentRecommendationsLabel.setVisible(False) layout.addLayout(recommendationsLayout) layout.addSpacerItem(verticalSpacer) layout.addLayout(currentItemLayout) runAutoButton = QPushButton("Run and Watch. TRUST ME, IT IS FUN!") layout.addWidget(runAutoButton) runAutoButton.clicked.connect(handleRunAutomatically) dialog.setLayout(layout) handleNewOrderButtonClicked() dialog.exec_() return
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 TutorialDialog(object): def __init__(self, parent, dp=None): self._dialog = QDialog(parent) self._dp = dp self._dialog.setWindowIcon(QIcon(':/images/icon.png')) self._ui = Ui_Dialog() self._ui.setupUi(self._dialog) self._current_index = 0 self._ui.slides.setCurrentIndex(self._current_index) self._slides_count = self._ui.slides.count() self._ui.next_button.clicked.connect(self._on_next_button_clicked) self._ui.prev_button.clicked.connect(self._on_prev_button_clicked) self._point_enabled_style = "background-color:#f9af61; " \ "border: 2px solid; " \ "border-radius: 3px; " \ "border-color:#f9af61;" self._point_disabled_style = "background-color:#cccccc; " \ "border: 2px solid; " \ "border-radius: 3px; " \ "border-color:#cccccc;" self._points = [self._ui.point] self._init_points() self._setup_buttons() self._set_labels_fonts() self._set_controls_font([self._ui.next_button, self._ui.prev_button]) self._slide_show = SlideShow(self._dialog, self._ui.slides) self._slide_show.current_index_changed.connect( self._on_current_index_changed) self._slide_show.clicked.connect(self._on_next_button_clicked) self._slide_show.key_pressed.connect(self._on_key_pressed) self._dialog.keyPressEvent = self._on_key_pressed def _on_next_button_clicked(self): if self._current_index + 1 >= self._slides_count: self._close() return self._slide_show.setCurrentIndex(self._current_index + 1) def _on_prev_button_clicked(self): if self._current_index - 1 < 0: return self._slide_show.setCurrentIndex(self._current_index - 1) def _on_key_pressed(self, ev): if ev.key() == Qt.Key_Left: self._on_prev_button_clicked() elif ev.key() == Qt.Key_Right: self._on_next_button_clicked() def _on_current_index_changed(self, new_index): self._current_index = new_index self._setup_buttons() def _setup_buttons(self): if self._current_index == 0: self._ui.prev_button.setDisabled(True) self._ui.prev_button.setStyleSheet( "border: 0; color:#ffffff; text-align:left;") else: self._ui.prev_button.setDisabled(False) self._ui.prev_button.setStyleSheet( "border: 0; color:#222222; text-align:left;") if self._current_index + 1 == self._slides_count: self._ui.next_button.setText(tr("GOT IT")) else: self._ui.next_button.setText(tr("NEXT")) self._setup_points() def _init_points(self): self._points[0].setText(' ') self._points[0].setFont(QFont("Noto Sans", 2)) for i in range(1, self._slides_count): new_point = QLabel() new_point.setText(' ') new_point.setFont(QFont("Noto Sans", 2)) new_point.setStyleSheet(self._point_disabled_style) self._points.append(new_point) self._ui.points_layout.addSpacing(8) self._ui.points_layout.addWidget(new_point) def _setup_points(self): for i, point in enumerate(self._points): style = self._point_enabled_style if i == self._current_index \ else self._point_disabled_style point.setStyleSheet(style) def _set_labels_fonts(self): self._set_controls_font(self._ui.slides.findChildren(QLabel)) def _set_controls_font(self, controls): if not self._dp or self._dp == 1: return for control in controls: font = control.font() font_size = font.pointSize() * self._dp control_font = QFont(font.family(), font_size, italic=font.italic()) control_font.setBold(font.bold()) control.setFont(control_font) def _close(self): self._slide_show.current_index_changed.disconnect( self._on_current_index_changed) self._dialog.accept() self._dialog.close() def show(self): logger.debug("Opening tutorial dialog") # Execute dialog self._dialog.exec_()
class MainWindow(QDialog, Ui_Form_name, Ui_message_box): def __init__(self): super().__init__() self.windows = Ui_Form_name() self.windows.setupUi(self) self.windows.toolButton.clicked.connect(self.file_opener) self.windows.toolButton_2.clicked.connect(self.file_destination) self.windows.pushButton.clicked.connect(self.converter) self.setWindowIcon(QIcon("icons/image.ico")) def file_opener(self): global file_name file_name = QFileDialog.getOpenFileName(None,"Open File", "c:\\users\\{}\\desktop".format(getpass.getuser()),"PDF files(*.pdf)") self.windows.lineEdit_4.setText(file_name[0].split("/")[-1]) def file_destination(self): global file_destination file_destination = QFileDialog.getExistingDirectory(None,"Destination", "c:\\users\\{}\\desktop".format(getpass.getuser())) self.windows.lineEdit_5.setText(file_destination.split('/')[-1]) def converter(self): if self.windows.lineEdit.text() and self.windows.lineEdit_5.text() and self.windows.lineEdit_4.text(): try: date_extracted = tabula.read_pdf("{}".format(file_name[0]), pages='{}'.format(self.windows.lineEdit.text()), lattice=True) data_frame = DataFrame(date_extracted) data_frame.to_excel("{}/{}.xlsx".format(file_destination,file_name[0].split("/")[-1])) self.window2 = QDialog() self.message_box = Ui_message_box() self.message_box.setupUi(self.window2) self.window2.setWindowModality(QtCore.Qt.ApplicationModal) self.window2.show() self.window2.setWindowIcon(QIcon("icons/image.ico")) self.message_box.pushButton.clicked.connect(self.close_message) except Exception as e: print(e) self.window4 = QDialog() self.message_box3 = Ui_error_message() self.message_box3.setupUi(self.window4) self.window4.setWindowModality(QtCore.Qt.ApplicationModal) self.window4.show() self.window4.setWindowIcon(QIcon("image.ico")) self.message_box3.pushButton.clicked.connect(self.error_warning) else: self.window3 = QDialog() self.message_box2 = Ui_invalid_message() self.message_box2.setupUi(self.window3) self.window3.setWindowModality(QtCore.Qt.ApplicationModal) self.window3.show() self.window3.setWindowIcon(QIcon("icons/image.ico")) self.message_box2.pushButton.clicked.connect(self.invalid_warning) def close_message(self): webbrowser.open("{}".format(file_destination)) self.window2.close() def invalid_warning(self): self.window3.close() def error_warning(self): self.window4.close()
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 Start(QWidget): def __init__(self): super().__init__() self.setWindowTitle('扫雷') self.setWindowIcon(QIcon(':/minesweeper.ico')) self.setFixedSize(1000, 700) self.setMouseTracking(True) self.setWindowFlags(Qt.FramelessWindowHint) self.btn = QPushButton(self) self.btn.setMouseTracking(True) self.btn.setIcon(QIcon(':/开始游戏.png')) self.btn.setIconSize(QSize(280, 200)) self.btn.setStyleSheet('QPushButton{border:None}') self.btn.move(350, 370) self.btn.clicked.connect(self.show_mode) self.msw = MineSweeperWindow(MineSweeper(0, 0, 0)) self.show() def show_mode(self): self.choose = QDialog() self.choose.setWindowIcon(QIcon(':/minesweeper.ico')) self.choose.setFixedSize(300, 300) self.close() btn1 = QPushButton('简单', self.choose) btn1.move(90, 50) btn1.clicked.connect(self.set_easy) btn2 = QPushButton('中等', self.choose) btn2.move(90, 100) btn2.clicked.connect(self.set_medium) btn3 = QPushButton('困难', self.choose) btn3.move(90, 150) btn3.clicked.connect(self.set_hard) btn4 = QPushButton('自定义', self.choose) btn4.move(90, 200) btn4.clicked.connect(self.set_free) self.choose.setWindowTitle('模式选择') self.choose.setWindowModality(Qt.ApplicationModal) self.choose.show() def paintEvent(self, e): painter = QPainter(self) background = QPixmap(':/封面2.jpg') painter.drawPixmap(self.rect(), background) def mouseMoveEvent(self, e): if 400 <= e.x() <= 580 and 420 <= e.y() <= 520: self.btn.setGeometry(340, 366, 300, 214) self.btn.setIconSize(QSize(300, 214)) else: self.btn.setGeometry(350, 370, 280, 200) self.btn.setIconSize(QSize(280, 200)) def set_easy(self): self.choose.close() self.msw = MineSweeperWindow(EasyMode()) self.msw.show() def set_medium(self): self.choose.close() self.msw = MineSweeperWindow(MediumMode()) self.msw.show() def set_hard(self): self.choose.close() self.msw = MineSweeperWindow(HardMode()) self.msw.show() def set_free(self): self.sf = SetFree() self.choose.close() self.sf.show()
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 SmartSyncDialog(object): def __init__(self, parent): self._dialog = QDialog(parent) self._dialog.setWindowIcon(QIcon(':/images/icon.png')) self._ui = Ui_Dialog() self._ui.setupUi(self._dialog) self._ui.centralWidget.setFrameShape(QFrame.NoFrame) self._ui.centralWidget.setLineWidth(1) self._model = None self._proxy_model = QSortFilterProxyModel() self._view = self._ui.folder_list_view self._view.setModel(self._proxy_model) self._view.expanded.connect(self.on_item_expanded) self._offline_paths = None # for frameless window moving self._x_coord = 0 self._y_coord = 0 self._dialog.mousePressEvent = self.on_mouse_press_event self._dialog.mouseMoveEvent = self.on_mouse_move_event self._loader_movie = QMovie(":/images/loader.gif") self._ui.loader_label.setMovie(self._loader_movie) def on_mouse_press_event(self, ev): self._x_coord = ev.x() self._y_coord = ev.y() def on_mouse_move_event(self, ev): self._dialog.move( ev.globalX() - self._x_coord, ev.globalY() - self._y_coord) def on_item_expanded(self, index): if self._model: self._model.on_item_expanded(self._proxy_model.mapToSource(index)) def show(self, root_path, hide_dotted=False): if LOGGING_ENABLED: logger.info( "Opening smart sync dialog for path '%s'...", root_path) self._model = TreeModel( root_path, hide_dotted=hide_dotted) self._proxy_model.setSourceModel(self._model) self.show_cursor_loading(show_movie=True) # Execute dialog result = self._dialog.exec_() if result == QDialog.Accepted: offline_dirs = self._model.get_added_to_offline_paths() new_online = list(self._model.get_removed_from_offline_paths()) new_offline = list(offline_dirs - self._offline_paths) if LOGGING_ENABLED: logger.debug("new offline dirs %s, new online dirs %s", new_offline, new_online) return new_offline, new_online else: return [], [] def set_offline_paths(self, offline_paths): self._offline_paths = offline_paths logger.debug("offline paths %s", offline_paths) self._model.set_offline_dirs(offline_paths) self.show_cursor_normal() self._view.expand(self._proxy_model.mapFromSource( self._model.get_root_path_index())) self._proxy_model.sort(0, Qt.AscendingOrder) 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) def show_cursor_normal(self): self._dialog.setCursor(Qt.ArrowCursor) if self._loader_movie.state() == QMovie.Running: self._loader_movie.stop() self._ui.stackedWidget.setCurrentIndex(0)
class View(QMainWindow): """View component of the MVC application. Presents the state of the application as well as the available means of interaction. Receives updates about the state from the Model and informs Controller about user interactions. """ def __init__(self, model, controller): """Define GUI elements and their layout. Parameters ---------- model : QObject Model component of the MVC application. controller : QObject Controller component of the MVC application. """ super().__init__() self._model = model self._controller = controller self.segmentcursor = False self.togglecolors = {"#1f77b4": "m", "m": "#1f77b4"} self.setWindowTitle("biopeaks") self.setGeometry(50, 50, 1750, 750) self.setWindowIcon(QIcon(":/python_icon.png")) # Figure for biosignal. self.figure0 = Figure() self.canvas0 = FigureCanvas(self.figure0) # Enforce minimum height, otherwise resizing with self.splitter causes # mpl to throw an error because figure is resized to height 0. The # widget can still be fully collapsed with self.splitter. self.canvas0.setMinimumHeight(1) # in pixels self.ax00 = self.figure0.add_subplot(1, 1, 1) self.ax00.set_frame_on(False) self.figure0.subplots_adjust(left=0.04, right=0.98, bottom=0.25) self.line00 = None self.scat = None self.segmentspan = None # Figure for marker. self.figure1 = Figure() self.canvas1 = FigureCanvas(self.figure1) self.canvas1.setMinimumHeight(1) self.ax10 = self.figure1.add_subplot(1, 1, 1, sharex=self.ax00) self.ax10.get_xaxis().set_visible(False) self.ax10.set_frame_on(False) self.figure1.subplots_adjust(left=0.04, right=0.98) self.line10 = None # Figure for statistics. self.figure2 = Figure() self.canvas2 = FigureCanvas(self.figure2) self.canvas2.setMinimumHeight(1) self.ax20 = self.figure2.add_subplot(3, 1, 1, sharex=self.ax00) self.ax20.get_xaxis().set_visible(False) self.ax20.set_frame_on(False) self.line20 = None self.ax21 = self.figure2.add_subplot(3, 1, 2, sharex=self.ax00) self.ax21.get_xaxis().set_visible(False) self.ax21.set_frame_on(False) self.line21 = None self.ax22 = self.figure2.add_subplot(3, 1, 3, sharex=self.ax00) self.ax22.get_xaxis().set_visible(False) self.ax22.set_frame_on(False) self.line22 = None self.figure2.subplots_adjust(left=0.04, right=0.98) self.navitools = CustomNavigationToolbar(self.canvas0, self) # Peak editing. self.editcheckbox = QCheckBox("editable", self) self.editcheckbox.stateChanged.connect(self._model.set_peakseditable) # Peak saving during batch processing. self.savecheckbox = QCheckBox("save during batch processing", self) self.savecheckbox.stateChanged.connect(self._model.set_savebatchpeaks) # Peak auto-correction during batch processing. self.correctcheckbox = QCheckBox("correct during batch processing", self) self.correctcheckbox.stateChanged.connect( self._model.set_correctbatchpeaks) # Selection of stats for saving. self.periodcheckbox = QCheckBox("period", self) self.periodcheckbox.stateChanged.connect( lambda: self.select_stats("period")) self.ratecheckbox = QCheckBox("rate", self) self.ratecheckbox.stateChanged.connect( lambda: self.select_stats("rate")) self.tidalampcheckbox = QCheckBox("tidal amplitude", self) self.tidalampcheckbox.stateChanged.connect( lambda: self.select_stats("tidalamp")) # Channel selection. self.sigchanmenulabel = QLabel("biosignal") self.sigchanmenu = QComboBox(self) self.sigchanmenu.addItem("A1") self.sigchanmenu.addItem("A2") self.sigchanmenu.addItem("A3") self.sigchanmenu.addItem("A4") self.sigchanmenu.addItem("A5") self.sigchanmenu.addItem("A6") self.sigchanmenu.currentTextChanged.connect(self._model.set_signalchan) self._model.set_signalchan( self.sigchanmenu.currentText()) # initialize with default value self.markerchanmenulabel = QLabel("marker") self.markerchanmenu = QComboBox(self) self.markerchanmenu.addItem("none") self.markerchanmenu.addItem("I1") self.markerchanmenu.addItem("I2") self.markerchanmenu.addItem("A1") self.markerchanmenu.addItem("A2") self.markerchanmenu.addItem("A3") self.markerchanmenu.addItem("A4") self.markerchanmenu.addItem("A5") self.markerchanmenu.addItem("A6") self.markerchanmenu.currentTextChanged.connect( self._model.set_markerchan) self._model.set_markerchan(self.markerchanmenu.currentText()) # Processing mode. self.batchmenulabel = QLabel("mode") self.batchmenu = QComboBox(self) self.batchmenu.addItem("single file") self.batchmenu.addItem("multiple files") self.batchmenu.currentTextChanged.connect(self._model.set_batchmode) self.batchmenu.currentTextChanged.connect(self.toggle_options) self._model.set_batchmode(self.batchmenu.currentText()) self.toggle_options(self.batchmenu.currentText()) # Modality selection. self.modmenulabel = QLabel("modality") self.modmenu = QComboBox(self) self.modmenu.addItem("ECG") self.modmenu.addItem("PPG") self.modmenu.addItem("RESP") self.modmenu.currentTextChanged.connect(self._model.set_modality) self.modmenu.currentTextChanged.connect(self.toggle_options) self._model.set_modality(self.modmenu.currentText()) self.toggle_options(self.modmenu.currentText()) # Segment selection. This widget can be openend / set visible from # the menu and closed from within itself (see mapping of segmentermap). self.segmentermap = QSignalMapper(self) self.segmenter = QDockWidget("select a segment", self) self.segmenter.setFeatures( QDockWidget.NoDockWidgetFeatures ) # disable closing such that widget can only be closed by confirming selection or custom button regex = QRegExp( "[0-9]*\.?[0-9]{4}") # Limit number of decimals to four validator = QRegExpValidator(regex) self.startlabel = QLabel("start") self.startedit = QLineEdit() self.startedit.setValidator(validator) self.endlabel = QLabel("end") self.endedit = QLineEdit() self.endedit.setValidator(validator) segmentfromcursor = QAction(QIcon(":/mouse_icon.png"), "select with mouse", self) segmentfromcursor.triggered.connect(self.enable_segmentedit) self.startedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.endedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.previewedit = QPushButton("preview segment") lambdafn = lambda: self._model.set_segment( [self.startedit.text(), self.endedit.text()]) self.previewedit.clicked.connect(lambdafn) self.confirmedit = QPushButton("confirm segment") self.confirmedit.clicked.connect(self._controller.segment_dataset) self.confirmedit.clicked.connect(self.segmentermap.map) self.segmentermap.setMapping(self.confirmedit, 0) self.abortedit = QPushButton("abort segmentation") self.abortedit.clicked.connect(self.segmentermap.map) self.segmentermap.setMapping(self.abortedit, 2) # resets the segment to None self.segmenterlayout = QFormLayout() self.segmenterlayout.addRow(self.startlabel, self.startedit) self.segmenterlayout.addRow(self.endlabel, self.endedit) self.segmenterlayout.addRow(self.previewedit) self.segmenterlayout.addRow(self.confirmedit) self.segmenterlayout.addRow(self.abortedit) self.segmenterwidget = QWidget() self.segmenterwidget.setLayout(self.segmenterlayout) self.segmenter.setWidget(self.segmenterwidget) self.segmenter.setVisible(False) self.segmenter.setAllowedAreas(Qt.RightDockWidgetArea) self.addDockWidget(Qt.RightDockWidgetArea, self.segmenter) # Custom file dialog. regex = QRegExp("[1-9][0-9]") validator = QRegExpValidator(regex) self.signallabel = QLabel("biosignal column") self.signaledit = QLineEdit() self.signaledit.setValidator(validator) self.markerlabel = QLabel("marker column") self.markeredit = QLineEdit() self.markeredit.setValidator(validator) regex = QRegExp("[0-9]{2}") validator = QRegExpValidator(regex) self.headerrowslabel = QLabel("number of header rows") self.headerrowsedit = QLineEdit() self.headerrowsedit.setValidator(validator) regex = QRegExp("[0-9]{5}") validator = QRegExpValidator(regex) self.sfreqlabel = QLabel("sampling rate") self.sfreqedit = QLineEdit() self.sfreqedit.setValidator(validator) self.separatorlabel = QLabel("column separator") self.separatormenu = QComboBox(self) self.separatormenu.addItem("comma") self.separatormenu.addItem("tab") self.separatormenu.addItem("colon") self.separatormenu.addItem("space") self.continuecustomfile = QPushButton("continue loading file") self.continuecustomfile.clicked.connect(self.set_customheader) self.customfiledialog = QDialog() self.customfiledialog.setWindowTitle("custom file info") self.customfiledialog.setWindowIcon(QIcon(":/file_icon.png")) self.customfiledialog.setWindowFlags( Qt.WindowCloseButtonHint ) # remove help button by only setting close button self.customfilelayout = QFormLayout() self.customfilelayout.addRow(self.signallabel, self.signaledit) self.customfilelayout.addRow(self.markerlabel, self.markeredit) self.customfilelayout.addRow(self.separatorlabel, self.separatormenu) self.customfilelayout.addRow(self.headerrowslabel, self.headerrowsedit) self.customfilelayout.addRow(self.sfreqlabel, self.sfreqedit) self.customfilelayout.addRow(self.continuecustomfile) self.customfiledialog.setLayout(self.customfilelayout) # Layout. menubar = self.menuBar() signalmenu = menubar.addMenu("biosignal") openSignal = signalmenu.addMenu("load") openEDF = QAction("EDF", self) openEDF.triggered.connect(lambda: self._model.set_filetype("EDF")) openEDF.triggered.connect(self._controller.load_channels) openSignal.addAction(openEDF) openOpenSignals = QAction("OpenSignals", self) openOpenSignals.triggered.connect( lambda: self._model.set_filetype("OpenSignals")) openOpenSignals.triggered.connect(self._controller.load_channels) openSignal.addAction(openOpenSignals) openCustom = QAction("Custom", self) openCustom.triggered.connect( lambda: self._model.set_filetype("Custom")) openCustom.triggered.connect(lambda: self.customfiledialog.exec_()) openSignal.addAction(openCustom) segmentSignal = QAction("select segment", self) segmentSignal.triggered.connect(self.segmentermap.map) self.segmentermap.setMapping(segmentSignal, 1) signalmenu.addAction(segmentSignal) self.segmentermap.mapped.connect(self.toggle_segmenter) saveSignal = QAction("save", self) saveSignal.triggered.connect(self._controller.save_channels) signalmenu.addAction(saveSignal) peakmenu = menubar.addMenu("peaks") findPeaks = QAction("find", self) findPeaks.triggered.connect(self._controller.find_peaks) peakmenu.addAction(findPeaks) autocorrectPeaks = QAction("autocorrect", self) autocorrectPeaks.triggered.connect(self._controller.autocorrect_peaks) peakmenu.addAction(autocorrectPeaks) savePeaks = QAction("save", self) savePeaks.triggered.connect(self._controller.save_peaks) peakmenu.addAction(savePeaks) loadPeaks = QAction("load", self) loadPeaks.triggered.connect(self._controller.load_peaks) peakmenu.addAction(loadPeaks) statsmenu = menubar.addMenu("statistics") calculateStats = QAction("calculate", self) calculateStats.triggered.connect(self._controller.calculate_stats) statsmenu.addAction(calculateStats) saveStats = QAction("save", self) saveStats.triggered.connect(self._controller.save_stats) statsmenu.addAction(saveStats) self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.progressBar = QProgressBar(self) self.progressBar.setRange(0, 1) self.statusBar.addPermanentWidget(self.progressBar) self.currentFile = QLabel() self.statusBar.addPermanentWidget(self.currentFile) self.centwidget = QWidget() # contains figures and navigationtoolbar self.setCentralWidget(self.centwidget) self.canvas0.setFocusPolicy( Qt.ClickFocus ) # only widgets (e.g. canvas) that currently have focus capture keyboard input self.canvas0.setFocus() self.canvas0.mpl_connect( "key_press_event", self._controller.edit_peaks ) # connect canvas to keyboard input for peak editing self.canvas0.mpl_connect( "button_press_event", self.get_xcursor) # connect canvas to mouse input for peak editing self.splitter = QSplitter( Qt.Vertical ) # arrange the three figure canvases in splitter object self.splitter.setOpaqueResize( False) # resizing gets very slow otherwise once axes are populated self.splitter.addWidget(self.canvas0) self.splitter.addWidget(self.canvas1) self.splitter.addWidget(self.canvas2) self.splitter.setChildrenCollapsible(False) self.vlayout0 = QVBoxLayout(self.centwidget) self.vlayout1 = QVBoxLayout() self.vlayoutA = QFormLayout() self.vlayoutB = QFormLayout() self.vlayoutC = QVBoxLayout() self.vlayoutD = QVBoxLayout() self.hlayout0 = QHBoxLayout() self.optionsgroupA = QGroupBox("processing options") self.vlayoutA.addRow(self.modmenulabel, self.modmenu) self.vlayoutA.addRow(self.batchmenulabel, self.batchmenu) self.optionsgroupA.setLayout(self.vlayoutA) self.optionsgroupB = QGroupBox("channels") self.vlayoutB.addRow(self.sigchanmenulabel, self.sigchanmenu) self.vlayoutB.addRow(self.markerchanmenulabel, self.markerchanmenu) self.optionsgroupB.setLayout(self.vlayoutB) self.optionsgroupC = QGroupBox("peaks") self.vlayoutC.addWidget(self.editcheckbox) self.vlayoutC.addWidget(self.savecheckbox) self.vlayoutC.addWidget(self.correctcheckbox) self.optionsgroupC.setLayout(self.vlayoutC) self.optionsgroupD = QGroupBox("select statistics for saving") self.vlayoutD.addWidget(self.periodcheckbox) self.vlayoutD.addWidget(self.ratecheckbox) self.vlayoutD.addWidget(self.tidalampcheckbox) self.optionsgroupD.setLayout(self.vlayoutD) self.vlayout1.addWidget(self.optionsgroupA) self.vlayout1.addWidget(self.optionsgroupB) self.vlayout1.addWidget(self.optionsgroupC) self.vlayout1.addWidget(self.optionsgroupD) self.optionsgroupwidget = QWidget() self.optionsgroupwidget.setLayout(self.vlayout1) self.optionsgroup = QDockWidget("configurations", self) self.optionsgroup.setAllowedAreas(Qt.LeftDockWidgetArea) self.toggleoptionsgroup = self.optionsgroup.toggleViewAction() self.toggleoptionsgroup.setText("show/hide configurations") menubar.addAction(self.toggleoptionsgroup) self.optionsgroup.setWidget(self.optionsgroupwidget) self.addDockWidget(Qt.LeftDockWidgetArea, self.optionsgroup) self.vlayout0.addWidget(self.splitter) self.hlayout0.addWidget(self.navitools) self.vlayout0.addLayout(self.hlayout0) # Subscribe to updates from the Model. self._model.signal_changed.connect(self.plot_signal) self._model.marker_changed.connect(self.plot_marker) self._model.peaks_changed.connect(self.plot_peaks) self._model.period_changed.connect(self.plot_period) self._model.rate_changed.connect(self.plot_rate) self._model.tidalamp_changed.connect(self.plot_tidalamp) self._model.path_changed.connect(self.display_path) self._model.segment_changed.connect(self.plot_segment) self._model.status_changed.connect(self.display_status) self._model.progress_changed.connect(self.display_progress) self._model.model_reset.connect(self.reset_plot) def plot_signal(self, signal): """Plot the biosignal. Receives updates in signal from Model. Parameters ---------- signal : ndarray of float Vector representing the biosignal. See Also -------- model.Model.signal """ self.ax00.clear() self.ax00.relim() self.navitools.update() # reset navitools history self.line00 = self.ax00.plot(self._model.sec, signal, zorder=1) self.ax00.set_xlabel("seconds", fontsize="large", fontweight="heavy") self.canvas0.draw() def plot_peaks(self, peaks): """Plot the extrema. Receives updates in peaks from Model. Parameters ---------- peaks : ndarray of int Vector representing the extrema. See Also -------- model.Model.peaks """ if self.ax00.collections: # self.scat is listed in ax.collections self.ax00.collections[0].remove() self.scat = self.ax00.scatter(self._model.sec[peaks], self._model.signal[peaks], c="m", zorder=2) self.canvas0.draw() def plot_segment(self, segment): """Show preview of segment. Receives updates in segment from Model. Parameters ---------- segment : list of float The start and end of the segment in seconds. See Also -------- model.Model.segment """ if segment is None: # if an invalid segment has been selected reset the segmenter interface self.toggle_segmenter(1) return if self.ax00.patches: # self.segementspan is listed in ax.patches self.ax00.patches[0].remove() self.segmentspan = self.ax00.axvspan(segment[0], segment[1], color="m", alpha=0.25) self.canvas0.draw() self.confirmedit.setEnabled(True) def plot_marker(self, marker): """Plot the marker channel. Receives updates in marker from Model. Parameters ---------- marker : list of ndarray Seconds element is vector representing the marker channel and first element is a vector representing the seconds associated with each sample in the marker channel. See Also -------- model.Model.marker """ self.ax10.clear() self.ax10.relim() self.line10 = self.ax10.plot(marker[0], marker[1]) self.canvas1.draw() def plot_period(self, period): """Plot instantaneous period. Receives updates in period from Model. Parameters ---------- period : ndarray of float Vector representing the instantaneous period. See Also -------- model.Model.periodintp """ self.ax20.clear() self.ax20.relim() self.navitools.home() if self._model.savestats["period"]: self.line20 = self.ax20.plot(self._model.sec, period, c="m") else: self.line20 = self.ax20.plot(self._model.sec, period) self.ax20.set_ylim(bottom=min(period), top=max(period)) self.ax20.set_title("period", pad=0, fontweight="heavy") self.ax20.grid(True, axis="y") self.navitools.update() self.canvas2.draw() def plot_rate(self, rate): """Plot instantaneous rate. Receives updates in rate from Model. Parameters ---------- rate : ndarray of float Vector representing the instantaneous rate. See Also -------- model.Model.rateintp """ self.ax21.clear() self.ax21.relim() self.navitools.home() if self._model.savestats["rate"]: self.line21 = self.ax21.plot(self._model.sec, rate, c="m") else: self.line21 = self.ax21.plot(self._model.sec, rate) self.ax21.set_ylim(bottom=min(rate), top=max(rate)) self.ax21.set_title("rate", pad=0, fontweight="heavy") self.ax21.grid(True, axis="y") self.navitools.update() self.canvas2.draw() def plot_tidalamp(self, tidalamp): """Plot instantaneous tidal amplitude. Receives updates in tidal amplitude from Model. Parameters ---------- tidalamp : ndarray of float Vector representing the instantaneous tidal amplitude. See Also -------- model.Model.tidalampintp """ self.ax22.clear() self.ax22.relim() self.navitools.home() if self._model.savestats["tidalamp"]: self.line22 = self.ax22.plot(self._model.sec, tidalamp, c="m") else: self.line22 = self.ax22.plot(self._model.sec, tidalamp) self.ax22.set_ylim(bottom=min(tidalamp), top=max(tidalamp)) self.ax22.set_title("amplitude", pad=0, fontweight="heavy") self.ax22.grid(True, axis="y") self.navitools.update() self.canvas2.draw() def display_path(self, path): """Display the path to the current dataset. Receives update in path from Model. Parameters ---------- path : str The path to the file containing the current dataset. See Also -------- model.Model.rpathsignal """ self.currentFile.setText(path) def display_status(self, status): """Display a status message. Receives updates in status message from Model. Parameters ---------- status : str A status message. See Also -------- model.Model.status """ self.statusBar.showMessage(status) def display_progress(self, progress): """Display task progress. Receives updates in progress from Model. Parameters ---------- progress : int Integer indicating the current task progress. See Also -------- model.Model.progress, controller.Worker, controller.threaded """ self.progressBar.setRange( 0, progress) # indicates busy state if progress is 0 def toggle_segmenter(self, visibility_state): """Toggle visibility of segmenter widget. Parameters ---------- visibility_state : int Update in state of the segmenter widget's visibility. """ if not self._model.loaded: return if visibility_state == 1: # open segmenter when called from signalmenu or clear segmenter upon selection of invalid segment self.segmenter.setVisible(True) self.confirmedit.setEnabled(False) self.startedit.clear() self.endedit.clear() elif visibility_state == 0: # close segmenter after segment has been confirmed self.segmenter.setVisible(False) elif visibility_state == 2: # close segmenter after segmentation has been aborted (reset segment) self._model.set_segment([0, 0]) self.segmenter.setVisible(False) if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() def enable_segmentedit(self): """Associate cursor position with a specific segmenter text field. Regulate if cursor position is associated with editing the start or end of a segment. """ self.editcheckbox.setChecked( False) # disable peak editing to avoid interference if self.startedit.hasFocus(): self.segmentcursor = "start" elif self.endedit.hasFocus(): self.segmentcursor = "end" def get_xcursor(self, mouse_event): """Retrieve input to segmenter text fields from cursor position. Retrieve the start or end of a segment in seconds from the current cursor position. Parameters ---------- mouse_event : MouseEvent Event containing information about the current cursor position in data coordinates. See Also -------- matplotlib.backend_bases.MouseEvent """ if mouse_event.button != 1: # 1 = left mouse button return if self.segmentcursor == "start": self.startedit.selectAll() self.startedit.insert("{:.2f}".format( mouse_event.xdata)) # limit number of decimal places to two elif self.segmentcursor == "end": self.endedit.selectAll() self.endedit.insert("{:.2f}".format(mouse_event.xdata)) self.segmentcursor = False # disable segment cursor again after value has been set def set_customheader(self): """Populate the customheader with inputs from the customfiledialog.""" mandatoryfields = self.signaledit.text() and self.headerrowsedit.text( ) and self.sfreqedit.text( ) # check if one of the mandatory fields is missing if not mandatoryfields: self._model.status = ( "Please provide values for 'biosignal column'" ", 'number of header rows' and 'sampling" " rate'.") return seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} self._model.customheader = dict.fromkeys( self._model.customheader, None ) # reset header here since it cannot be reset in controller.load_chanels self._model.customheader["signalidx"] = int(self.signaledit.text()) self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) self._model.customheader["sfreq"] = int(self.sfreqedit.text()) self._model.customheader["separator"] = seps[ self.separatormenu.currentText()] if self.markeredit.text(): # not mandatory self._model.customheader["markeridx"] = int(self.markeredit.text()) self.customfiledialog.done(QDialog.Accepted) # close the dialog window self._controller.load_channels() # move on to file selection def select_stats(self, statistic): """Select statistics to be saved. Parameters ---------- statistic : str The selected statistic. """ self._model.savestats[ statistic] ^= True # toggle boolean with xor operator line = None if statistic == "period": if self.line20: line = self.line20[0] elif statistic == "rate": if self.line21: line = self.line21[0] elif statistic == "tidalamp": if self.line22: line = self.line22[0] if line: line.set_color(self.togglecolors[line.get_color()]) self.canvas2.draw() def toggle_options(self, state): """Toggle availability of configuration options. Based on current state. Parameters ---------- state : str The aspect of the current state to which the availability of configuration options needs to be adapted. """ if state in ["ECG", "PPG"]: self.tidalampcheckbox.setEnabled(False) self.tidalampcheckbox.setChecked(False) self.ax22.set_visible(False) self.canvas2.draw() elif state == "RESP": self.tidalampcheckbox.setEnabled(True) self.ax22.set_visible(True) self.canvas2.draw() elif state == "multiple files": self.editcheckbox.setEnabled(False) self.editcheckbox.setChecked(False) self.savecheckbox.setEnabled(True) self.correctcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(False) elif state == "single file": self.editcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(True) self.savecheckbox.setEnabled(False) self.savecheckbox.setChecked(False) self.correctcheckbox.setEnabled(False) self.correctcheckbox.setChecked(False) def reset_plot(self): """Reset plot elements associated with the current dataset.""" self.ax00.clear() self.ax00.relim() self.line00 = None self.scat = None self.segmentspan = None self.ax10.clear() self.ax10.relim() self.line10 = None self.ax20.clear() self.ax20.relim() self.line20 = None self.ax21.clear() self.ax21.relim() self.line21 = None self.ax22.clear() self.ax22.relim() self.line22 = None self.canvas0.draw() self.canvas1.draw() self.canvas2.draw() self.navitools.update() self.currentFile.clear()