class WindowController(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle( "Spongo - Projet M1 ISEN Yncréa Brest | Margaux DOUDET et Alexandre THOMAS" ) self.setFixedSize(1280, 720) self.setWindowIcon(QIcon(":/img/spongo_icon.png")) self.stacked_widget = QStackedWidget() self.stacked_widget.setObjectName("page-container") self.setCentralWidget(self.stacked_widget) self._widgets = { "/menu": MenuController(), "/parameters": ParametersController(), "/analysis": AnalysisController(), "/history": HistoryController() } for w in self._widgets.values(): self.stacked_widget.addWidget(w) w.changeWidget.connect(self._route) self._route("/menu") self.show() def closeEvent(self, event: QCloseEvent): ask_exit = self.stacked_widget.currentWidget().askExit() if ask_exit: event.accept() else: event.ignore() @Slot(str, object) def _route(self, route_name: str, parameters: object = None): next_widget = None for r, w in self._widgets.items(): if r == route_name: next_widget = w break if next_widget is None: print("[WARNING] Unknown widget : %s" % str(next_widget)) return current_widget = self.stacked_widget.currentWidget() current_widget.stop() self.stacked_widget.setCurrentWidget(next_widget) if route_name == "/analysis": next_widget.start(parameters[0], parameters[1]) elif route_name == "/history": next_widget.start(parameters) else: next_widget.start()
def toolbar_stack(mainwindow, settings): stack_toolbars = QStackedWidget() stack_toolbars.setObjectName('toolbar_stack') for model, params in settings['multiplet'].items(): if model == '1stOrd': toolbar = FirstOrderBar(mainwindow, model, params) else: toolbar = MultipletBar(mainwindow, model, params) # toolbar.setObjectName(f'multiplet_{model_name}_toolbar') stack_toolbars.addWidget(toolbar) mainwindow.toolbars[f'multiplet_{model}'] = toolbar for spins, params in settings['nspin'].items(): # model = str(spins) # need str so BaseToolbar name inits toolbar = SecondOrderBar(mainwindow, spins, params) stack_toolbars.addWidget(toolbar) mainwindow.toolbars[toolbar.objectName()] = toolbar for model, params in settings['dnmr'].items(): toolbar = DNMR_Bar(mainwindow, model, params) stack_toolbars.addWidget(toolbar) mainwindow.toolbars[toolbar.objectName()] = toolbar stack_toolbars.setCurrentWidget(mainwindow.toolbars['multiplet_AB']) return stack_toolbars
class MainWidget(QWidget): def __init__(self, parent=None): super(MainWidget, self).__init__(parent) self.initUI() def initUI(self): layout = QVBoxLayout(self) self.stack = QStackedWidget(parent=self) self.search = SearchWidget(parent=self) self.search.searchButton.clicked.connect(self.goSearch) self.back = BackWidget(parent=self) self.back.backButton.clicked.connect(self.goBack) self.stack.addWidget(self.search) self.stack.addWidget(self.back) layout.addWidget(self.stack) def goSearch(self): self.stack.setCurrentWidget(self.back) def goBack(self): self.stack.setCurrentWidget(self.search)
class KitchenManagerWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Meal Planner") # Menu bar self.menu = self.menuBar() self.file_menu = self.menu.addMenu("File") #Goto Meal Plan Action goto_meal_plan_action = QAction("Meal planner", self) goto_meal_plan_action.triggered.connect(self.goto_meal_planner_widget) # Goto Scraper Action goto_scraper_action = QAction("Scraper", self) goto_scraper_action.triggered.connect(self.goto_scraper_widget) # Goto Recipe Collection Action goto_recipe_collection_action = QAction("Recipe Collection", self) goto_recipe_collection_action.triggered.connect( self.goto_recipe_collection_widget) # Exit QAction exit_action = QAction("Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.exit_app) self.file_menu.addAction(goto_meal_plan_action) self.file_menu.addAction(goto_recipe_collection_action) self.file_menu.addAction(goto_scraper_action) self.file_menu.addAction(exit_action) self.widget_scraper = scraper_widget.ScraperWidget() self.widget_meal_plan = meal_plan_widget.MealPlanWidget() self.widget_recipe_collection = recipe_collection_widget.RecipeCollectionWidget( ) #Start on the scraper self.stacked_widgets = QStackedWidget() self.stacked_widgets.addWidget(self.widget_scraper) self.stacked_widgets.addWidget(self.widget_meal_plan) self.stacked_widgets.addWidget(self.widget_recipe_collection) self.stacked_widgets.setCurrentWidget(self.widget_recipe_collection) self.setCentralWidget(self.stacked_widgets) @Slot() def goto_recipe_collection_widget(self, checked): self.stacked_widgets.setCurrentWidget(self.widget_recipe_collection) @Slot() def goto_meal_planner_widget(self, checked): self.stacked_widgets.setCurrentWidget(self.widget_meal_plan) @Slot() def goto_scraper_widget(self, checked): self.stacked_widgets.setCurrentWidget(self.widget_scraper) @Slot() def exit_app(self, checked): QApplication.quit()
class UiMainWindow: def setupUi(self, main_window): main_window.setObjectName('main_window') main_window.setWindowTitle('qt_mvc Demo') main_window.resize(800, 600) # pyqtgraph configuration setConfigOption('background', 'w') setConfigOption('foreground', 'k') # Divide window into left (toolbar) and right(main) vertical layouts self.central_widget = QWidget(main_window) self.central_widget.setObjectName('centralwidget') self.central_layout = QHBoxLayout(self.central_widget) self.central_layout.setObjectName('centrallayout') self.left_bar_layout = QVBoxLayout() self.left_bar_layout.setObjectName('left_bar_layout') self.main_layout = QVBoxLayout() self.main_layout.setObjectName('main_layout') self.central_layout.addLayout(self.left_bar_layout, 0) self.central_layout.addLayout(self.main_layout, 1) # Populate left toolbar self.calctype = CalcTypeButtonGroup('Calc Type') self.calctype.setObjectName('calctype_menu') self.stack_model_selections = QStackedWidget() self.stack_model_selections.setObjectName('model_selection_stack') self.multiplet_menu = MultipletButtonGroup('Multiplet') self.multiplet_menu.setObjectName('multiplet_menu') self.abc_menu = ABC_ButtonGroup('Number of Spins') self.abc_menu.setObjectName('abc_menu') self.dnmr_menu = DNMR_ButtonGroup('DNMR') self.dnmr_menu.setObjectName('dnmr_menu') for menu in [self.multiplet_menu, self.abc_menu, self.dnmr_menu]: self.stack_model_selections.addWidget(menu) self.stack_model_selections.setCurrentWidget(self.multiplet_menu) self.left_bar_layout.addWidget(self.calctype, 0) self.left_bar_layout.addWidget(self.stack_model_selections, 0) self.left_bar_layout.addWidget(QWidget(), 1) # Add toolbars and plot area to main layout self.toolbars = toolbar_stack(main_window, main_window.view_state) self.plot = PlotWidget() self.plot.getViewBox().invertX(True) # Reverse x axis "NMR style" self.main_layout.addWidget(self.toolbars, 0) self.main_layout.addWidget(self.plot, 1) main_window.setCentralWidget(self.central_widget)
class Main_Widget(QWidget): def __init__(self, parent=None): super(Main_Widget, self).__init__(parent) self.init_UI() def init_UI(self): layout = QHBoxLayout() self.stack = QStackedWidget(parent=self) self.welcome = Welcome_Widget(parent=self) self.welcome.login.Button_Login.clicked.connect( self.check_user_details) self.user = User_Widget(parent=None) self.user.Button_Quit.clicked.connect(self.user_quit) self.stack.addWidget(self.welcome) self.stack.addWidget(self.user) layout.addWidget(self.stack) def check_user_details(self): self.stack.setCurrentWidget(self.user) def user_quit(self): self.stack.setCurrentWidget(self.welcome)
class MainWindow(QWidget): widget_stack = None main_widget = None exercise_widget = None exercise_view_widget = None def __init__(self): QWidget.__init__(self) self.setup_window() self.widget_stack.setCurrentWidget(self.main_widget) def setup_window(self): self.widget_stack = QStackedWidget(self) self.main_widget = MainWidget() self.exercise_widget = ExerciseWidget() self.exercise_view_widget = ViewWidget() self.widget_stack.addWidget(self.main_widget) self.widget_stack.addWidget(self.exercise_widget) self.widget_stack.addWidget(self.exercise_view_widget) self.main_widget.start_exercise_file.connect(self.show_exercise_window) self.main_widget.open_exercise_file.connect( self.show_exercise_view_widget) self.exercise_view_widget.return_button.clicked.connect( self.show_main_widget) def resizeEvent(self, event): self.widget_stack.resize(self.size()) @Slot() def show_main_widget(self): if self.widget_stack.currentWidget() == self.exercise_view_widget: self.exercise_view_widget.clear_widget() self.widget_stack.setCurrentWidget(self.main_widget) @Slot(str) def show_exercise_view_widget(self, file: str): self.widget_stack.setCurrentWidget(self.exercise_view_widget) self.exercise_view_widget.open_exercise_file(file) @Slot(str) def show_exercise_window(self, file: str): self.exercise_widget.set_file(file) self.exercise_widget.setup_exercise() self.exercise_widget.start() self.widget_stack.setCurrentWidget(self.exercise_widget)
class SCOUTS(QMainWindow): """Main Window Widget for SCOUTS.""" style = { 'title': 'QLabel {font-size: 18pt; font-weight: 600}', 'header': 'QLabel {font-size: 12pt; font-weight: 520}', 'label': 'QLabel {font-size: 10pt}', 'button': 'QPushButton {font-size: 10pt}', 'md button': 'QPushButton {font-size: 12pt}', 'run button': 'QPushButton {font-size: 18pt; font-weight: 600}', 'line edit': 'QLineEdit {font-size: 10pt}', 'checkbox': 'QCheckBox {font-size: 10pt}', 'radio button': 'QRadioButton {font-size: 10pt}' } def __init__(self) -> None: """SCOUTS Constructor. Defines all aspects of the GUI.""" # ### # ### Main Window setup # ### # Inherits from QMainWindow super().__init__() self.rootdir = get_project_root() self.threadpool = QThreadPool() # Sets values for QMainWindow self.setWindowTitle("SCOUTS") self.setWindowIcon( QIcon( os.path.abspath(os.path.join(self.rootdir, 'src', 'scouts.ico')))) # Creates StackedWidget as QMainWindow's central widget self.stacked_pages = QStackedWidget(self) self.setCentralWidget(self.stacked_pages) # Creates Widgets for individual "pages" and adds them to the StackedWidget self.main_page = QWidget() self.samples_page = QWidget() self.gating_page = QWidget() self.pages = (self.main_page, self.samples_page, self.gating_page) for page in self.pages: self.stacked_pages.addWidget(page) # ## Sets widget at program startup self.stacked_pages.setCurrentWidget(self.main_page) # ### # ### MAIN PAGE # ### # Main page layout self.main_layout = QVBoxLayout(self.main_page) # Title section # Title self.title = QLabel(self.main_page) self.title.setText('SCOUTS - Single Cell Outlier Selector') self.title.setStyleSheet(self.style['title']) self.title.adjustSize() self.main_layout.addWidget(self.title) # ## Input section # Input header self.input_header = QLabel(self.main_page) self.input_header.setText('Input settings') self.input_header.setStyleSheet(self.style['header']) self.main_layout.addChildWidget(self.input_header) self.input_header.adjustSize() self.main_layout.addWidget(self.input_header) # Input frame self.input_frame = QFrame(self.main_page) self.input_frame.setFrameShape(QFrame.StyledPanel) self.input_frame.setLayout(QFormLayout()) self.main_layout.addWidget(self.input_frame) # Input button self.input_button = QPushButton(self.main_page) self.input_button.setStyleSheet(self.style['button']) self.set_icon(self.input_button, 'x-office-spreadsheet') self.input_button.setObjectName('input') self.input_button.setText(' Select input file (.xlsx or .csv)') self.input_button.clicked.connect(self.get_path) # Input path box self.input_path = QLineEdit(self.main_page) self.input_path.setObjectName('input_path') self.input_path.setStyleSheet(self.style['line edit']) # Go to sample naming page self.samples_button = QPushButton(self.main_page) self.samples_button.setStyleSheet(self.style['button']) self.set_icon(self.samples_button, 'preferences-other') self.samples_button.setText(' Name samples...') self.samples_button.clicked.connect(self.goto_samples_page) # Go to gating page self.gates_button = QPushButton(self.main_page) self.gates_button.setStyleSheet(self.style['button']) self.set_icon(self.gates_button, 'preferences-other') self.gates_button.setText(' Gating && outlier options...') self.gates_button.clicked.connect(self.goto_gates_page) # Add widgets above to input frame Layout self.input_frame.layout().addRow(self.input_button, self.input_path) self.input_frame.layout().addRow(self.samples_button) self.input_frame.layout().addRow(self.gates_button) # ## Analysis section # Analysis header self.analysis_header = QLabel(self.main_page) self.analysis_header.setText('Analysis settings') self.analysis_header.setStyleSheet(self.style['header']) self.analysis_header.adjustSize() self.main_layout.addWidget(self.analysis_header) # Analysis frame self.analysis_frame = QFrame(self.main_page) self.analysis_frame.setFrameShape(QFrame.StyledPanel) self.analysis_frame.setLayout(QVBoxLayout()) self.main_layout.addWidget(self.analysis_frame) # Cutoff text self.cutoff_text = QLabel(self.main_page) self.cutoff_text.setText('Type of outlier to select:') self.cutoff_text.setToolTip( 'Choose whether to select outliers using the cutoff value from a reference\n' 'sample (OutR) or by using the cutoff value calculated for each sample\n' 'individually (OutS)') self.cutoff_text.setStyleSheet(self.style['label']) # Cutoff button group self.cutoff_group = QButtonGroup(self) # Cutoff by sample self.cutoff_sample = QRadioButton(self.main_page) self.cutoff_sample.setText('OutS') self.cutoff_sample.setObjectName('sample') self.cutoff_sample.setStyleSheet(self.style['radio button']) self.cutoff_sample.setChecked(True) self.cutoff_group.addButton(self.cutoff_sample) # Cutoff by reference self.cutoff_reference = QRadioButton(self.main_page) self.cutoff_reference.setText('OutR') self.cutoff_reference.setObjectName('ref') self.cutoff_reference.setStyleSheet(self.style['radio button']) self.cutoff_group.addButton(self.cutoff_reference) # Both cutoffs self.cutoff_both = QRadioButton(self.main_page) self.cutoff_both.setText('both') self.cutoff_both.setObjectName('sample ref') self.cutoff_both.setStyleSheet(self.style['radio button']) self.cutoff_group.addButton(self.cutoff_both) # Markers text self.markers_text = QLabel(self.main_page) self.markers_text.setStyleSheet(self.style['label']) self.markers_text.setText('Show results for:') self.markers_text.setToolTip( 'Individual markers: for each marker, select outliers\n' 'Any marker: select cells that are outliers for AT LEAST one marker' ) # Markers button group self.markers_group = QButtonGroup(self) # Single marker self.single_marker = QRadioButton(self.main_page) self.single_marker.setText('individual markers') self.single_marker.setObjectName('single') self.single_marker.setStyleSheet(self.style['radio button']) self.single_marker.setChecked(True) self.markers_group.addButton(self.single_marker) # Any marker self.any_marker = QRadioButton(self.main_page) self.any_marker.setText('any marker') self.any_marker.setObjectName('any') self.any_marker.setStyleSheet(self.style['radio button']) self.markers_group.addButton(self.any_marker) # Both methods self.both_methods = QRadioButton(self.main_page) self.both_methods.setText('both') self.both_methods.setObjectName('single any') self.both_methods.setStyleSheet(self.style['radio button']) self.markers_group.addButton(self.both_methods) # Tukey text self.tukey_text = QLabel(self.main_page) self.tukey_text.setStyleSheet(self.style['label']) # Tukey button group self.tukey_text.setText('Tukey factor:') self.tukey_group = QButtonGroup(self) # Low Tukey value self.tukey_low = QRadioButton(self.main_page) self.tukey_low.setText('1.5') self.tukey_low.setStyleSheet(self.style['radio button']) self.tukey_low.setChecked(True) self.tukey_group.addButton(self.tukey_low) # High Tukey value self.tukey_high = QRadioButton(self.main_page) self.tukey_high.setText('3.0') self.tukey_high.setStyleSheet(self.style['radio button']) self.tukey_group.addButton(self.tukey_high) # Add widgets above to analysis frame layout self.analysis_frame.layout().addWidget(self.cutoff_text) self.cutoff_buttons = QHBoxLayout() for button in self.cutoff_group.buttons(): self.cutoff_buttons.addWidget(button) self.analysis_frame.layout().addLayout(self.cutoff_buttons) self.analysis_frame.layout().addWidget(self.markers_text) self.markers_buttons = QHBoxLayout() for button in self.markers_group.buttons(): self.markers_buttons.addWidget(button) self.analysis_frame.layout().addLayout(self.markers_buttons) self.analysis_frame.layout().addWidget(self.tukey_text) self.tukey_buttons = QHBoxLayout() for button in self.tukey_group.buttons(): self.tukey_buttons.addWidget(button) self.tukey_buttons.addWidget(QLabel()) # aligns row with 2 buttons self.analysis_frame.layout().addLayout(self.tukey_buttons) # ## Output section # Output header self.output_header = QLabel(self.main_page) self.output_header.setText('Output settings') self.output_header.setStyleSheet(self.style['header']) self.output_header.adjustSize() self.main_layout.addWidget(self.output_header) # Output frame self.output_frame = QFrame(self.main_page) self.output_frame.setFrameShape(QFrame.StyledPanel) self.output_frame.setLayout(QFormLayout()) self.main_layout.addWidget(self.output_frame) # Output button self.output_button = QPushButton(self.main_page) self.output_button.setStyleSheet(self.style['button']) self.set_icon(self.output_button, 'folder') self.output_button.setObjectName('output') self.output_button.setText(' Select output folder') self.output_button.clicked.connect(self.get_path) # Output path box self.output_path = QLineEdit(self.main_page) self.output_path.setStyleSheet(self.style['line edit']) # Generate CSV checkbox self.output_csv = QCheckBox(self.main_page) self.output_csv.setText('Export multiple text files (.csv)') self.output_csv.setStyleSheet(self.style['checkbox']) self.output_csv.setChecked(True) # Generate XLSX checkbox self.output_excel = QCheckBox(self.main_page) self.output_excel.setText('Export multiple Excel spreadsheets (.xlsx)') self.output_excel.setStyleSheet(self.style['checkbox']) self.output_excel.clicked.connect(self.enable_single_excel) # Generate single, large XLSX checkbox self.single_excel = QCheckBox(self.main_page) self.single_excel.setText( 'Also save one multi-sheet Excel spreadsheet') self.single_excel.setToolTip( 'After generating all Excel spreadsheets, SCOUTS combines them into ' 'a single\nExcel spreadsheet where each sheet corresponds to an output' 'file from SCOUTS') self.single_excel.setStyleSheet(self.style['checkbox']) self.single_excel.setEnabled(False) self.single_excel.clicked.connect(self.memory_warning) # Add widgets above to output frame layout self.output_frame.layout().addRow(self.output_button, self.output_path) self.output_frame.layout().addRow(self.output_csv) self.output_frame.layout().addRow(self.output_excel) self.output_frame.layout().addRow(self.single_excel) # ## Run & help-quit section # Run button (stand-alone) self.run_button = QPushButton(self.main_page) self.set_icon(self.run_button, 'system-run') self.run_button.setText(' Run!') self.run_button.setStyleSheet(self.style['run button']) self.main_layout.addWidget(self.run_button) self.run_button.clicked.connect(self.run) # Help-quit frame (invisible) self.helpquit_frame = QFrame(self.main_page) self.helpquit_frame.setLayout(QHBoxLayout()) self.helpquit_frame.layout().setMargin(0) self.main_layout.addWidget(self.helpquit_frame) # Help button self.help_button = QPushButton(self.main_page) self.set_icon(self.help_button, 'help-about') self.help_button.setText(' Help') self.help_button.setStyleSheet(self.style['md button']) self.help_button.clicked.connect(self.get_help) # Quit button self.quit_button = QPushButton(self.main_page) self.set_icon(self.quit_button, 'process-stop') self.quit_button.setText(' Quit') self.quit_button.setStyleSheet(self.style['md button']) self.quit_button.clicked.connect(self.close) # Add widgets above to help-quit layout self.helpquit_frame.layout().addWidget(self.help_button) self.helpquit_frame.layout().addWidget(self.quit_button) # ### # ### SAMPLES PAGE # ### # Samples page layout self.samples_layout = QVBoxLayout(self.samples_page) # ## Title section # Title self.samples_title = QLabel(self.samples_page) self.samples_title.setText('Name your samples') self.samples_title.setStyleSheet(self.style['title']) self.samples_title.adjustSize() self.samples_layout.addWidget(self.samples_title) # Subtitle self.samples_subtitle = QLabel(self.samples_page) string = ( 'Please name the samples to be analysed by SCOUTS.\n\nSCOUTS searches the first ' 'column of your data\nand locates the exact string as part of the sample name.' ) self.samples_subtitle.setText(string) self.samples_subtitle.setStyleSheet(self.style['label']) self.samples_subtitle.adjustSize() self.samples_layout.addWidget(self.samples_subtitle) # ## Sample addition section # Sample addition frame self.samples_frame = QFrame(self.samples_page) self.samples_frame.setFrameShape(QFrame.StyledPanel) self.samples_frame.setLayout(QGridLayout()) self.samples_layout.addWidget(self.samples_frame) # Sample name box self.sample_name = QLineEdit(self.samples_page) self.sample_name.setStyleSheet(self.style['line edit']) self.sample_name.setPlaceholderText('Sample name ...') # Reference check self.is_reference = QCheckBox(self.samples_page) self.is_reference.setText('Reference?') self.is_reference.setStyleSheet(self.style['checkbox']) # Add sample to table self.add_sample_button = QPushButton(self.samples_page) QShortcut(QKeySequence("Return"), self.add_sample_button, self.write_to_sample_table) self.set_icon(self.add_sample_button, 'list-add') self.add_sample_button.setText(' Add sample (Enter)') self.add_sample_button.setStyleSheet(self.style['button']) self.add_sample_button.clicked.connect(self.write_to_sample_table) # Remove sample from table self.remove_sample_button = QPushButton(self.samples_page) QShortcut(QKeySequence("Delete"), self.remove_sample_button, self.remove_from_sample_table) self.set_icon(self.remove_sample_button, 'list-remove') self.remove_sample_button.setText(' Remove sample (Del)') self.remove_sample_button.setStyleSheet(self.style['button']) self.remove_sample_button.clicked.connect( self.remove_from_sample_table) # Add widgets above to sample addition layout self.samples_frame.layout().addWidget(self.sample_name, 0, 0) self.samples_frame.layout().addWidget(self.is_reference, 1, 0) self.samples_frame.layout().addWidget(self.add_sample_button, 0, 1) self.samples_frame.layout().addWidget(self.remove_sample_button, 1, 1) # ## Sample table self.sample_table = QTableWidget(self.samples_page) self.sample_table.setColumnCount(2) self.sample_table.setHorizontalHeaderItem(0, QTableWidgetItem('Sample')) self.sample_table.setHorizontalHeaderItem( 1, QTableWidgetItem('Reference?')) self.sample_table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.sample_table.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.samples_layout.addWidget(self.sample_table) # ## Save & clear buttons # Save & clear frame (invisible) self.saveclear_frame = QFrame(self.samples_page) self.saveclear_frame.setLayout(QHBoxLayout()) self.saveclear_frame.layout().setMargin(0) self.samples_layout.addWidget(self.saveclear_frame) # Clear samples button self.clear_samples = QPushButton(self.samples_page) self.set_icon(self.clear_samples, 'edit-delete') self.clear_samples.setText(' Clear table') self.clear_samples.setStyleSheet(self.style['md button']) self.clear_samples.clicked.connect(self.prompt_clear_data) # Save samples button self.save_samples = QPushButton(self.samples_page) self.set_icon(self.save_samples, 'document-save') self.save_samples.setText(' Save samples') self.save_samples.setStyleSheet(self.style['md button']) self.save_samples.clicked.connect(self.goto_main_page) # Add widgets above to save & clear layout self.saveclear_frame.layout().addWidget(self.clear_samples) self.saveclear_frame.layout().addWidget(self.save_samples) # ### # ### GATING PAGE # ### # Gating page layout self.gating_layout = QVBoxLayout(self.gating_page) # ## Title section # Title self.gates_title = QLabel(self.gating_page) self.gates_title.setText('Gating & outlier options') self.gates_title.setStyleSheet(self.style['title']) self.gates_title.adjustSize() self.gating_layout.addWidget(self.gates_title) # ## Gating options section # Gating header self.gate_header = QLabel(self.gating_page) self.gate_header.setText('Gating') self.gate_header.setStyleSheet(self.style['header']) self.gate_header.adjustSize() self.gating_layout.addWidget(self.gate_header) # Gating frame self.gate_frame = QFrame(self.gating_page) self.gate_frame.setFrameShape(QFrame.StyledPanel) self.gate_frame.setLayout(QFormLayout()) self.gating_layout.addWidget(self.gate_frame) # Gating button group self.gating_group = QButtonGroup(self) # Do not gate samples self.no_gates = QRadioButton(self.gating_page) self.no_gates.setObjectName('no_gate') self.no_gates.setText("Don't gate samples") self.no_gates.setStyleSheet(self.style['radio button']) self.no_gates.setChecked(True) self.gating_group.addButton(self.no_gates) self.no_gates.clicked.connect(self.activate_gate) # CyToF gating self.cytof_gates = QRadioButton(self.gating_page) self.cytof_gates.setObjectName('cytof') self.cytof_gates.setText('Mass Cytometry gating') self.cytof_gates.setStyleSheet(self.style['radio button']) self.cytof_gates.setToolTip( 'Exclude cells for which the average expression of all\n' 'markers is below the selected value') self.gating_group.addButton(self.cytof_gates) self.cytof_gates.clicked.connect(self.activate_gate) # CyToF gating spinbox self.cytof_gates_value = QDoubleSpinBox(self.gating_page) self.cytof_gates_value.setMinimum(0) self.cytof_gates_value.setMaximum(1) self.cytof_gates_value.setValue(0.1) self.cytof_gates_value.setSingleStep(0.05) self.cytof_gates_value.setEnabled(False) # scRNA-Seq gating self.rnaseq_gates = QRadioButton(self.gating_page) self.rnaseq_gates.setText('scRNA-Seq gating') self.rnaseq_gates.setStyleSheet(self.style['radio button']) self.rnaseq_gates.setToolTip( 'When calculating cutoff, ignore reads below the selected value') self.rnaseq_gates.setObjectName('rnaseq') self.gating_group.addButton(self.rnaseq_gates) self.rnaseq_gates.clicked.connect(self.activate_gate) # scRNA-Seq gating spinbox self.rnaseq_gates_value = QDoubleSpinBox(self.gating_page) self.rnaseq_gates_value.setMinimum(0) self.rnaseq_gates_value.setMaximum(10) self.rnaseq_gates_value.setValue(0) self.rnaseq_gates_value.setSingleStep(1) self.rnaseq_gates_value.setEnabled(False) # export gated population checkbox self.export_gated = QCheckBox(self.gating_page) self.export_gated.setText('Export gated cells as an output file') self.export_gated.setStyleSheet(self.style['checkbox']) self.export_gated.setEnabled(False) # Add widgets above to Gate frame layout self.gate_frame.layout().addRow(self.no_gates, QLabel()) self.gate_frame.layout().addRow(self.cytof_gates, self.cytof_gates_value) self.gate_frame.layout().addRow(self.rnaseq_gates, self.rnaseq_gates_value) self.gate_frame.layout().addRow(self.export_gated, QLabel()) # ## Outlier options section # Outlier header self.outlier_header = QLabel(self.gating_page) self.outlier_header.setText('Outliers') self.outlier_header.setStyleSheet(self.style['header']) self.outlier_header.adjustSize() self.gating_layout.addWidget(self.outlier_header) # Outlier frame self.outlier_frame = QFrame(self.gating_page) self.outlier_frame.setFrameShape(QFrame.StyledPanel) self.outlier_frame.setLayout(QVBoxLayout()) self.gating_layout.addWidget(self.outlier_frame) # Top outliers information self.top_outliers = QLabel(self.gating_page) self.top_outliers.setStyleSheet(self.style['label']) self.top_outliers.setText( 'By default, SCOUTS selects the top outliers from the population') self.top_outliers.setStyleSheet(self.style['label']) # Bottom outliers data self.bottom_outliers = QCheckBox(self.gating_page) self.bottom_outliers.setText('Include results for low outliers') self.bottom_outliers.setStyleSheet(self.style['checkbox']) # Non-outliers data self.not_outliers = QCheckBox(self.gating_page) self.not_outliers.setText('Include results for non-outliers') self.not_outliers.setStyleSheet(self.style['checkbox']) # Add widgets above to Gate frame layout self.outlier_frame.layout().addWidget(self.top_outliers) self.outlier_frame.layout().addWidget(self.bottom_outliers) self.outlier_frame.layout().addWidget(self.not_outliers) # ## Save/back button self.save_gates = QPushButton(self.gating_page) self.set_icon(self.save_gates, 'go-next') self.save_gates.setText(' Back to main menu') self.save_gates.setStyleSheet(self.style['md button']) self.gating_layout.addWidget(self.save_gates) self.save_gates.clicked.connect(self.goto_main_page) # ## Add empty label to take vertical space self.empty_label = QLabel(self.gating_page) self.empty_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.gating_layout.addWidget(self.empty_label) # ### # ### ICON SETTING # ### def set_icon(self, widget: QWidget, icon: str) -> None: """Associates an icon to a widget.""" i = QIcon() i.addPixmap( QPixmap( os.path.abspath( os.path.join(self.rootdir, 'src', 'default_icons', f'{icon}.svg')))) widget.setIcon(QIcon.fromTheme(icon, i)) # ### # ### STACKED WIDGET PAGE SWITCHING # ### def goto_main_page(self) -> None: """Switches stacked widget pages to the main page.""" self.stacked_pages.setCurrentWidget(self.main_page) def goto_samples_page(self) -> None: """Switches stacked widget pages to the samples table page.""" self.stacked_pages.setCurrentWidget(self.samples_page) def goto_gates_page(self) -> None: """Switches stacked widget pages to the gating & other options page.""" self.stacked_pages.setCurrentWidget(self.gating_page) # ### # ### MAIN PAGE GUI LOGIC # ### def get_path(self) -> None: """Opens a dialog box and sets the chosen file/folder path, depending on the caller widget.""" options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog sender_name = self.sender().objectName() if sender_name == 'input': query, _ = QFileDialog.getOpenFileName(self, "Select file", "", "All Files (*)", options=options) elif sender_name == 'output': query = QFileDialog.getExistingDirectory(self, "Select Directory", options=options) else: return if query: getattr(self, f'{sender_name}_path').setText(query) def enable_single_excel(self) -> None: """Enables checkbox for generating a single Excel output.""" if self.output_excel.isChecked(): self.single_excel.setEnabled(True) else: self.single_excel.setEnabled(False) self.single_excel.setChecked(False) # ### # ### SAMPLE NAME/SAMPLE TABLE GUI LOGIC # ### def write_to_sample_table(self) -> None: """Writes data to sample table.""" table = self.sample_table ref = 'no' sample = self.sample_name.text() if sample: for cell in range(table.rowCount()): item = table.item(cell, 0) if item.text() == sample: self.same_sample() return if self.is_reference.isChecked(): for cell in range(table.rowCount()): item = table.item(cell, 1) if item.text() == 'yes': self.more_than_one_reference() return ref = 'yes' sample = QTableWidgetItem(sample) is_reference = QTableWidgetItem(ref) is_reference.setFlags(Qt.ItemIsEnabled) row_position = table.rowCount() table.insertRow(row_position) table.setItem(row_position, 0, sample) table.setItem(row_position, 1, is_reference) self.is_reference.setChecked(False) self.sample_name.setText('') def remove_from_sample_table(self) -> None: """Removes data from sample table.""" table = self.sample_table rows = set(index.row() for index in table.selectedIndexes()) for index in sorted(rows, reverse=True): self.sample_table.removeRow(index) def prompt_clear_data(self) -> None: """Prompts option to clear all data in the sample table.""" if self.confirm_clear_data(): table = self.sample_table while table.rowCount(): self.sample_table.removeRow(0) # ### # ### GATING GUI LOGIC # ### def activate_gate(self) -> None: """Activates/deactivates buttons related to gating.""" if self.sender().objectName() == 'no_gate': self.cytof_gates_value.setEnabled(False) self.rnaseq_gates_value.setEnabled(False) self.export_gated.setEnabled(False) self.export_gated.setChecked(False) elif self.sender().objectName() == 'cytof': self.cytof_gates_value.setEnabled(True) self.rnaseq_gates_value.setEnabled(False) self.export_gated.setEnabled(True) elif self.sender().objectName() == 'rnaseq': self.cytof_gates_value.setEnabled(False) self.rnaseq_gates_value.setEnabled(True) self.export_gated.setEnabled(True) # ### # ### CONNECT SCOUTS TO ANALYTICAL MODULES # ### def run(self) -> None: """Runs SCOUTS as a Worker, based on user input in the GUI.""" try: data = self.parse_input() except Exception as error: trace = traceback.format_exc() self.propagate_error((error, trace)) else: data['widget'] = self worker = Worker(func=start_scouts, **data) worker.signals.started.connect(self.analysis_has_started) worker.signals.finished.connect(self.analysis_has_finished) worker.signals.success.connect(self.success_message) worker.signals.error.connect(self.propagate_error) self.threadpool.start(worker) def parse_input(self) -> Dict: """Returns user input on the GUI as a dictionary.""" # Input and output input_dict = { 'input_file': str(self.input_path.text()), 'output_folder': str(self.output_path.text()) } if not input_dict['input_file'] or not input_dict['output_folder']: raise NoIOPathError # Set cutoff by reference or by sample rule input_dict['cutoff_rule'] = self.cutoff_group.checkedButton( ).objectName() # 'sample', 'ref', 'sample ref' # Outliers for each individual marker or any marker in row input_dict['marker_rule'] = self.markers_group.checkedButton( ).objectName() # 'single', 'any', 'single any' # Tukey factor used for calculating cutoff input_dict['tukey_factor'] = float( self.tukey_group.checkedButton().text()) # '1.5', '3.0' # Output settings input_dict['export_csv'] = True if self.output_csv.isChecked( ) else False input_dict['export_excel'] = True if self.output_excel.isChecked( ) else False input_dict['single_excel'] = True if self.single_excel.isChecked( ) else False # Retrieve samples from sample table input_dict['sample_list'] = [] for tuples in self.yield_samples_from_table(): input_dict['sample_list'].append(tuples) if not input_dict['sample_list']: raise NoSampleError # Set gate cutoff (if any) input_dict['gating'] = self.gating_group.checkedButton().objectName( ) # 'no_gate', 'cytof', 'rnaseq' input_dict['gate_cutoff_value'] = None if input_dict['gating'] != 'no_gate': input_dict['gate_cutoff_value'] = getattr( self, f'{input_dict["gating"]}_gates_value').value() input_dict['export_gated'] = True if self.export_gated.isChecked( ) else False # Generate results for non-outliers input_dict['non_outliers'] = False if self.not_outliers.isChecked(): input_dict['non_outliers'] = True # Generate results for bottom outliers input_dict['bottom_outliers'] = False if self.bottom_outliers.isChecked(): input_dict['bottom_outliers'] = True # return dictionary with all gathered inputs return input_dict def yield_samples_from_table( self) -> Generator[Tuple[str, str], None, None]: """Yields sample names from the sample table.""" table = self.sample_table for cell in range(table.rowCount()): sample_name = table.item(cell, 0).text() sample_type = table.item(cell, 1).text() yield sample_name, sample_type # ### # ### MESSAGE BOXES # ### def analysis_has_started(self) -> None: """Disables run button while SCOUTS analysis is underway.""" self.run_button.setText(' Working...') self.run_button.setEnabled(False) def analysis_has_finished(self) -> None: """Enables run button after SCOUTS analysis has finished.""" self.run_button.setEnabled(True) self.run_button.setText(' Run!') def success_message(self) -> None: """Info message box used when SCOUTS finished without errors.""" title = "Analysis finished!" mes = "Your analysis has finished. No errors were reported." if self.stacked_pages.isEnabled() is True: QMessageBox.information(self, title, mes) def memory_warning(self) -> None: """Warning message box used when user wants to generate a single excel file.""" if self.sender().isChecked(): title = 'Memory warning!' mes = ( "Depending on your dataset, this option can consume a LOT of memory and take" " a long time to process. Please make sure that your computer can handle it!" ) QMessageBox.information(self, title, mes) def same_sample(self) -> None: """Error message box used when the user tries to input the same sample twice in the sample table.""" title = 'Error: sample name already in table' mes = ( "Sorry, you can't do this because this sample name is already in the table. " "Please select a different name.") QMessageBox.critical(self, title, mes) def more_than_one_reference(self) -> None: """Error message box used when the user tries to input two reference samples in the sample table.""" title = "Error: more than one reference selected" mes = ( "Sorry, you can't do this because there is already a reference column in the table. " "Please remove it before adding a reference.") QMessageBox.critical(self, title, mes) def confirm_clear_data(self) -> bool: """Question message box used to confirm user action of clearing sample table.""" title = 'Confirm Action' mes = "Table will be cleared. Are you sure?" reply = QMessageBox.question(self, title, mes, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: return True return False # ### # ### EXCEPTIONS & ERRORS # ### def propagate_error(self, error: Tuple[Exception, str]) -> None: """Calls the appropriate error message box based on type of Exception raised.""" if isinstance(error[0], NoIOPathError): self.no_io_path_error_message() elif isinstance(error[0], NoReferenceError): self.no_reference_error_message() elif isinstance(error[0], NoSampleError): self.no_sample_error_message() elif isinstance(error[0], PandasInputError): self.pandas_input_error_message() elif isinstance(error[0], SampleNamingError): self.sample_naming_error_message() else: self.generic_error_message(error) def no_io_path_error_message(self) -> None: """Message displayed when the user did not include an input file path, or an output folder path.""" title = 'Error: no file/folder' message = ("Sorry, no input file and/or output folder was provided. " "Please add the path to the necessary file/folder.") QMessageBox.critical(self, title, message) def no_reference_error_message(self) -> None: """Message displayed when the user wants to analyse cutoff based on a reference, but did not specify what sample corresponds to the reference.""" title = "Error: No reference selected" message = ( "Sorry, no reference sample was found on the sample list, but analysis was set to " "reference. Please add a reference sample, or change the rule for cutoff calculation." ) QMessageBox.critical(self, title, message) def no_sample_error_message(self) -> None: """Message displayed when the user did not add any samples to the sample table.""" title = "Error: No samples selected" message = ( "Sorry, the analysis cannot be performed because no sample names were input. " "Please add your sample names.") QMessageBox.critical(self, title, message) def pandas_input_error_message(self) -> None: """Message displayed when the input file cannot be read (likely because it is not a Excel or csv file).""" title = 'Error: unexpected input file' message = ( "Sorry, the input file could not be read. Please make sure that " "the data is save in a valid format (supported formats are: " ".csv, .xlsx).") QMessageBox.critical(self, title, message) def sample_naming_error_message(self) -> None: """Message displayed when none of the sample names passed by the user are found in the input DataFrame.""" title = 'Error: sample names not in input file' message = ( "Sorry, your sample names were not found in the input file. Please " "make sure that the names were typed correctly (case-sensitive).") QMessageBox.critical(self, title, message) def generic_error_message(self, error: Tuple[Exception, str]) -> None: """Error message box used to display any error message (including traceback) for any uncaught errors.""" title = 'An error occurred!' name, trace = error QMessageBox.critical(self, title, f"{str(name)}\n\nfull traceback:\n{trace}") def not_implemented_error_message(self) -> None: """Error message box used when the user accesses a functionality that hasn't been implemented yet.""" title = "Not yet implemented" mes = "Sorry, this functionality has not been implemented yet." QMessageBox.critical(self, title, mes) # ### # ### HELP & QUIT # ### @staticmethod def get_help() -> None: """Opens SCOUTS documentation on the browser. Called when the user clicks the "help" button""" webbrowser.open('https://scouts.readthedocs.io/en/master/') def closeEvent(self, event: QEvent) -> None: """Defines the message box for when the user wants to quit SCOUTS.""" title = 'Quit SCOUTS' mes = "Are you sure you want to quit?" reply = QMessageBox.question(self, title, mes, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.stacked_pages.setEnabled(False) message = self.quit_message() waiter = Waiter(waiter_func=self.threadpool.activeThreadCount) waiter.signals.started.connect(message.show) waiter.signals.finished.connect(message.destroy) waiter.signals.finished.connect(sys.exit) self.threadpool.start(waiter) event.ignore() def quit_message(self) -> QDialog: """Displays a window while SCOUTS is exiting""" message = QDialog(self) message.setWindowTitle('Exiting SCOUTS') message.resize(300, 50) label = QLabel('SCOUTS is exiting, please wait...', message) label.setStyleSheet(self.style['label']) label.adjustSize() label.setAlignment(Qt.AlignCenter) label.move(int((message.width() - label.width()) / 2), int((message.height() - label.height()) / 2)) return message
class MainWindow(QMainWindow): def __init__(self): self.status_bar = None self.progress_bar = None super().__init__() self.create() def create(self): self.create_window() self.create_status_bar() self.create_menu() self.create_central_widgets() def create_window(self): self.setWindowTitle("Bluebonnet Computing") self.resize(1200,700) def create_status_bar(self): self.status_bar = self.statusBar() self.add_permanent_progress_bar() self.display_status("Welcome") def add_permanent_progress_bar(self): self.progress_bar = QProgressBar() progress_bar_label = QLabel("Status: ") #widgets are added proceduarally from left to right self.status_bar.addPermanentWidget(progress_bar_label) self.status_bar.addPermanentWidget(self.progress_bar) def display_status(self, message): timeout = 10000 self.status_bar.showMessage(message, timeout=timeout) def create_menu(self): """ Create the main window menu bar on initialization """ #create menu bar self.menu = self.menuBar() ### FILE ### #file menu self.file_menu = self.menu.addMenu("File") #home option in file home_action = QAction("Home", self) home_action.setShortcut("Ctrl+H") home_action.triggered.connect(self.menu_file_home) self.file_menu.addAction(home_action) #exit option in file exit_action = QAction("Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.menu_file_exit) self.file_menu.addAction(exit_action) ### TASK ### #task menu self.task_menu = self.menu.addMenu("Tasks") #reconcile statements option in tasks reconcile_action = QAction("Reconcile Statements", self) reconcile_action.setShortcut("Alt+R") reconcile_action.triggered.connect(self.menu_task_reconcile) self.task_menu.addAction(reconcile_action) def create_central_widgets(self): """ Application uses central widgets to load menu selections """ #create all widget instances to be used as central widgets self.launch_widget = launch() self.reconcile_widget = reconcile(self.progress_bar) #add above widget instances to a stack instance self.stacked_widgets = QStackedWidget() self.stacked_widgets.addWidget(self.launch_widget) self.stacked_widgets.addWidget(self.reconcile_widget) #on application start set the launch widget to display self.stacked_widgets.setCurrentWidget(self.launch_widget) self.setCentralWidget(self.stacked_widgets) @Slot() def menu_file_home(self): """ Helper for home_action object in self.create_menu Reset application to launch configuration """ self.stacked_widgets.setCurrentWidget(self.launch_widget) self.setCentralWidget(self.stacked_widgets) @Slot() def menu_file_exit(self): """ Helper for exit_action object in self.create_menu """ QApplication.quit() @Slot() def menu_task_reconcile(self): """ Helper for reconcile_action in self.create_menu """ self.stacked_widgets.setCurrentWidget(self.reconcile_widget) self.setCentralWidget(self.stacked_widgets)
class MainWindowUi(QMainWindow): """sets up ui properties of MainWindowUi class""" def __init__(self) -> None: """inits MainWindow class configuring parameters of MainWindow class and inherits from QtWidget.QMainWindow loads .ui file sets up file and directory path vars, inits click events(menuebar, coboboxes, btns) and shows gui the first time Returns: None""" super(MainWindowUi, self).__init__() self.setWindowTitle("It_Hilfe") self.resize(820, 450) self.setWindowIcon(QIcon("./data/favicon2.png")) self.setMinimumSize(700, 250) self.file_path = None self.dir = None self.last_open_file_path = None self.last_open_file_dir = None self.initial_theme = None self.registered_devices = {} self.user_config_file = "./data/user_config.json" # setup stackedwidget self.stacked_widget = QStackedWidget() self.setCentralWidget(self.stacked_widget) self.setup_menubar() self.setup_p_view() self.setup_p_register() self.setup_p_create() self.setup_p_preferences() self.setup_signals() self.font = QFont() self.font.setPointSize(9) self.validate(self.set_user_preferences, file_path=self.user_config_file, schema=validate_json.ItHilfeUserPreferencesSchema, forbidden=[""]) # setup statusbar self.statusbar = self.statusBar() self.stacked_widget.setCurrentWidget(self.p_view) def setup_menubar(self) -> None: """inits menubar Returns: None""" self.menu_Bar = self.menuBar() menu_file = self.menu_Bar.addMenu("file") self.action_open = QAction("open") self.action_save = QAction("save") self.action_new = QAction("new") self.action_print = QAction("print") self.action_preferences = QAction("preferences") self.action_hide_menu_bar = QAction("hide menubar") self.action_print.setShortcut(QKeySequence("Ctrl+p")) self.action_open.setShortcut(QKeySequence("Ctrl+o")) self.action_save.setShortcut(QKeySequence("Ctrl+s")) self.action_hide_menu_bar.setShortcut(QKeySequence("Ctrl+h")) self.action_hide_menu_bar.setIcon(QIcon("./data/show_hide.ico")) self.action_print.setIcon(QIcon("./data/print2.ico")) self.action_open.setIcon(QIcon("./data/open.ico")) self.action_save.setIcon(QIcon("./data/save.ico")) self.action_new.setIcon(QIcon("./data/newfile.ico")) self.action_preferences.setIcon(QIcon("./data/preferences.ico")) menu_file.addAction(self.action_open) menu_file.addAction(self.action_save) menu_file.addAction(self.action_new) menu_file.addAction(self.action_print) menu_file.addAction(self.action_preferences) menu_edit = self.menu_Bar.addMenu("edit") self.action_register = QAction("register") self.action_register.setShortcut(QKeySequence("Ctrl+n")) self.action_register.setIcon(QIcon("./data/register.ico")) menu_edit.addAction(self.action_register) menu_view = self.menu_Bar.addMenu("view") menu_view.addAction(self.action_hide_menu_bar) def setup_p_view(self) -> None: """inits stacked widget page widget Returns: None""" self.p_view = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_view) self.model = QStandardItemModel(self.p_view) self.model.setHorizontalHeaderLabels(labels) self.filters = [] source_model = self.model for filter_num in range(7): filter = QSortFilterProxyModel() filter.setSourceModel(source_model) filter.setFilterKeyColumn(filter_num) source_model = filter self.filters.append(filter) delegate = ComboDelegate() self.table = QtWidgets.QTableView(self.p_view) self.table.setModel(self.filters[-1]) self.table.setItemDelegateForColumn(2, delegate) self.table.horizontalHeader().setStretchLastSection(True) self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.header = FilterHeader(self.table) self.header.set_filter_boxes() self.header.setMaximumHeight(50) self.table.setHorizontalHeader(self.header) self.bt_burger = QPushButton(self.p_view) self.bt_burger.setIcon(QIcon("./data/menu2.svg")) self.bt_burger.setIconSize(QSize(30, 30)) self.bt_burger.setToolTip('slide out description') l_burger = QLabel("menu", self.p_view) self.bt_register_new = QPushButton(self.p_view) self.bt_register_new.setIcon(QIcon("./data/add.ico")) self.bt_register_new.setIconSize(QSize(30, 30)) self.bt_register_new.setToolTip("register new") l_register_new = QLabel("register new", self.p_view) self.bt_delete_column = QPushButton(self.p_view) self.bt_delete_column.setIcon(QIcon("./data/remove.ico")) self.bt_delete_column.setIconSize(QSize(30, 30)) self.bt_delete_column.setToolTip( "delete columns with min 1 cell selected") l_delete = QLabel("delete column", self.p_view) self.bt_hide_show_filter = QPushButton(self.p_view) self.bt_hide_show_filter.setIcon(QIcon("./data/show_hide.ico")) self.bt_hide_show_filter.setIconSize(QSize(30, 30)) self.bt_hide_show_filter.setToolTip("hide/show filter input") l_hide_show = QLabel("hide/show", self.p_view) self.left_btn_frame = QFrame(self.p_view) self.left_btn_frame.setMaximumWidth(40) self.left_btn_frame.setContentsMargins(0, 0, 0, 0) self.left_menu_frame = QFrame(self.p_view) self.left_menu_frame.setMaximumWidth(0) self.left_menu_frame.setContentsMargins(0, 0, 0, 0) p_view_layout2 = QtWidgets.QVBoxLayout(self.left_btn_frame) p_view_layout2.addWidget(self.bt_burger) p_view_layout2.addWidget(self.bt_register_new) p_view_layout2.addWidget(self.bt_delete_column) p_view_layout2.addWidget(self.bt_hide_show_filter) p_view_layout2.setAlignment(Qt.AlignTop) p_view_layout2.setContentsMargins(0, 0, 0, 0) self.p_view_layout3 = QtWidgets.QVBoxLayout(self.left_menu_frame) self.p_view_layout3.addWidget(l_burger) self.p_view_layout3.addWidget(l_register_new) self.p_view_layout3.addWidget(l_delete) self.p_view_layout3.addWidget(l_hide_show) self.p_view_layout3.setAlignment(Qt.AlignTop | Qt.AlignCenter) self.p_view_layout3.setContentsMargins(0, 0, 0, 0) self.p_view_layout3.setSpacing(25) p_view_layout = QHBoxLayout(self.p_view) p_view_layout.setContentsMargins(0, 0, 0, 0) p_view_layout.addWidget(self.left_btn_frame) p_view_layout.addWidget(self.left_menu_frame) p_view_layout.addWidget(self.table) self.p_view.setLayout(p_view_layout) self.p_view.addAction(self.action_open) self.p_view.addAction(self.action_save) self.p_view.addAction(self.action_new) self.p_view.addAction(self.action_print) self.p_view.addAction(self.action_register) self.p_view.addAction(self.action_hide_menu_bar) def setup_p_register(self) -> None: """inits stacked widget page widgets Returns: None""" self.p_register = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_register) l_user = QtWidgets.QLabel("Username", self.p_register) self.in_username = QtWidgets.QLineEdit(self.p_register) l_devicename = QtWidgets.QLabel("Devicename", self.p_register) self.in_devicename = QtWidgets.QLineEdit(self.p_register) l_devicetype = QtWidgets.QLabel("DeviceType", self.p_register) self.in_combobox_devicetype = QtWidgets.QComboBox(self.p_register) l_os = QtWidgets.QLabel("OS", self.p_register) self.in_combobox_os = QtWidgets.QComboBox(self.p_register) l_comment = QtWidgets.QLabel("Comment", self.p_register) self.in_comment = QtWidgets.QTextEdit(self.p_register) self.bt_enter_register = QPushButton("register", self.p_register) self.bt_cancel_register = QPushButton("cancel", self.p_register) p_register_layout = QtWidgets.QVBoxLayout(self.p_register) p_register_layout.addWidget(l_user) p_register_layout.addWidget(self.in_username) p_register_layout.addWidget(l_devicename) p_register_layout.addWidget(self.in_devicename) p_register_layout.addWidget(l_devicetype) p_register_layout.addWidget(self.in_combobox_devicetype) p_register_layout.addWidget(l_os) p_register_layout.addWidget(self.in_combobox_os) p_register_layout.addWidget(l_comment) p_register_layout.addWidget(self.in_comment) p_register_layout.addWidget(self.bt_enter_register) p_register_layout.addWidget(self.bt_cancel_register) def setup_p_create(self) -> None: """inits stacked widget page widget Returns: None""" self.p_create = QtWidgets.QWidget() self.stacked_widget.addWidget(self.p_create) l_new_filepath = QtWidgets.QLabel("new filepath", self.p_create) self.bt_mod_new_path = QPushButton("mod filepath", self.p_create) self.in_new_filepath = QtWidgets.QLineEdit(self.p_create) l_new_filename = QtWidgets.QLabel("new filename", self.p_create) self.in_new_filename = QtWidgets.QLineEdit(self.p_create) self.bt_create = QPushButton("create", self.p_create) self.bt_cancel_create = QPushButton("cancel", self.p_create) p_create_layout = QtWidgets.QVBoxLayout(self.p_create) p_create_layout.addWidget(l_new_filepath) p_create_layout.addWidget(self.in_new_filepath) p_create_layout.addWidget(l_new_filename) p_create_layout.addWidget(self.in_new_filename) p_create_layout.addStretch(100) p_create_layout.addWidget(self.bt_mod_new_path) p_create_layout.addWidget(self.bt_create) p_create_layout.addWidget(self.bt_cancel_create) def setup_p_preferences(self) -> None: """inits setup_p_preferences stacked widget page widget Returns: None""" self.p_preferences = QWidget() self.p_preferences.resize(500, 250) self.p_preferences.setWindowTitle("preferences") self.list_Widget = QListWidget(self.p_preferences) self.list_Widget.addItems(["appearance", "about"]) self.list_Widget.setMaximumWidth(100) self.stacked_widget_preferences = QStackedWidget(self.p_preferences) # setup appearance self.apperence_widget = QWidget() self.stacked_widget_preferences.addWidget(self.apperence_widget) self.in_combo_themes = QComboBox(self.apperence_widget) self.in_combo_themes.addItems(["dark_theme", "light_theme"]) self.in_combo_theme_initial = QComboBox(self.apperence_widget) self.in_combo_theme_initial.addItems(["dark_theme", "light_theme"]) self.text_size_slider = QSlider(QtCore.Qt.Orientation.Horizontal, self.apperence_widget) self.text_size_slider.setTickPosition(QSlider.TickPosition.TicksAbove) self.text_size_slider.setMaximum(15) self.text_size_slider.setMinimum(8) stacked_widget_preferences_layout = QGridLayout(self.apperence_widget) stacked_widget_preferences_layout.setAlignment(QtCore.Qt.AlignTop) stacked_widget_preferences_layout.addWidget(QLabel("theme"), 0, 0) stacked_widget_preferences_layout.addWidget(self.in_combo_themes, 0, 1) stacked_widget_preferences_layout.addWidget(QLabel("initial theme"), 1, 0) stacked_widget_preferences_layout.addWidget( self.in_combo_theme_initial, 1, 1) stacked_widget_preferences_layout.addWidget(QLabel("Fontsize"), 2, 0) stacked_widget_preferences_layout.addWidget(self.text_size_slider, 2, 1) self.about_widget = QWidget() self.stacked_widget_preferences.addWidget(self.about_widget) about_text_edit = QTextEdit(self.about_widget) about_text_edit.setText( "developed by Maurice Jarck\nwith kind support from Shuai Lou\n07.2020-04.2021" ) about_text_edit.setEnabled(False) stacked_widget_about_layout = QGridLayout(self.about_widget) stacked_widget_about_layout.addWidget(about_text_edit) p_apperance_layout = QHBoxLayout(self.p_preferences) p_apperance_layout.addWidget(self.list_Widget) p_apperance_layout.addWidget(self.stacked_widget_preferences) def setup_signals(self) -> None: """connects signals Returns: None""" # header for filter, editor in zip(self.filters, self.header.editors): editor.textChanged.connect(filter.setFilterRegExp) # line edit self.in_new_filename.returnPressed.connect(lambda: self.validate( self.new, line_edit_list=[self.in_new_filepath, self.in_new_filename], data=False)) # comboboxes self.in_combobox_devicetype.addItems( ["choose here"] + [x.__name__ for x in valid_devices]) self.in_combobox_devicetype.currentIndexChanged.connect( lambda: self.update_combobox( self.in_combobox_os, valid_devices[ self.in_combobox_devicetype.currentIndex() - 1].expected_OS )) self.in_combo_themes.currentIndexChanged.connect( lambda: self.change_theme(self.in_combo_themes.currentText())) self.in_combo_theme_initial.currentTextChanged.connect(lambda: setattr( self, "initial_theme", self.in_combo_theme_initial.currentText())) # btns self.bt_delete_column.clicked.connect(self.delete) # self.bt_hide_show_filter.clicked.connect(lambda: self.toggle_hide_show_ani(37, 47, "height", self.header, b"maximumHeight")) self.bt_hide_show_filter.clicked.connect(self.header.hide_show) # self.bt_hide_show_filter.clicked.connect(lambda: self.toggle_hide_show_ani(30, 44, "height", self.header, b"maximumHeight")) self.bt_register_new.clicked.connect( lambda: self.stacked_widget.setCurrentWidget(self.p_register)) self.bt_enter_register.clicked.connect(lambda: self.validate( self.register, line_edit_list=[self.in_username, self.in_devicename], combo_box_list=[self.in_combobox_devicetype, self.in_combobox_os], forbidden=list(self.registered_devices.keys()), checkfname=True)) self.bt_create.clicked.connect(lambda: self.validate( self.new, line_edit_list=[self.in_new_filepath, self.in_new_filename], data=False)) self.bt_mod_new_path.clicked.connect(lambda: self.new(True)) self.bt_burger.clicked.connect(lambda: self.toggle_hide_show_ani( 0, 66, "width", self.left_menu_frame, b"maximumWidth", )) # menu bar self.action_register.triggered.connect( lambda: self.stacked_widget.setCurrentWidget(self.p_register)) self.action_open.triggered.connect(self.get_open_file_path) self.action_save.triggered.connect(self.save) self.action_new.triggered.connect(lambda: self.new(True)) self.action_print.triggered.connect( lambda: self.validate(self.print, data=False, checkfname=True)) self.action_hide_menu_bar.triggered.connect( lambda: self.toggle_hide_show(self.menu_Bar)) self.action_preferences.triggered.connect(self.p_preferences.show) # cancel self.bt_cancel_register.clicked.connect(lambda: self.cancel([ self.in_username, self.in_devicename, self.in_combobox_os, self. in_comment ])) # list widget self.list_Widget.currentRowChanged.connect( lambda: self.stacked_widget_preferences.setCurrentIndex( self.list_Widget.currentIndex().row())) # slider self.text_size_slider.sliderMoved.connect( lambda: self.change_font_size(self.text_size_slider.value())) # self.text_size_slider.sliderMoved.connect(lambda: print(self.text_size_slider.value())) def change_theme(self, theme) -> None: """changes theme according to combobox selection Returns: None""" with open(f"./data/{theme}.css", "r") as file: stylesheed = " ".join(file.readlines()) self.setStyleSheet(stylesheed) self.p_preferences.setStyleSheet(stylesheed) if self.in_combo_themes.currentText() == "dark_theme": self.left_btn_frame.setStyleSheet( u"background: #455364; border: 0px solid;") self.p_view_layout3.setSpacing(30) else: self.left_btn_frame.setStyleSheet( u"background: #ADADAD; border: 0px solid;") self.p_view_layout3.setSpacing(25) return self.in_combo_themes.currentText() def toggle_hide_show_ani(self, collapsed_val: int, expanded_val: int, actual: str, to_animate, property: bytes): """interpolates over a defined range of vales and sets it to a given property of a given widget""" if getattr(to_animate, actual)() == expanded_val: destination = collapsed_val else: destination = expanded_val print(getattr(to_animate, actual)(), destination) self.ani = QPropertyAnimation(to_animate, property) self.ani.setDuration(300) self.ani.setStartValue(getattr(to_animate, actual)()) self.ani.setEndValue(destination) self.ani.setEasingCurve(QEasingCurve.Linear) self.ani.start() def toggle_hide_show(self, widget: QWidget) -> None: """toggles visibiliy of a given widget Arg: widget: widget which is aimed to be hidden or shown Returs: None""" if widget.isVisible(): widget.hide() else: widget.show() def reopen_last_file(self) -> None: """asks for reopening of the last opened file""" if self.last_open_file_path != "" or self.last_open_file_path is not None: reopen_dialog = QMessageBox.question( self.p_view, "reopen last file?", "Do you want to reopen the last edited file?", QMessageBox.Yes | QMessageBox.No) if reopen_dialog == QMessageBox.Yes: self.file_path = self.last_open_file_path self.load() def change_font_size(self, size: int) -> None: """changes all font sizes""" self.font.setPointSize(size) self.menu_Bar.setFont(self.font) self.header.setFont(self.font) self.table.setFont(self.font) self.p_preferences.setFont(self.font) def set_user_preferences(self) -> None: """Reads user_config file and sets its propertys""" with open(self.user_config_file, "r") as config_file: data = dict(json.load(config_file)) self.last_open_file_path = data["last_open_file_path"] self.initial_theme = data['initial_theme'] self.change_font_size(data['font_size']) self.text_size_slider.setValue(data['font_size']) self.in_combo_theme_initial.setCurrentText(self.initial_theme) self.in_combo_themes.setCurrentText(self.initial_theme) with open(f"./data/{self.initial_theme}.css") as file: style_sheed = " ".join(file.readlines()) self.setStyleSheet(style_sheed) self.p_preferences.setStyleSheet(style_sheed) self.bt_burger.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_register_new.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_delete_column.setStyleSheet( "border: 0px solid; background: transparent;") self.bt_hide_show_filter.setStyleSheet( "border: 0px solid; background: transparent;") self.left_menu_frame.setStyleSheet(u" border: 0px solid;") if self.initial_theme == "dark_theme": self.left_btn_frame.setStyleSheet( u"background: #455364; border: 0px solid;") else: self.left_btn_frame.setStyleSheet( u"background: #ADADAD; border: 0px solid;") def cancel(self, widgets: list) -> None: """click event for all cancel buttons shows fist page in stacked widget and clears all widgets in widgets Args: widgets: defines list containing widgets to clear, only widgets with method .clear() are possible Returns: None""" for widget in widgets: widget.clear() self.stacked_widget.setCurrentWidget(self.p_view) def update_combobox(self, box, data: list) -> None: """ clears combo box updates combobox so that old content not needed any more isnt displayed and adds 'choose here' dummy to ensure an index change will be made (updating next box depends on index change) Args: box: instance of pyqt5.QtWidgets.qComboBox data: data supposed to be inserted into combobox Returns: None""" box.clear() box.addItems(["choose here"] + data) def validate(self, command, file_path: str = None, schema=None, line_edit_list: list = None, combo_box_list: list = None, data=None, forbidden: list = None, checkfname: bool = None) -> None: """validates user input Args: command: function to be called after vailidation process if finished line_edit_list: contents pyqt5.QtWidgets.QlineEdit instances to be checked if empty or current text in forbidden or not in allowed combo_box_list: contents pyqt5.QtWidgets.qComboBox instances to be checked if nothing selected data: data to be passed into command function if needed forbidden: houses keys which are not allowed to be entered checkfname: check weather an file path exists or not Returns: None""" fails = 0 if line_edit_list is not None: for x in line_edit_list: if x.text() == "": x.setText("fill all fields") fails += 1 if forbidden is not None and x.text() in forbidden: x.setText("in forbidden!!") fails += 1 if combo_box_list is not None: for combobox in combo_box_list: if combobox.currentText() == "": self.statusbar.showMessage("all comboboxes must be filled") fails += 1 if checkfname is True and self.file_path is None: self.statusbar.showMessage( "no file path specified, visit Ctrl+o or menuebar/edit/open to fix" ) fails += 1 if file_path is not None: if forbidden is not None and file_path in forbidden: fails += 1 self.statusbar.showMessage("select a file to continue") else: try: validate_json.validate(file_path, schema) except ValidationError as e: self.msg_box = QtWidgets.QMessageBox.critical( self, "validation failed", f"Invalid Json file, problem in: {e.messages}") fails += 1 if fails == 0: if data is None: command() else: command(data) else: message = f"problem\ncommand: {command.__name__}\nfails: {fails}" print(message) return message def register(self) -> None: """registers a new device and saves Returns: None""" logic.register(devname=self.in_devicename.text(), devtype=[ device for device in valid_devices if device.__name__ == self.in_combobox_devicetype.currentText() ].pop(), username=self.in_username.text(), os=self.in_combobox_os.currentText(), comment=self.in_comment.toPlainText(), datetime=str(datetime.datetime.now()), registered_devices=self.registered_devices) new_values = [ self.in_devicename.text(), self.in_username.text(), self.in_combobox_os.currentText(), [ device.__name__ for device in valid_devices if device.__name__ == self.in_combobox_devicetype.currentText() ].pop(), self.in_comment.toPlainText(), str(datetime.datetime.now()) ] row = [QStandardItem(str(item)) for item in new_values] self.model.appendRow(row) self.stacked_widget.setCurrentWidget(self.p_view) self.in_devicename.clear() self.in_username.clear() self.in_combobox_os.clear() self.in_comment.clear() self.save() def delete(self) -> None: """deletes all rows associated with min 1 slected cell Returns: None""" rows = sorted(set(index.row() for index in self.table.selectedIndexes()), reverse=True) qb = QMessageBox() answ = qb.question(self, 'delete rows', f"Are you sure to delete {rows} rows?", qb.Yes | qb.No) if answ == qb.Yes: for row in rows: self.registered_devices.pop( str(self.model.index(row, 0).data())) self.model.removeRow(row) qb.information(self, 'notification', f"deleted {rows} row") else: qb.information(self, 'notification', "Nothing Changed") self.save() def get_open_file_path(self) -> None: """gets file-path and set it to self.file_path, extra step for json validation Returns: None""" self.file_path = \ QFileDialog.getOpenFileName(self, "open file", f"{self.last_open_file_dir or 'c://'}", "json files (*json)")[0] self.validate(command=self.load, file_path=self.file_path, schema=validate_json.ItHilfeDataSchema, forbidden=[""]) def load(self) -> None: """opens json file and loads its content into registered devices Returns: None""" self.model.clear() self.registered_devices.clear() with open(self.file_path, "r") as file: data = dict(json.load(file)) devices = data["devices"].values() self.last_open_file_dir = data["last_open_file_dir"] for value in devices: row = [] for i, item in enumerate(value): cell = QStandardItem(str(item)) row.append(cell) if i == 0 or i == 3 or i == 5: cell.setEditable(False) self.model.appendRow(row) new = [x for x in valid_devices if x.__name__ == value[3]].pop(0)(value[0], value[1], value[4], value[5]) new.OS = value[2] self.registered_devices[value[0]] = new self.model.setHorizontalHeaderLabels(labels) self.statusbar.showMessage("") # auto complete for a in range(len(self.header.editors)): completer = QCompleter([ self.model.data(self.model.index(x, a)) for x in range(self.model.rowCount()) ]) completer.setCompletionMode(QCompleter.InlineCompletion) self.header.editors[a].setCompleter(completer) def save(self) -> None: """saves content fo self.registered_devices into specified json file Returns: None""" if not self.file_path: self.statusbar.showMessage( "no file path set all changes get lost if closed") else: with open( self.file_path, 'w', ) as file: devices = { k: [ v.name, v.user, v.OS, v.__class__.__name__, v.comment, v.datetime ] for (k, v) in enumerate(self.registered_devices.values()) } last_open_file_dir = "/".join(self.file_path.split("/")[:-1]) resulting_dict = { "devices": devices, "last_open_file_dir": last_open_file_dir } json.dump(resulting_dict, file) self.statusbar.showMessage("saved file") with open(self.user_config_file, "w") as user_preferences_file: json.dump( { "last_open_file_path": self.last_open_file_path, "initial_theme": self.initial_theme, "font_size": self.text_size_slider.value() }, user_preferences_file) def new(self, stage: bool, test: bool = False) -> None: """creates new csv file to save into stage is True: set filepath stage is False: set new name, save Args: stage: determines a which stage to execute this function Returns: None""" if stage is True: if not test: self.dir = QFileDialog.getExistingDirectory( self, "select a folder", "c://") self.stacked_widget.setCurrentWidget(self.p_create) self.in_new_filepath.setText(self.dir) self.registered_devices.clear() else: self.file_path = self.dir + f"/{self.in_new_filename.text()}.json" self.save() self.stacked_widget.setCurrentWidget(self.p_view) def print(self, test: bool) -> None: """setup and preview pViewTable for paper printing Returns: None""" with open(self.file_path) as f: self.data = json.dumps(dict(json.load(f)), sort_keys=True, indent=6, separators=(".", "=")) self.document = QtWidgets.QTextEdit() self.document.setText(self.data) if not test: printer = QPrinter() previewDialog = QPrintPreviewDialog(printer, self) previewDialog.paintRequested.connect( lambda: self.document.print_(printer)) previewDialog.exec_()
class ExperimentView(QMainWindow): """View widget for Experiment class and the main window of this application.""" def __init__(self, parent=None): super().__init__(parent=parent) self._model = None self._save_path = None """Save path of the experiment that is being edited.""" self._processes = [] """Process handles for experiments that were started.""" # Exceptions --------------------------------------------------------------------------------------------------- self.__excepthook__ = sys.excepthook sys.excepthook = self.excepthook # Menu bar ----------------------------------------------------------------------------------------------------- menubar = self.menuBar() filemenu = menubar.addMenu("File") action_new = filemenu.addAction("New") action_new.triggered.connect(self.actionNew) action_new.setShortcut(QKeySequence.New) action_open = filemenu.addAction("Open") action_open.triggered.connect(self.actionOpen) action_open.setShortcut(QKeySequence.Open) action_import = filemenu.addAction("Import") action_import.triggered.connect(self.actionImport) filemenu.addSeparator() action_save = filemenu.addAction("Save") action_save.triggered.connect(self.actionSave) action_save.setShortcut(QKeySequence.Save) action_save_as = filemenu.addAction("Save As...") action_save_as.triggered.connect(self.actionSaveAs) action_save_as.setShortcut(QKeySequence.SaveAs) action_export = filemenu.addAction("Export...") action_export.triggered.connect(self.actionExport) filemenu.addSeparator() action_exit = filemenu.addAction("Exit") action_exit.triggered.connect(self.close) runmenu = menubar.addMenu("Run") action_start_ex = runmenu.addAction("Start Experiment") action_start_ex.triggered.connect(self.actionStartExperiment) action_start_ex.setShortcut("F9") # Property tree ------------------------------------------------------------------------------------------------ self.tree = PropertyTree() self.tree_view = self.tree.getView() self.tree_view.currentIndexChanged.connect(self.displayTreeItem) # Property tree dock widget ------------------------------------------------------------------------------------ self.property_tree_dock = QDockWidget("Properties", self) self.property_tree_dock.setWidget(self.tree_view) self.property_tree_dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.property_tree_dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.addDockWidget(Qt.LeftDockWidgetArea, self.property_tree_dock) # Editing widgets ---------------------------------------------------------------------------------------------- self.general_view = GeneralView() self.signal_editor = SchemeEditor() for name in node_types: self.signal_editor.toolbox().addItem(name, node_types[name]()) self.blocks = StackedDictWidget() self.groups = StackedDictWidget() # Sequence editor ---------------------------------------------------------------------------------------------- self.sequence_editor = SequenceEditor() # Central widget ----------------------------------------------------------------------------------------------- self.central_widget = QStackedWidget() self.setCentralWidget(self.central_widget) scrollarea = QScrollArea() scrollarea.setWidget(self.general_view) self.central_widget.addWidget(scrollarea) self.central_widget.addWidget(self.signal_editor) self.central_widget.addWidget(self.blocks) self.central_widget.addWidget(self.groups) self.central_widget.addWidget(self.sequence_editor) # New experiment view is created with a new experiment --------------------------------------------------------- self.actionNew() # Get/Set methods ================================================================================================== def model(self) -> Optional[Experiment]: """Return the experiment assosiated with this view.""" return self._model def projectTitle(self) -> str: """Return document title, i.e. name of file this experiment is saved as. If no file has been associated with this experiment, returns "Untitled". """ if self.savePath(): return Path(self.savePath()).stem return "Untitled" def savePath(self) -> Optional[str]: """Return path of the file from which the """ return self._save_path def setModel(self, ex: Experiment, /): self._model = ex self.tree.setExperiment(ex) self.signal_editor.setScheme(ex.signal_scheme) self.sequence_editor.setScheme(ex.sequence_scheme) ex.blocks.itemAdded.connect(self._onBlockAdded) ex.blocks.itemRenamed.connect(self._onBlockRenamed) ex.blocks.itemRemoved.connect(self._onBlockRemoved) ex.groups.itemAdded.connect(self._onGroupAdded) ex.groups.itemRenamed.connect(self._onGroupRenamed) ex.groups.itemRemoved.connect(self._onGroupRemoved) self.updateView() # Experiment syncronization ======================================================================================== def updateModel(self): """Update the experiment when data in the view was changed by the user.""" ex = self.model() if ex is None: return # For each block, write it's data to the experiment for name in self.blocks.keys(): block_view: BlockView = self.blocks.widget(name).widget() block_view.updateModel() # For each group, write its data to the experiment for name in self.groups.keys(): group_view: GroupView = self.groups.widget(name).widget() group_view.updateModel() # Write general experiment data self.general_view.updateModel(ex) # Write the selected sequence ex.sequence = [ node.title() for node in self.sequence_editor.selectedSequence()[1] ] def updateView(self): ex = self.model() if ex is None: return # General properties ------------------------------------------------------------------------------------------- general = self.general_view general.name.setText(ex.name) general.inlet_type.setCurrentText( general.inlet_type_import_values[ex.inlet]) general.lsl_stream_name.setCurrentText(ex.lsl_stream_name) general.lsl_filename.setText(ex.raw_data_path) general.hostname_port.setText(ex.hostname_port) general.dc.setChecked(ex.dc) if ex.prefilter_band[0] is None: general.prefilter_lower_bound_enable.setChecked(False) general.prefilter_lower_bound.setValue(0) else: general.prefilter_lower_bound_enable.setChecked(True) general.prefilter_lower_bound.setValue(ex.prefilter_band[0]) if ex.prefilter_band[1] is None: general.prefilter_upper_bound_enable.setChecked(False) general.prefilter_upper_bound.setValue(0) else: general.prefilter_upper_bound_enable.setChecked(True) general.prefilter_upper_bound.setValue(ex.prefilter_band[1]) general.plot_raw.setChecked(ex.plot_raw) general.plot_signals.setChecked(ex.plot_signals) general.discard_channels.setText(ex.discard_channels) general.reference_sub.setText(ex.reference_sub) general.show_photo_rectangle.setChecked(ex.show_photo_rectangle) general.show_notch_filters.setChecked(ex.show_notch_filters) # Blocks and groups -------------------------------------------------------------------------------------------- while self.tree.blocks.rowCount() > 0: name = self.tree.blocks.child(0).text() self.tree.blocks.removeRow(0) self.blocks.removeWidget(name) self.sequence_editor.toolbox().removeItem(name) while self.tree.groups.rowCount() > 0: name = self.tree.groups.child(0).text() self.tree.groups.removeRow(0) self.groups.removeWidget(name) self.sequence_editor.toolbox().removeItem(name) for name in ex.blocks: ex.blocks.itemAdded.emit(name) for name in ex.groups: ex.groups.itemAdded.emit(name) # Sequence ----------------------------------------------------------------------------------------------------- for sgraph, slist, button in self.sequence_editor.sequences(): if ex.sequence == [node.title() for node in slist]: button.setChecked(True) break def _onBlockAdded(self, name): """Function that gets called when a new block has been added to the experiment.""" # Add an item to the property tree tree_item = QStandardItem(name) tree_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable) self.tree.blocks.appendRow(tree_item) # Add a view to the widget stack block_view = BlockView() block_view.setModel(self.model().blocks[name]) scrollarea = QScrollArea() scrollarea.setWidget(block_view) self.blocks.addWidget(name, scrollarea) # Add a node to draw the sequence node = BlockNode() node.setTitle(name) self.sequence_editor.toolbox().addItem(name, node) # Select this item self.tree_view.setCurrentIndex(self.tree.indexFromItem(tree_item)) def _onGroupAdded(self, name): """Function that gets called when a new group has been added to the experiment.""" # Add an item to the property tree tree_item = QStandardItem(name) tree_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable) self.tree.groups.appendRow(tree_item) # Add a view to the widget stack group_view = GroupView() group_view.setModel(self.model().groups[name]) scrollarea = QScrollArea() scrollarea.setWidget(group_view) self.groups.addWidget(name, scrollarea) # Add a node to draw the sequence node = GroupNode() node.setTitle(name) self.sequence_editor.toolbox().addItem(name, node) # Select this item self.tree_view.setCurrentIndex(self.tree.indexFromItem(tree_item)) def _onBlockRenamed(self, old_name, new_name): """Function that gets called when a block has been renamed.""" # Rename it in the property tree for i in range(self.tree.blocks.rowCount()): item = self.tree.blocks.child(i) if item.text() == old_name: item.setText(new_name) break # Rename in widget stack current_key = self.blocks.currentKey() w = self.blocks.removeWidget(old_name) self.blocks.addWidget(new_name, w) if current_key == old_name: self.blocks.setCurrentKey(new_name) # Rename in the sequence editor node = self.sequence_editor.toolbox().removeItem(old_name) node.setTitle(new_name) self.sequence_editor.toolbox().addItem(new_name, node) for node in self.sequence_editor.scheme().graph.nodes: if node.title() == old_name: node.setTitle(new_name) # Rename it in the sequence editor's current sequence widget for _1, _2, button in self.sequence_editor.sequences(): label = button.text() new_label = " → ".join( [new_name if x == old_name else x for x in label.split(" → ")]) button.setText(new_label) def _onGroupRenamed(self, old_name, new_name): """Function that gets called when a group has been renamed.""" # Rename it in the property tree for i in range(self.tree.groups.rowCount()): item = self.tree.groups.child(i) if item.text() == old_name: item.setText(new_name) break # Rename in widget stack current_key = self.groups.currentKey() w = self.groups.removeWidget(old_name) self.groups.addWidget(new_name, w) if current_key == old_name: self.groups.setCurrentKey(new_name) # Rename in the sequence editor node = self.sequence_editor.toolbox().removeItem(old_name) node.setTitle(new_name) self.sequence_editor.toolbox().addItem(new_name, node) for node in self.sequence_editor.scheme().graph.nodes: if node.title() == old_name: node.setTitle(new_name) # Rename it in the sequence editor's current sequence widget for _1, _2, button in self.sequence_editor.sequences(): label = button.text() new_label = " → ".join( [new_name if x == old_name else x for x in label.split(" → ")]) button.setText(new_label) def _onBlockRemoved(self, name): """Function that gets called when a block has been removed from the experiment.""" # Find and remove it from the property tree for i in range(self.tree.blocks.rowCount()): item = self.tree.blocks.child(i) if item.text() == name: self.tree.blocks.removeRow(i) break # Remove from widget stack and the sequence editor toolbox self.blocks.removeWidget(name) self.sequence_editor.toolbox().removeItem(name) def _onGroupRemoved(self, name): """Function that gets called when a block has been removed from the experiment.""" # Find and remove it from the property tree for i in range(self.tree.groups.rowCount()): item = self.tree.groups.child(i) if item.text() == name: self.tree.groups.removeRow(i) break # Remove from widget stack and the sequence editor toolbox self.groups.removeWidget(name) self.sequence_editor.toolbox().removeItem(name) # Property tree syncronization ===================================================================================== def displayTreeItem(self, index: QModelIndex): """Display the widget corresponding to the item in the property tree.""" item = self.tree.itemFromIndex(index) if item is self.tree.general: # General scrollarea = self.general_view.parent().parent() assert type(scrollarea) == QScrollArea self.central_widget.setCurrentWidget(scrollarea) elif item is self.tree.signals: # Signal editor self.central_widget.setCurrentWidget(self.signal_editor) elif item.parent() is self.tree.blocks: # A block self.central_widget.setCurrentWidget(self.blocks) self.blocks.setCurrentKey(item.text()) elif item.parent() is self.tree.groups: # A group self.central_widget.setCurrentWidget(self.groups) self.groups.setCurrentKey(item.text()) elif item is self.tree.sequence: # Sequence editor self.central_widget.setCurrentWidget(self.sequence_editor) # User actions ===================================================================================================== def actionNew(self) -> bool: if self.model() and not self.promptSaveChanges(): return False # Action cancelled during prompt self.setModel(Experiment()) self._save_path = None self.setWindowTitle(self.projectTitle() + " - NFB Studio") return True def actionExport(self) -> bool: self.updateModel() if (len(self.model().sequence_scheme.graph.nodes) == 0): # No nodes in sequence scheme, cancel operation # TODO: A better way to signal to the user that he needs to create a sequence? self.central_widget.setCurrentWidget(self.sequence_editor) return False data = self.model().export() file_path = QFileDialog.getSaveFileName(filter="XML Files (*.xml)")[0] if file_path == "": return False # Action was cancelled if os.path.splitext(file_path)[1] == "": # No extension file_path = file_path + ".xml" with open(file_path, "w", encoding="utf-8") as file: file.write(data) return True def actionSave(self) -> bool: """User action "Save". Saves file to its location, or promts user if no location exists yet. Returns True if file was saved, and False if action was cancelled. """ if self.savePath() is None: return self.actionSaveAs() self.fileSave(self.savePath()) self.setWindowTitle(self.projectTitle() + " - NFB Studio") return True def actionSaveAs(self) -> bool: """User action "Save As". Promts user to save file as. Returns True if file was saved, and False if action was cancelled. """ path = QFileDialog.getSaveFileName( filter="Experiment Files (*.nfbex)")[0] if path == "": return False # Action was cancelled if os.path.splitext(path)[1] == "": # No extension path = path + ".nfbex" self._save_path = path self.fileSave(self.savePath()) self.setWindowTitle(self.projectTitle() + " - NFB Studio") return True def actionOpen(self) -> bool: """User action "Open". Promts user to open a file. Returns True if file was opened, and False if action was cancelled. """ if self.model() and not self.promptSaveChanges(): return False # Action cancelled during prompt path = QFileDialog.getOpenFileName( filter="Experiment Files (*.nfbex)")[0] if path == "": return False self.fileOpen(path) self.setWindowTitle(self.projectTitle() + " - NFB Studio") return True def actionImport(self) -> bool: """User action "Import". Promts user to import a file. Returns True if file was imported, and False if action was cancelled or failed. """ if self.model() and not self.promptSaveChanges(): return False # Action cancelled during prompt file_path = QFileDialog.getOpenFileName(filter="XML Files (*.xml)")[0] if file_path == "": return False try: with open(file_path, encoding="utf-8") as file: data = file.read() except UnicodeDecodeError: try: with open(file_path, encoding="cp1251") as file: data = file.read() except: QMessageBox.critical( title="Unable to read the file", text="NFB Studio was unable to read this file.") return False ex = Experiment.import_xml(data) self.setModel(ex) return True def actionStartExperiment(self): self.updateModel() if (len(self.model().sequence_scheme.graph.nodes) == 0): # No nodes in sequence scheme, cancel operation # TODO: A better way to signal to the user that he needs to create a sequence? self.central_widget.setCurrentWidget(self.sequence_editor) return results_path = QFileDialog.getExistingDirectory( caption="Select a folder to save experiment results", options=QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) if results_path == "": return False # Action was cancelled data = self.model().export() temp_dir = QDir.tempPath() + "/nfb_studio" os.makedirs(temp_dir, exist_ok=True) timestamp = datetime.now() file_path = "{}/experiment ({:04d}-{:02d}-{:02d} {:02d}-{:02d}-{:02d}).xml".format( temp_dir, timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second) with open(file_path, "w", encoding="utf-8") as file: file.write(data) proc = Process(target=run, args=(file_path, results_path)) proc.start() self._processes.append(proc) def promptSaveChanges(self) -> bool: """Prompt the user to save changes to current project. Display a message box asking the user if they want to save changes. If user selects Save, execute actionSave. Returns True if the user decided to discard changes or saved the file, and False if the user cancelled operation either during prompt or during save. """ prompt = QMessageBox() prompt.setWindowTitle("NFB Studio") prompt.setIcon(QMessageBox.Warning) prompt.setText("Save changes to \"{}\" before closing?".format( self.projectTitle())) prompt.setStandardButtons(QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) prompt.setDefaultButton(QMessageBox.Save) answer = prompt.exec_() if answer == QMessageBox.Cancel: return False if answer == QMessageBox.Save: return self.actionSave() return True def closeEvent(self, event): if self.model(): event.setAccepted(self.promptSaveChanges()) else: event.accept() # File operations ================================================================================================== def fileOpen(self, path): with open(path, encoding="utf-8") as file: data = file.read() ex = Experiment.load(data) self.setModel(ex) self._save_path = path def fileSave(self, path): self.updateModel() data = self.model().save() with open(path, "w", encoding="utf-8") as file: file.write(data) # Exception handling =============================================================================================== def excepthook(self, etype, value, tb): message = QMessageBox(self) message.setWindowTitle("Critical Error") message.setText( "An unknown critical error occured. It is recommended to save your work and restart NFB Studio.\n\n" "Please inform the developer, describing what you were doing before the error, and attach the text below." ) message.setIcon(message.Icon.Critical) exception_field = QTextEdit() exception_field.setText("".join( traceback.format_exception(etype, value, tb))) exception_field.setReadOnly(True) message.layout().addWidget(exception_field, 1, 0, 1, -1) message.exec_() self.__excepthook__(etype, value, tb)
# --------------------------- # StackedWidgetに登録されたwidgetをwidgetのインスタンスで指定して表示する # --------------------------- import sys from PySide2.QtWidgets import QApplication, QTextEdit, QStackedWidget app = QApplication(sys.argv) qw_text_edit_1 = QTextEdit() qw_text_edit_1.append('1') qw_text_edit_2 = QTextEdit() qw_text_edit_2.append('2') qw_stack = QStackedWidget() qw_stack.addWidget(qw_text_edit_1) qw_stack.addWidget(qw_text_edit_2) # 表示するwidgetをwidgetのインスタンスで指定する qw_stack.setCurrentWidget(qw_text_edit_2) print(qw_stack.currentWidget()) qw_stack.show() sys.exit(app.exec_())
class PrefWindow(QMainWindow, Ui_PrefWindow): def __init__(self, parent=None, person=None, type_res=None, fd_res=None): super().__init__(parent) self.setupUi(self) self.person = person self.type_res = type_res self.fd_res = fd_res self.add_widgets() self.setup_connections() #self.resize(900,600) self.read_settings() self.show() def get_sex_bd(self): sex, bd_year, bd_mon, bd_day = spartan.get_sex_bd_from_db() return (sex, (bd_year, bd_mon, bd_day)) def add_widgets(self): sex, bd = self.get_sex_bd() self.pref_widget = PrefWidget(sex, bd, self.type_res, self.fd_res) self.req_widget = ReqWidget(self.person, sex, *bd) self.res_widget = ResWidget(self.type_res, self.fd_res) self.stacked_widget = QStackedWidget() self.stacked_widget.addWidget(self.pref_widget) self.stacked_widget.addWidget(self.req_widget) self.stacked_widget.addWidget(self.res_widget) central_widget = QWidget() self.setCentralWidget(central_widget) lay = QVBoxLayout(central_widget) lay.addWidget(self.stacked_widget) def show_req(self): self.stacked_widget.setCurrentWidget(self.req_widget) def show_res(self): self.stacked_widget.setCurrentWidget(self.res_widget) def show_pref(self): self.pref_widget.sex = self.req_widget.sex self.pref_widget.bd = (self.req_widget.bd_year, self.req_widget.bd_mon, self.req_widget.bd_day) self.pref_widget.set_preview_text() self.stacked_widget.setCurrentWidget(self.pref_widget) def setup_connections(self): self.pref_widget.req_btn.clicked.connect(self.show_req) self.pref_widget.res_btn.clicked.connect(self.show_res) self.req_widget.back_btn.clicked.connect(self.show_pref) self.res_widget.back_btn.clicked.connect(self.show_pref) def closeEvent(self, event): settings = QSettings("spartan", "spartan") settings.setValue("pref/geometry", self.saveGeometry()) settings.setValue("pref/windowState", self.saveState()) super().closeEvent(event) def read_settings(self): settings = QSettings("spartan", "spartan") self.restoreGeometry(settings.value("pref/geometry")) self.restoreState(settings.value("pref/windowState"))
class Anime(QFrame): def __init__(self, parent=None): QFrame.__init__(self, parent) self.m_mouse_down = False self.setFrameShape(QFrame.StyledPanel) css = """ QFrame{ Background: #212121; color:white; font: 12pt "Roboto"; font-weight:bold; } """ self.setStyleSheet(css) self.setWindowFlags(Qt.FramelessWindowHint) self.setMouseTracking(True) self.m_titleBar = TitleBar() self.m_content = QWidget(self) vbox = QVBoxLayout(self) vbox.addWidget(self.m_titleBar) vbox.setMargin(0) vbox.setSpacing(0) layout = QVBoxLayout(self) layout.addWidget(self.m_content) layout.setMargin(5) layout.setSpacing(0) vbox.addLayout(layout) self.widgets() def contentWidget(self): return self.m_content def titleBar(self): return self.m_titleBar def mousePressEvent(self, event): self.m_old_pos = event.pos() self.m_mouse_down = event.button() == Qt.LeftButton def mouseMoveEvent(self, event): x = event.x() y = event.y() def mouseReleaseEvent(self, event): m_mouse_down = False def widgets(self): self.combined_hor_layout = QHBoxLayout(self.contentWidget()) self.combined_hor_layout.setMargin(0) ################################################################################################################ # on the left ################################################################################################################ self.option_layout = QVBoxLayout(self.contentWidget()) self.download_anime = QPushButton(self) self.manga = QPushButton(self) self.left_button_properties() self.option_layout.addWidget(self.download_anime, stretch=0) self.option_layout.addWidget(self.manga, stretch=0) self.option_layout.addStretch() self.option_layout.setSpacing(10) ################################################################################################################ # on the right ################################################################################################################ self.vertical_right_layout = QVBoxLayout(self.contentWidget()) self.vertical_right_layout.setMargin(1) self.stack = QStackedWidget(self) self.page_1 = QWidget() self.page_2 = QWidget() self.stack_details() self.stack.addWidget(self.page_1) self.stack.addWidget(self.page_2) self.stack.setCurrentWidget(self.page_1) self.vertical_right_layout.addWidget(self.stack) ################################################################################################################ self.combined_hor_layout.addLayout(self.option_layout) self.combined_hor_layout.addLayout(self.vertical_right_layout) def left_button_properties(self): self.button_css = '''QPushButton {color: #ffffff; background-color: #212121; border: 0px solid; font:13px \"Roboto\";} QPushButton:hover {background-color: rgb(85, 170, 255);}''' self.download_anime.setText("Download Anime") self.download_anime.setStyleSheet(self.button_css) self.download_anime.setMinimumHeight(40) self.download_anime.setCursor(Qt.PointingHandCursor) self.download_anime.setFixedWidth(120) self.download_anime.clicked.connect(lambda: self.stack.setCurrentWidget(self.page_1)) self.manga.setText("Read mangas") self.manga.setStyleSheet(self.button_css) self.manga.setMinimumHeight(40) self.manga.setCursor(Qt.PointingHandCursor) self.manga.setFixedWidth(120) self.manga.clicked.connect(lambda: self.stack.setCurrentWidget(self.page_2)) def stack_details(self): page_css = '''background-color: #2D2D2D; color: #ffffff;''' ################################################################################################################ # page1 ################################################################################################################ self.page_1.setStyleSheet(page_css) self.page_1_layout = QVBoxLayout(self) self.anime = QWebEngineView() self.anime.setUrl(QUrl("https://animixplay.to")) self.page_1_layout.addWidget(self.anime) self.page_1.setLayout(self.page_1_layout) ################################################################################################################ # page 2 ################################################################################################################ self.page_2.setStyleSheet(page_css) self.page_2_layout = QVBoxLayout(self) self.manga_read = QWebEngineView() self.manga_read.setUrl(QUrl("https://manganelo.com")) self.page_2_layout.addWidget(self.manga_read) self.page_2.setLayout(self.page_2_layout)