def run(self): """ Runs the Matlab script """ config = Config() subprocess.run( [config.get_matlab_path(), '-nodisplay', '-r', self.matlab_script])
def closeEvent(self, event): """Override the QWidget closing event to check if there are unsaved modifications :param event: closing event """ if self.check_unsaved_modifications() == False or self.test: can_exit = True else: self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_exit = self.pop_up_close.can_exit() if can_exit: self.project.unsaveModifications() # Clean up config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.remove_raw_files_useless() event.accept() else: event.ignore()
def loadSavedProjects(self): """Load the savedProjects dictionary from the saved_projects.yml file. If the saved_projects.yml file is not existing, it is created with the "{paths: []}" value and the returned dictionnary is {paths: []}. :returns: the dictionary """ config = Config() try: with open( os.path.join(config.get_config_path(), 'saved_projects.yml'), 'r') as stream: try: if version.parse(yaml.__version__) > version.parse("5.1"): return yaml.load(stream, Loader=yaml.FullLoader) else: return yaml.load(stream) except yaml.YAMLError as exc: print(exc) except FileNotFoundError as exc: with open( os.path.join(config.get_config_path(), 'saved_projects.yml'), 'w') as stream: yaml.dump({'paths': []}, stream, default_flow_style=False) return {'paths': []}
def set_projects_directory_as_default(dialog): """ Sets the projects directory as default :param dialog: current file dialog """ config = Config() projects_directory = config.get_projects_save_path() if not os.path.exists(projects_directory): os.makedirs(projects_directory) dialog.setDirectory(projects_directory)
def verify_slices(self, file_paths): """Make 'Show all slices' checkbox unclickable if len(file_paths) > 1. :param file_paths: the selected documents """ # Updating the config self.config = Config() if len(file_paths) > 1: self.config.setShowAllSlices(False) self.check_box_slices.setCheckState(Qt.Unchecked) self.check_box_slices.setCheckable(False) else: self.check_box_slices.setCheckable(True) self.show_slices(file_paths)
def saveSavedProjects(self): """Saves the savedProjects dictionary to the saved_projects.yml file.""" config = Config() with (open(os.path.join(config.get_config_path(), 'saved_projects.yml'), 'w', encoding='utf8')) as configfile: yaml.dump(self.savedProjects, configfile, default_flow_style=False, allow_unicode=True)
def rows_borders_added(self, links): """Add the links and the added row to the good rows :param links: Old links to reput """ # Plus added to the last row sources_images_dir = Config().getSourceImageDir() add_search_bar_label = ClickableLabel() add_search_bar_label.setObjectName('plus') add_search_bar_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "green_plus.png"))) add_search_bar_picture = add_search_bar_picture.scaledToHeight(20) add_search_bar_label.setPixmap(add_search_bar_picture) add_search_bar_label.clicked.connect(self.add_search_bar) self.rows[len(self.rows) - 1][6] = add_search_bar_label # Link added to every row, except the first one for i in range(1, len(self.rows)): row = self.rows[i] link_choice = QComboBox() link_choice.setObjectName('link') link_choice.addItem("AND") link_choice.addItem("OR") if len(links) >= i: link_choice.setCurrentText(links[i - 1]) row[0] = link_choice
def update_package_library_action(self): """Update the package library action depending on the mode.""" if Config().get_clinical_mode() == True: self.action_package_library.setDisabled(True) # self.action_install_processes.setDisabled(True) else: self.action_package_library.setEnabled(True)
def create_view_actions(self): """Create the actions and their shortcuts in each menu""" self.action_create.setShortcut('Ctrl+N') self.action_open.setShortcut('Ctrl+O') self.action_save.setShortcut('Ctrl+S') self.addAction(self.action_save) self.action_save_as.setShortcut('Ctrl+Shift+S') self.addAction(self.action_save_as) self.action_import.setShortcut('Ctrl+I') for i in range(self.config.get_max_projects()): self.saved_projects_actions.append( QAction(self, visible=False, triggered=self.open_recent_project)) if Config().get_clinical_mode() == True: self.action_package_library.setDisabled(True) else: self.action_package_library.setEnabled(True) self.action_exit.setShortcut('Ctrl+W') self.action_undo.setShortcut('Ctrl+Z') self.action_redo.setShortcut('Ctrl+Y') # if Config().get_clinical_mode() == True: # self.action_install_processes.setDisabled(True) # else: # self.action_install_processes.setEnabled(True) # Connection of the several triggered signals of the actions to some # other methods self.action_create.triggered.connect(self.create_project_pop_up) self.action_open.triggered.connect(self.open_project_pop_up) self.action_exit.triggered.connect(self.close) self.action_save.triggered.connect(self.save) self.action_save_as.triggered.connect(self.save_as) self.action_import.triggered.connect(self.import_data) self.action_see_all_projects.triggered.connect(self.see_all_projects) self.action_project_properties.triggered.connect( self.project_properties_pop_up) self.action_software_preferences.triggered.connect( self.software_preferences_pop_up) self.action_package_library.triggered.connect( self.package_library_pop_up) self.action_undo.triggered.connect(self.undo) self.action_redo.triggered.connect(self.redo) self.action_documentation.triggered.connect(self.documentation) self.action_install_processes_folder.triggered.connect( lambda: self.install_processes_pop_up(folder=True)) self.action_install_processes_zip.triggered.connect( lambda: self.install_processes_pop_up(folder=False))
def verify_slices(self, file_paths): """Make 'Show all slices' checkbox unclickable if len(file_paths) > 1. :param file_paths: the selected documents """ # Updating the config self.config = Config() if len(file_paths) > 1: self.config.setShowAllSlices(False) self.check_box_slices.setCheckState(Qt.Unchecked) self.check_box_slices.setCheckable(False) else: self.check_box_slices.setCheckable(True) self.show_slices(file_paths) if self.config.isRadioView() == True: self.label_orientation.setText("Radiological orientation") else: self.label_orientation.setText("Neurological orientation")
def create_tabs(self): """Create the tabs and initializes the DataBrowser and PipelineManager classes.""" self.config = Config() self.tabs.setAutoFillBackground(False) self.tabs.setStyleSheet('QTabBar{font-size:16pt;text-align: center}') self.tabs.setMovable(True) self.tabs.addTab(self.data_browser, "Data Browser") # To uncomment when the Data Viewer will be created # self.image_viewer = ImageViewer() self.tabs.addTab(self.image_viewer, "Data Viewer") self.tabs.addTab(self.pipeline_manager, "Pipeline Manager") self.tabs.currentChanged.connect(self.tab_changed) vertical_layout = QVBoxLayout() vertical_layout.addWidget(self.tabs) self.centralWindow.setLayout(vertical_layout)
def create_project_pop_up(self): """Create a new project.""" if self.check_unsaved_modifications(): self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_switch = self.pop_up_close.can_exit() else: can_switch = True if can_switch: # Opens a pop-up when the 'New project' action is clicked and # updates the recent projects self.exPopup = PopUpNewProject() if self.exPopup.exec_(): self.project.session.unsave_modifications() self.remove_raw_files_useless() # We remove the useless # files from the old project file_name = self.exPopup.selectedFiles() self.exPopup.get_filename(self.exPopup.selectedFiles()) file_name = self.exPopup.relative_path # Removing the old project from the list of currently opened # projects config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.project = Project(self.exPopup.relative_path, True) self.update_project(file_name) # project updated everywhere
def layout_view(self): """Create the layout.""" sources_images_dir = Config().getSourceImageDir() self.button_cross.setStyleSheet('background-color:rgb(255, 255, 255);') self.button_cross.setIcon( QIcon(os.path.join(sources_images_dir, 'gray_cross.png'))) self.button_cross.clicked.connect(self.reset_search_bar) search_bar_layout = QHBoxLayout() search_bar_layout.addWidget(self.rapid_search) search_bar_layout.addWidget(self.button_cross) push_button_tags = QPushButton("Visualized tags") push_button_tags.clicked.connect(self.update_tags) self.push_button_tag_filter.clicked.connect(self.update_tag_to_filter) push_button_ok = QPushButton("OK") push_button_ok.clicked.connect(self.ok_clicked) push_button_cancel = QPushButton("Cancel") push_button_cancel.clicked.connect(self.close) # Layout buttons_layout = QHBoxLayout() buttons_layout.addWidget(push_button_tags) buttons_layout.addWidget(self.push_button_tag_filter) buttons_layout.addStretch(1) buttons_layout.addWidget(push_button_ok) buttons_layout.addWidget(push_button_cancel) main_layout = QVBoxLayout() main_layout.addLayout(search_bar_layout) main_layout.addWidget(self.advanced_search) main_layout.addWidget(self.table_data) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) screen_resolution = QApplication.instance().desktop().screenGeometry() width, height = screen_resolution.width(), screen_resolution.height() self.setMinimumWidth(0.6 * width) self.setMinimumHeight(0.8 * height)
def _clean_up(): global main_window config = Config() opened_projects = config.get_opened_projects() try: opened_projects.remove(main_window.project.folder) config.set_opened_projects(opened_projects) main_window.remove_raw_files_useless() except AttributeError as e: opened_projects = [] config.set_opened_projects(opened_projects) print("\nClean up before closing mia done ...\n")
def create_view_window(self): """Create the main window view.""" sources_images_dir = Config().getSourceImageDir() app_icon = QIcon(os.path.join(sources_images_dir, 'brain_mri.jpeg')) self.setWindowIcon(app_icon) background_color = self.config.getBackgroundColor() text_color = self.config.getTextColor() if self.config.dev_mode: self.windowName += " (Developer mode)" self.windowName += " - " self.setStyleSheet("background-color:" + background_color + ";color:" + text_color + ";") self.statusBar().showMessage('Please create a new project (Ctrl+N) or ' 'open an existing project (Ctrl+O)') self.setWindowTitle(self.windowName + self.projectName)
def fill_last_tag(self): """ Fills the cells corresponding to the last selected tag """ # Cells of the last tag for col in range(self.idx_last_tag + 1, self.nb_col): nb_scans_ok = 0 # Creating a tag_list that will contain # couples tag_name/tag_value that # will then querying the Database for row in range(self.nb_row): tag_list = [] for idx_first_columns in range(self.idx_last_tag + 1): tag_name = self.table.horizontalHeaderItem( idx_first_columns).text() tag_type = self.project.session.get_field( COLLECTION_CURRENT, tag_name).field_type value_str = self.table.item(row, idx_first_columns).data( Qt.EditRole) value_database = table_to_database(value_str, tag_type) tag_list.append([tag_name, value_database]) tag_last_columns = self.push_buttons[-1].text() tag_last_columns_type = self.project.session.get_field( COLLECTION_CURRENT, tag_last_columns).field_type value_last_columns_str = self.table.horizontalHeaderItem( col).data(Qt.EditRole) value_last_columns_database = table_to_database( value_last_columns_str, tag_last_columns_type) tag_list.append( [tag_last_columns, value_last_columns_database]) item = QTableWidgetItem() item.setFlags(QtCore.Qt.ItemIsEnabled) # Getting the list of the scans that corresponds to the couples # tag_name/tag_values generator_scans = self.project.session.filter_documents( COLLECTION_CURRENT, self.prepare_filter(tag_list)) # List of scans created, given the generator list_scans = [ getattr(scan, TAG_FILENAME) for scan in generator_scans ] sources_images_dir = Config().getSourceImageDir() if list_scans: icon = QIcon( os.path.join(sources_images_dir, 'green_v.png')) length = len(list_scans) nb_scans_ok += length text = str(length) item.setText(text) tool_tip = '' # Setting as tooltip all the corresponding scans for tpl in list_scans: tool_tip += (tpl + '\n') tool_tip = tool_tip[:-1] item.setToolTip(tool_tip) else: icon = QIcon( os.path.join(sources_images_dir, 'red_cross.png')) item.setIcon(icon) self.table.setItem(row, col, item) item = QTableWidgetItem() item.setText(str(nb_scans_ok)) item.setFont(self.font) self.table.setItem(self.nb_row, col, item)
def __init__(self, project): """ Initialization of the Count Table :param project: current project in the software """ super().__init__() self.project = project # Issue #147: Allow to interact with database while the popup is open # self.setModal(True) self.setWindowTitle("Count table") # Font self.font = QFont() self.font.setBold(True) # values_list will contain the different values of each selected tag self.values_list = [[], []] self.label_tags = QLabel('Tags: ') # Each push button will allow the user to add a tag to the count table push_button_tag_1 = QPushButton() push_button_tag_1.setText("Tag n°1") push_button_tag_1.clicked.connect(lambda: self.select_tag(0)) push_button_tag_2 = QPushButton() push_button_tag_2.setText("Tag n°2") push_button_tag_2.clicked.connect(lambda: self.select_tag(1)) # The list of all the push buttons # (the user can add as many as he or she wants) self.push_buttons = [] self.push_buttons.insert(0, push_button_tag_1) self.push_buttons.insert(1, push_button_tag_2) # Labels to add/remove a tag (a push button) sources_images_dir = Config().getSourceImageDir() self.remove_tag_label = ClickableLabel() remove_tag_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "red_minus.png"))) remove_tag_picture = remove_tag_picture.scaledToHeight(20) self.remove_tag_label.setPixmap(remove_tag_picture) self.remove_tag_label.clicked.connect(self.remove_tag) self.add_tag_label = ClickableLabel() self.add_tag_label.setObjectName('plus') add_tag_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "green_plus.png"))) add_tag_picture = add_tag_picture.scaledToHeight(15) self.add_tag_label.setPixmap(add_tag_picture) self.add_tag_label.clicked.connect(self.add_tag) # Push button that is pressed to launch the computations self.push_button_count = QPushButton() self.push_button_count.setText('Count scans') self.push_button_count.clicked.connect(self.count_scans) # The table that will be filled self.table = QTableWidget() # Layouts self.v_box_final = QVBoxLayout() self.setLayout(self.v_box_final) self.refresh_layout()
def preferences(self): '''Preferences for the dataviewer ''' #Get initial config: im_sec = Config().getViewerFramerate() config = Config().getViewerConfig() ref = Config().get_referential() dialog = Qt.QDialog() dialog.setWindowTitle('Preferences') dialog.resize(600, 400) layout = Qt.QVBoxLayout() layout.setContentsMargins(25, 25, 25, 25) dialog.setLayout(layout) #Change Neuro/Radio configuration config_layout = QHBoxLayout() title_config = Qt.QLabel() title_config.setText('Configuration: ') box = Qt.QComboBox() box.addItem('Neuro') box.addItem('Radio') config_layout.addWidget(title_config) config_layout.addWidget(box) if config == 'radio': box.setCurrentIndex(1) #set automatic time frame rate frame_rate_layout = QHBoxLayout() title = Qt.QLabel() title.setText('Automatic time image display:') slider = Qt.QSlider(Qt.Qt.Horizontal) slider.setRange(1, 100) slider.setValue(int(im_sec)) size = QtCore.QSize(180, 15) slider.setMinimumSize(size) slow_label = Qt.QLabel() fast_label = Qt.QLabel() slow_label.setText('slow') fast_label.setText('fast') frame_rate_layout.addWidget(title) frame_rate_layout.addWidget(slow_label) frame_rate_layout.addWidget(slider) frame_rate_layout.addWidget(fast_label) frame_rate_layout.insertSpacing(1, 200) #Change referential ref_layout = QHBoxLayout() title_ref = Qt.QLabel() title_ref.setText('Referential: ') box2 = Qt.QComboBox() box2.addItem('World Coordinates') box2.addItem('Image referential') ref_layout.addWidget(title_ref) ref_layout.addWidget(box2) box2.setCurrentIndex(int(ref)) #Set general vertical layout layout.addLayout(config_layout) layout.addLayout(frame_rate_layout) layout.addLayout(ref_layout) layout.addStretch(1) #Save and cancel buttons hlay = Qt.QHBoxLayout() layout.addLayout(hlay) ok = Qt.QPushButton('Save') hlay.addStretch(1) hlay.addWidget(ok) ok.clicked.connect(dialog.accept) ok.setDefault(True) cancel = Qt.QPushButton('Cancel') hlay.addWidget(cancel) cancel.clicked.connect(dialog.reject) hlay.addStretch(1) res = dialog.exec_() if res == Qt.QDialog.Accepted: new_config = box.currentText().lower() new_ref = box2.currentIndex() #Save Config parameters and reload images #when config and referential have changed Config().setViewerFramerate(slider.value()) Config().setViewerConfig(new_config) Config().set_referential(new_ref) if new_config != config: self.anaviewer.changeConfig(new_config) if new_ref != ref: self.anaviewer.changeRef()
def __init__(self, project): """Initialise the MiniViewer object :param project: current project in the software """ super().__init__() self.project = project self.first_time = True # The MiniViewer is set hidden to give more space to the data_browser self.setHidden(True) # When multiple selection, limiting the number of thumbnails to # max_scans self.max_scans = 4 # Config that allows to read the software preferences self.config = Config() # Initializing some components of the MiniViewer self.labels = QWidget() self.frame = QFrame() self.scroll_area = QScrollArea() self.scroll_area.setWidget(self.frame) self.frame_final = QFrame() self.label_nb_slices = QLabel() self.label_nb_slices.setText("Maximum number of slices: ") self.line_edit_nb_slices = QLineEdit() self.line_edit_nb_slices.setText(str(self.config.getNbAllSlicesMax())) self.line_edit_nb_slices.returnPressed.connect(self.update_nb_slices) # All these objects are depending on the number of scans to visualize self.im_2D = [] self.slider_3D = [] self.slider_4D = [] self.slider_5D = [] self.txt_slider_3D = [] self.txt_slider_4D = [] self.txt_slider_5D = [] self.label3D = [] self.label4D = [] self.label5D = [] self.imageLabel = [] self.img = [] self.label_description = [] # Layouts self.createLayouts() self.setLayout(self.v_box_final) # Checkboxes self.check_box_slices = QCheckBox('Show all slices (no cursors)') if self.config.getShowAllSlices() == True: self.check_box_slices.setCheckState(Qt.Checked) else: self.check_box_slices.setCheckState(Qt.Unchecked) self.check_box_slices.stateChanged.connect( self.update_visualization_method) self.check_box_cursors = QCheckBox('Chain cursors') self.check_box_cursors.setToolTip("Allows to connect all cursors " "when selecting multiple documents") if self.config.getChainCursors() == True: self.check_box_cursors.setCheckState(Qt.Checked) else: self.check_box_cursors.setCheckState(Qt.Unchecked) self.check_box_cursors.stateChanged.connect( self.check_box_cursors_state_changed) self.file_paths = ""
def import_data(self): """Call the import software (MRI File Manager), reads the imported files and loads them into the database. """ # Opens the conversion software to convert the MRI files in Nifti/Json config = Config() home = expanduser("~") code_exit = subprocess.call([ 'java', '-Xmx4096M', '-jar', config.get_mri_conv_path(), '[ProjectsDir] ' + home, '[ExportNifti] ' + os.path.join(self.project.folder, 'data', 'raw_data'), '[ExportToMIA] PatientName-StudyName-' 'CreationDate-SeqNumber-Protocol-' 'SequenceName-AcquisitionTime', 'CloseAfterExport' ]) # 'NoLogExport'if we don't want log export if code_exit == 0: # Database filled new_scans = data_loader.read_log(self.project, self) # Table updated documents = self.project.session.get_documents_names( COLLECTION_CURRENT) self.data_browser.table_data.scans_to_visualize = documents self.data_browser.table_data.scans_to_search = documents self.data_browser.table_data.add_columns() self.data_browser.table_data.fill_headers() self.data_browser.table_data.add_rows(new_scans) self.data_browser.reset_search_bar() self.data_browser.frame_advanced_search.setHidden(True) self.data_browser.advanced_search.rows = [] elif code_exit == 100: # User only close mri_conv and do nothing pass else: print( "\nmri_conv, did not work properly. Current absolute path to " "MRIManager.jar defined in File > MIA Preferences:\n{0}\n". format(config.get_mri_conv_path())) if not os.path.isfile(config.get_mri_conv_path()): mssgText = ("Warning: mri_conv did not work properly. The " "current absolute path to MRIManager.jar doesn't " "seem to be correctly defined.\nCurrent absolute " "path to MRIManager.jar defined in\nFile > MIA " "Preferences:\n{0}".format( config.get_mri_conv_path())) else: mssgText = ("Warning : mri_conv did not work properly. Please " "check if the currently installed mri_conv Java " "ARchive is not corrupted.\nCurrent absolute path " "to MRIManager.jar defined in\nFile > MIA " "Preferences:\n{0}".format( config.get_mri_conv_path())) msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("populse_mia - Warning: " "Data import issue!") msg.setText(mssgText) msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec()
def filter_documents(self): '''Filter documents already loaded in the Databrowser ''' dialog = Qt.QDialog() dialog.setWindowTitle('Filter documents') dialog.resize(1150, 500) layout = Qt.QVBoxLayout() dialog.setLayout(layout) #Some specific filtering #QLineEdit for research self.search_bar = RapidSearch(dialog) self.search_bar.textChanged.connect(self.search_str) #Cancel search button sources_images_dir = Config().getSourceImageDir() button_cross = QToolButton() button_cross.setStyleSheet('background-color:rgb(255, 255, 255);') button_cross.setIcon( QIcon(os.path.join(sources_images_dir, 'gray_cross.png'))) button_cross.clicked.connect(self.reset_search_bar) title = Qt.QLabel() title.setText('Search by FileName: ') layout.addWidget(title) search_bar_layout = QHBoxLayout() search_bar_layout.addWidget(self.search_bar) search_bar_layout.addSpacing(3) search_bar_layout.addWidget(button_cross) #Add layout to dialogBox layout.addLayout(search_bar_layout) layout.addSpacing(8) self.table_data = TableDataBrowser( self.project, self, self.project.session.get_shown_tags(), False, True, link_viewer=False) layout.addWidget(self.table_data) hlay = Qt.QHBoxLayout() layout.addLayout(hlay) ok = Qt.QPushButton('Import') hlay.addWidget(ok) ok.clicked.connect(dialog.accept) ok.setDefault(True) cancel = Qt.QPushButton('Cancel') hlay.addWidget(cancel) cancel.clicked.connect(dialog.reject) hlay.addStretch(1) # Reducing the list of scans to selection all_scans = self.table_data.scans_to_visualize self.table_data.scans_to_visualize = self.documents self.table_data.scans_to_search = self.documents self.table_data.update_visualized_rows(all_scans) res = dialog.exec_() if res == Qt.QDialog.Accepted: points = self.table_data.selectedIndexes() result_names = [] for point in points: row = point.row() # We get the FileName of the scan from the first row scan_name = self.table_data.item(row, 0).text() value = self.project.session.get_value(COLLECTION_CURRENT, scan_name, TAG_FILENAME) value = os.path.abspath( os.path.join(self.project.folder, value)) result_names.append(value) self.display_files(result_names)
def add_search_bar(self): """Create and define the advanced research bar.""" row_layout = [] # NOT choice not_choice = QComboBox() not_choice.setObjectName('not') not_choice.addItem("") not_choice.addItem("NOT") # Field choice field_choice = QComboBox() field_choice.setObjectName('field') if len(self.tags_list) > 0: for tag in self.tags_list: field_choice.addItem(tag) else: for tag in self.project.session.get_shown_tags(): field_choice.addItem(tag) field_choice.model().sort(0) field_choice.addItem("All visualized tags") # Value choice condition_value = QLineEdit() condition_value.setObjectName('value') # Condition choice condition_choice = QComboBox() condition_choice.setObjectName('condition') condition_choice.addItem("==") condition_choice.addItem("!=") condition_choice.addItem(">=") condition_choice.addItem("<=") condition_choice.addItem(">") condition_choice.addItem("<") condition_choice.addItem("BETWEEN") condition_choice.addItem("IN") condition_choice.addItem("CONTAINS") condition_choice.addItem("HAS VALUE") condition_choice.addItem("HAS NO VALUE") condition_choice.model().sort(0) # Signal to update the placeholder text of the value condition_choice.currentTextChanged.connect( lambda: self.displayValueRules(condition_choice, condition_value)) # Signal to update the list of conditions, depending on the tag type field_choice.currentTextChanged.connect( lambda: self.displayConditionRules(field_choice, condition_choice)) # Minus to remove the row sources_images_dir = Config().getSourceImageDir() remove_row_label = ClickableLabel() remove_row_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "red_minus.png"))) remove_row_picture = remove_row_picture.scaledToHeight(30) remove_row_label.setPixmap(remove_row_picture) # Everything appended to the row row_layout.append(None) # Link room row_layout.append(not_choice) row_layout.append(field_choice) row_layout.append(condition_choice) row_layout.append(condition_value) row_layout.append(remove_row_label) row_layout.append(None) # Add row room # Signal to remove the row remove_row_label.clicked.connect(lambda: self.remove_row(row_layout)) self.rows.append(row_layout) self.refresh_search() self.displayConditionRules(field_choice, condition_choice)
def switch_project(self, file_path, name): """Check if it's possible to open the selected project and quit the current one. :param file_path: raw file_path :param name: project name :return: Boolean """ # /!\ file_path and path are the same param # Switching project only if it's a different one if file_path != self.project.folder: # If the file exists if os.path.exists(os.path.join(file_path)): # If it is a MIA project if os.path.exists(os.path.join( file_path, "properties", "properties.yml")) \ and os.path.exists(os.path.join( file_path, "database", "mia.db")) \ and os.path.exists(os.path.join( file_path, "data", "raw_data")) \ and os.path.exists(os.path.join( file_path, "data", "derived_data")) \ and os.path.exists(os.path.join( file_path, "data", "downloaded_data")) \ and os.path.exists(os.path.join( file_path, "filters")): # We check for invalid scans in the project try: temp_database = Project(file_path, False) except IOError: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("project already opened") msg.setInformativeText("The project at " + str(file_path) + " is already opened in another " "instance of the software.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False problem_list = data_loader.verify_scans(temp_database) # Message if invalid files if problem_list: str_msg = "" for element in problem_list: str_msg += element + "\n\n" msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText( "These files have been modified or removed since " "they have been converted for the first time:") msg.setInformativeText(str_msg) msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() self.project.session.unsave_modifications() self.remove_raw_files_useless() # We remove the useless files from the old project # project removed from the opened projects list config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.project = temp_database # New Database self.update_project(file_path) # project updated everywhere return True # Not a MIA project else: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("The project selected isn't a " "valid MIA project") msg.setInformativeText("The project selected " + name + " isn't a MIA project" ".\nPlease select a " "valid one.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False # The project doesn't exist anymore else: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("The project selected doesn't exist anymore") msg.setInformativeText("The project selected " + name + " doesn't exist anymore." "\nPlease select " "another one.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False
def save_project_as(self): """Open a pop-up to save the current project as""" self.exPopup = PopUpSaveProjectAs() if self.exPopup.exec_(): old_folder = self.project.folder file_name = self.exPopup.relative_path database_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'database') properties_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'properties') filters_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'filters') data_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'data') raw_data_path = os.path.join(data_path, 'raw_data') derived_data_path = os.path.join(data_path, 'derived_data') downloaded_data_path = os.path.join(data_path, 'downloaded_data') # List of projects updated if not self.test: self.saved_projects_list = self.saved_projects.addSavedProject( file_name) self.update_recent_projects_actions() os.makedirs(self.exPopup.relative_path) os.mkdir(data_path) os.mkdir(raw_data_path) os.mkdir(derived_data_path) os.mkdir(downloaded_data_path) os.mkdir(filters_path) # Data files copied if os.path.exists(os.path.join(old_folder, 'data')): for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'raw_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'raw_data')) for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'derived_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'derived_data')) for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'downloaded_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'downloaded_data')) if os.path.exists(os.path.join(old_folder, 'filters')): for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'filters', '*')): shutil.copy(filename, os.path.join(os.path.relpath(filters_path))) # First we register the Database before commiting the last # pending modifications shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db'), os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db')) # We commit the last pending modifications self.project.saveModifications() os.mkdir(properties_path) shutil.copy( os.path.join(os.path.relpath(old_folder), 'properties', 'properties.yml'), os.path.relpath(properties_path)) # We copy the Database with all the modifications commited in # the new project os.mkdir(os.path.relpath(database_path)) shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db'), os.path.relpath(database_path)) # We remove the Database with all the modifications saved in # the old project os.remove( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db')) # We reput the Database without the last modifications # in the old project shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db'), os.path.join(os.path.relpath(old_folder), 'database', 'mia.db')) os.remove( os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db')) self.remove_raw_files_useless() # We remove the useless files from the old project # Removing the old project from the list of # currently opened projects config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) # project updated everywhere self.project = Project(self.exPopup.relative_path, False) self.project.setName(os.path.basename(self.exPopup.relative_path)) self.project.setDate(datetime.now().strftime('%d/%m/%Y %H:%M:%S')) self.project.saveModifications() self.update_project(file_name, call_update_table=False) # project updated everywhere # If some files have been set in the pipeline editors, # display a warning message if self.pipeline_manager.pipelineEditorTabs.has_pipeline_nodes(): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("This action moves the current database. " "All pipelines will need to be initialized " "again before they can run.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec()
class MainWindow(QMainWindow): """Initialize software appearance and define interactions with the user. .. Methods: - __init__ : initialise the object MainWindow - add_clinical_tags: add the clinical tags to the database and the data browser - check_unsaved_modifications: check if there are differences between the current project and the database - closeEvent: override the closing event to check if there are unsaved modifications - create_view_actions: create the actions in each menu - create_view_menus: create the menu-bar - create_view_window: create the main window view - create_project_pop_up: create a new project - create_tabs: create the tabs - documentation: open the documentation in a web browser - import_data: call the import software (MRI File Manager) - install_processes_pop_up: open the install processes pop-up - open_project_pop_up: open a pop-up to open a project and updates the recent projects - open_recent_project: open a recent project - package_library_pop_up: open the package library pop-up - project_properties_pop_up: open the project properties pop-up - redo: redo the last action made by the user - remove_raw_files_useless: remove the useless raw files of the current project - save: save either the current project or the current pipeline - save_as: save either the current project or the current pipeline under a new name - save_project_as: open a pop-up to save the current project as - saveChoice: checks if the project needs to be saved as or just saved - see_all_projects: open a pop-up to show the recent projects - software_preferences_pop_up: open the MIA2 preferences pop-up - switch_project: switches project if it's possible - tab_changed: method called when the tab is changed - undo: undoes the last action made by the user - update_package_library_action: update the package library action depending on the mode - update_project: update the project once the database has been updated - update_recent_projects_actions: update the list of recent projects """ def __init__(self, project, test=False, deleted_projects=None): """Main window class, initializes the software appearance and defines interactions with the user. :Parameter: - :project: current project in the software - :test: boolean if the widget is launched from unit tests or not - :deleted_projects: projects that have been deleted """ super(MainWindow, self).__init__() QApplication.restoreOverrideCursor() # We associate these methods and the instance to be able to call them # from anywhere. QCoreApplication.instance().title = self.windowTitle QCoreApplication.instance().set_title = self.setWindowTitle if deleted_projects is not None and deleted_projects: self.msg = PopUpDeletedProject(deleted_projects) self.config = Config() self.config.setSourceImageDir( os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "sources_images")) self.windowName = "MIA - Multiparametric Image Analysis" self.projectName = "Unnamed project" self.project = project self.test = test self.saved_projects = SavedProjects() self.saved_projects_list = self.saved_projects.pathsList self.saved_projects_actions = [] # Define main window view self.create_view_window() # Initialize menu self.menu_file = self.menuBar().addMenu('File') self.menu_edition = self.menuBar().addMenu('Edit') self.menu_help = self.menuBar().addMenu('Help') self.menu_about = self.menuBar().addMenu('About') self.menu_more = self.menuBar().addMenu('More') self.menu_install_process = QMenu('Install processes', self) self.menu_saved_projects = QMenu('Saved projects', self) # Initialize tabs self.tabs = QTabWidget() self.data_browser = DataBrowser(self.project, self) self.image_viewer = QLabel("Coming soon...") self.pipeline_manager = PipelineManagerTab(self.project, [], self) self.centralWindow = QWidget() # Initialize menu actions sources_images_dir = Config().getSourceImageDir() self.action_save_project = self.menu_file.addAction("Save project") self.action_save_project_as = self.menu_file.addAction("Save " "project as") self.action_create = QAction('New project', self) self.action_open = QAction('Open project', self) self.action_save = QAction('Save', self) self.action_save_as = QAction('Save as', self) self.action_import = QAction( QIcon(os.path.join(sources_images_dir, 'Blue.png')), 'Import', self) self.action_see_all_projects = QAction('See all projects', self) self.action_project_properties = QAction('Project properties', self) self.action_software_preferences = QAction('MIA preferences', self) self.action_package_library = QAction('Package library manager', self) self.action_exit = QAction( QIcon(os.path.join(sources_images_dir, 'exit.png')), 'Exit', self) self.action_undo = QAction('Undo', self) self.action_redo = QAction('Redo', self) self.action_documentation = QAction('Documentation', self) self.action_install_processes_folder = QAction('From folder', self) self.action_install_processes_zip = QAction('From zip file', self) # Connect actions & menus views self.create_view_actions() self.create_view_menus() # Create Tabs self.create_tabs() self.setCentralWidget(self.centralWindow) self.showMaximized() def add_clinical_tags(self): """Add the clinical tags to the database and the data browser""" added_tags = self.project.add_clinical_tags() for tag in added_tags: column = self.data_browser.table_data.get_index_insertion(tag) self.data_browser.table_data.add_column(column, tag) def check_unsaved_modifications(self): """Check if there are differences between the current project and the database. :return: Boolean. True if there are unsaved modifications, False otherwise """ if self.project.isTempProject: if len(self.project.session.get_documents_names( COLLECTION_CURRENT)) > 0: return True else: return False elif self.project.hasUnsavedModifications(): return True else: return False def closeEvent(self, event): """Override the QWidget closing event to check if there are unsaved modifications :param event: closing event """ if self.check_unsaved_modifications() == False or self.test: can_exit = True else: self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_exit = self.pop_up_close.can_exit() if can_exit: self.project.unsaveModifications() # Clean up config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.remove_raw_files_useless() event.accept() else: event.ignore() def create_view_actions(self): """Create the actions and their shortcuts in each menu""" self.action_create.setShortcut('Ctrl+N') self.action_open.setShortcut('Ctrl+O') self.action_save.setShortcut('Ctrl+S') self.addAction(self.action_save) self.action_save_as.setShortcut('Ctrl+Shift+S') self.addAction(self.action_save_as) self.action_import.setShortcut('Ctrl+I') for i in range(self.config.get_max_projects()): self.saved_projects_actions.append( QAction(self, visible=False, triggered=self.open_recent_project)) if Config().get_clinical_mode() == True: self.action_package_library.setDisabled(True) else: self.action_package_library.setEnabled(True) self.action_exit.setShortcut('Ctrl+W') self.action_undo.setShortcut('Ctrl+Z') self.action_redo.setShortcut('Ctrl+Y') # if Config().get_clinical_mode() == True: # self.action_install_processes.setDisabled(True) # else: # self.action_install_processes.setEnabled(True) # Connection of the several triggered signals of the actions to some # other methods self.action_create.triggered.connect(self.create_project_pop_up) self.action_open.triggered.connect(self.open_project_pop_up) self.action_exit.triggered.connect(self.close) self.action_save.triggered.connect(self.save) self.action_save_as.triggered.connect(self.save_as) self.action_import.triggered.connect(self.import_data) self.action_see_all_projects.triggered.connect(self.see_all_projects) self.action_project_properties.triggered.connect( self.project_properties_pop_up) self.action_software_preferences.triggered.connect( self.software_preferences_pop_up) self.action_package_library.triggered.connect( self.package_library_pop_up) self.action_undo.triggered.connect(self.undo) self.action_redo.triggered.connect(self.redo) self.action_documentation.triggered.connect(self.documentation) self.action_install_processes_folder.triggered.connect( lambda: self.install_processes_pop_up(folder=True)) self.action_install_processes_zip.triggered.connect( lambda: self.install_processes_pop_up(folder=False)) def create_view_menus(self): """Create the menu-bar view.""" self.menu_more.addMenu(self.menu_install_process) # Actions in the "File" menu self.menu_file.addAction(self.action_create) self.menu_file.addAction(self.action_open) self.action_save_project.triggered.connect(self.saveChoice) self.action_save_project_as.triggered.connect(self.save_project_as) self.menu_file.addSeparator() self.menu_file.addAction(self.action_import) self.menu_file.addSeparator() self.menu_file.addMenu(self.menu_saved_projects) for i in range(self.config.get_max_projects()): self.menu_saved_projects.addAction(self.saved_projects_actions[i]) self.menu_saved_projects.addSeparator() self.menu_saved_projects.addAction(self.action_see_all_projects) self.menu_file.addSeparator() self.menu_file.addAction(self.action_software_preferences) self.menu_file.addAction(self.action_project_properties) self.menu_file.addAction(self.action_package_library) self.menu_file.addSeparator() self.menu_file.addAction(self.action_exit) self.update_recent_projects_actions() # Actions in the "Edition" menu self.menu_edition.addAction(self.action_undo) self.menu_edition.addAction(self.action_redo) # Actions in the "Help" menu self.menu_help.addAction(self.action_documentation) self.menu_help.addAction('Credits') # Actions in the "More > Install processes" menu self.menu_install_process.addAction( self.action_install_processes_folder) self.menu_install_process.addAction(self.action_install_processes_zip) def create_view_window(self): """Create the main window view.""" sources_images_dir = Config().getSourceImageDir() app_icon = QIcon(os.path.join(sources_images_dir, 'brain_mri.jpeg')) self.setWindowIcon(app_icon) background_color = self.config.getBackgroundColor() text_color = self.config.getTextColor() if self.config.dev_mode: self.windowName += " (Developer mode)" self.windowName += " - " self.setStyleSheet("background-color:" + background_color + ";color:" + text_color + ";") self.statusBar().showMessage('Please create a new project (Ctrl+N) or ' 'open an existing project (Ctrl+O)') self.setWindowTitle(self.windowName + self.projectName) def create_project_pop_up(self): """Create a new project.""" if self.check_unsaved_modifications(): self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_switch = self.pop_up_close.can_exit() else: can_switch = True if can_switch: # Opens a pop-up when the 'New project' action is clicked and # updates the recent projects self.exPopup = PopUpNewProject() if self.exPopup.exec_(): self.project.session.unsave_modifications() self.remove_raw_files_useless() # We remove the useless # files from the old project file_name = self.exPopup.selectedFiles() self.exPopup.get_filename(self.exPopup.selectedFiles()) file_name = self.exPopup.relative_path # Removing the old project from the list of currently opened # projects config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.project = Project(self.exPopup.relative_path, True) self.update_project(file_name) # project updated everywhere def create_tabs(self): """Create the tabs and initializes the DataBrowser and PipelineManager classes.""" self.config = Config() self.tabs.setAutoFillBackground(False) self.tabs.setStyleSheet('QTabBar{font-size:16pt;text-align: center}') self.tabs.setMovable(True) self.tabs.addTab(self.data_browser, "Data Browser") # To uncomment when the Data Viewer will be created # self.image_viewer = ImageViewer() self.tabs.addTab(self.image_viewer, "Data Viewer") self.tabs.addTab(self.pipeline_manager, "Pipeline Manager") self.tabs.currentChanged.connect(self.tab_changed) vertical_layout = QVBoxLayout() vertical_layout.addWidget(self.tabs) self.centralWindow.setLayout(vertical_layout) @staticmethod def documentation(): """Open the documentation in a web browser.""" webbrowser.open( 'https://populse.github.io/populse_mia/html/index.html') def install_processes_pop_up(self, folder=False): """Open the install processes pop-up. :param folder: boolean, True if installing from a folder """ self.pop_up_install_processes = InstallProcesses(self, folder=folder) self.pop_up_install_processes.show() self.pop_up_install_processes.process_installed.connect( self.pipeline_manager.processLibrary.update_process_library) self.pop_up_install_processes.process_installed.connect( self.pipeline_manager.processLibrary.pkg_library.update_config) def import_data(self): """Call the import software (MRI File Manager), reads the imported files and loads them into the database. """ # Opens the conversion software to convert the MRI files in Nifti/Json config = Config() home = expanduser("~") code_exit = subprocess.call([ 'java', '-Xmx4096M', '-jar', config.get_mri_conv_path(), '[ProjectsDir] ' + home, '[ExportNifti] ' + os.path.join(self.project.folder, 'data', 'raw_data'), '[ExportToMIA] PatientName-StudyName-' 'CreationDate-SeqNumber-Protocol-' 'SequenceName-AcquisitionTime', 'CloseAfterExport' ]) # 'NoLogExport'if we don't want log export if code_exit == 0: # Database filled new_scans = data_loader.read_log(self.project, self) # Table updated documents = self.project.session.get_documents_names( COLLECTION_CURRENT) self.data_browser.table_data.scans_to_visualize = documents self.data_browser.table_data.scans_to_search = documents self.data_browser.table_data.add_columns() self.data_browser.table_data.fill_headers() self.data_browser.table_data.add_rows(new_scans) self.data_browser.reset_search_bar() self.data_browser.frame_advanced_search.setHidden(True) self.data_browser.advanced_search.rows = [] elif code_exit == 100: # User only close mri_conv and do nothing pass else: print( "\nmri_conv, did not work properly. Current absolute path to " "MRIManager.jar defined in File > MIA Preferences:\n{0}\n". format(config.get_mri_conv_path())) if not os.path.isfile(config.get_mri_conv_path()): mssgText = ("Warning: mri_conv did not work properly. The " "current absolute path to MRIManager.jar doesn't " "seem to be correctly defined.\nCurrent absolute " "path to MRIManager.jar defined in\nFile > MIA " "Preferences:\n{0}".format( config.get_mri_conv_path())) else: mssgText = ("Warning : mri_conv did not work properly. Please " "check if the currently installed mri_conv Java " "ARchive is not corrupted.\nCurrent absolute path " "to MRIManager.jar defined in\nFile > MIA " "Preferences:\n{0}".format( config.get_mri_conv_path())) msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("populse_mia - Warning: " "Data import issue!") msg.setText(mssgText) msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() def open_project_pop_up(self): """Open a pop-up to open a project and updates the recent projects.""" # Ui_Dialog() is defined in pop_ups.py # We check for unsaved modifications if self.check_unsaved_modifications(): # If there are unsaved modifications, we ask the user what he # wants to do self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_switch = self.pop_up_close.can_exit() else: can_switch = True # We can open a new project if can_switch: self.exPopup = PopUpOpenProject() if self.exPopup.exec_(): file_name = self.exPopup.selectedFiles() self.exPopup.get_filename(file_name) file_name = self.exPopup.relative_path self.switch_project(file_name, self.exPopup.name) # We switch the project def open_recent_project(self): """Open a recent project.""" # We check for unsaved modifications if self.check_unsaved_modifications(): # If there are unsaved modifications, we ask the user what he # wants to do self.pop_up_close = PopUpQuit(self.project) self.pop_up_close.save_as_signal.connect(self.saveChoice) self.pop_up_close.exec() can_switch = self.pop_up_close.can_exit() else: can_switch = True # We can open a new project if can_switch: action = self.sender() if action: file_name = action.data() entire_path = os.path.abspath(file_name) path, name = os.path.split(entire_path) relative_path = os.path.relpath(file_name) self.switch_project(relative_path, name) # We switch the project def package_library_pop_up(self): """Open the package library pop-up""" self.pop_up_package_library = PackageLibraryDialog(self) self.pop_up_package_library.setGeometry(300, 200, 800, 600) self.pop_up_package_library.show() self.pop_up_package_library.signal_save.connect( self.pipeline_manager.processLibrary.update_process_library) def project_properties_pop_up(self): """Open the project properties pop-up""" old_tags = self.project.session.get_shown_tags() self.pop_up_settings = PopUpProperties(self.project, self.data_browser, old_tags) self.pop_up_settings.setGeometry(300, 200, 800, 600) self.pop_up_settings.show() if self.pop_up_settings.exec_(): self.data_browser.table_data.update_visualized_columns( old_tags, self.project.session.get_shown_tags()) def redo(self): """Redo the last action made by the user.""" if self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Data Browser': # In Data Browser self.project.redo(self.data_browser.table_data) # Action remade in the Database elif self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Pipeline Manager': # In Pipeline Manager self.pipeline_manager.redo() def remove_raw_files_useless(self): """Remove the useless raw files of the current project""" # If it's unnamed project, we can remove the whole project if self.project.isTempProject: self.project.database.__exit__(None, None, None) shutil.rmtree(self.project.folder) else: for filename in glob.glob( os.path.join(os.path.relpath(self.project.folder), 'data', 'raw_data', '*')): scan = os.path.basename(filename) # The file is removed only if it's not a scan in the project, # and if it's not a logExport # Json files associated to nii files are kept for the raw_ # data folder file_name, file_extension = os.path.splitext(scan) file_in_database = False for database_scan in self.project.session.get_documents_names( COLLECTION_CURRENT): if file_name in database_scan: file_in_database = True if "logExport" in scan: file_in_database = True if not file_in_database: os.remove(filename) for filename in glob.glob( os.path.join(os.path.relpath(self.project.folder), 'data', 'derived_data', '*')): scan = os.path.basename(filename) # The file is removed only if it's not a scan in the project, # and if it's not a logExport if (self.project.session.get_document( COLLECTION_CURRENT, os.path.join("data", "derived_data", scan)) is None and "logExport" not in scan): os.remove(filename) for filename in glob.glob( os.path.join(os.path.relpath(self.project.folder), 'data', 'downloaded_data', '*')): scan = os.path.basename(filename) # The file is removed only if it's not a scan in the project, # and if it's not a logExport if (self.project.session.get_document( COLLECTION_CURRENT, os.path.join("data", "downloaded_data", scan)) is None and "logExport" not in scan): os.remove(filename) self.project.database.__exit__(None, None, None) def save(self): """Save either the current project or the current pipeline""" if self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Data Browser': # In Data Browser self.saveChoice() elif self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Pipeline Manager': # In Pipeline Manager self.pipeline_manager.savePipeline() def save_as(self): """Save either the current project or the current pipeline under a new name. """ if self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Data Browser': # In Data Browser self.save_project_as() elif self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Pipeline Manager': # In Pipeline Manager self.pipeline_manager.savePipelineAs() def save_project_as(self): """Open a pop-up to save the current project as""" self.exPopup = PopUpSaveProjectAs() if self.exPopup.exec_(): old_folder = self.project.folder file_name = self.exPopup.relative_path database_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'database') properties_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'properties') filters_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'filters') data_path = os.path.join( os.path.relpath(self.exPopup.relative_path), 'data') raw_data_path = os.path.join(data_path, 'raw_data') derived_data_path = os.path.join(data_path, 'derived_data') downloaded_data_path = os.path.join(data_path, 'downloaded_data') # List of projects updated if not self.test: self.saved_projects_list = self.saved_projects.addSavedProject( file_name) self.update_recent_projects_actions() os.makedirs(self.exPopup.relative_path) os.mkdir(data_path) os.mkdir(raw_data_path) os.mkdir(derived_data_path) os.mkdir(downloaded_data_path) os.mkdir(filters_path) # Data files copied if os.path.exists(os.path.join(old_folder, 'data')): for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'raw_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'raw_data')) for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'derived_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'derived_data')) for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'data', 'downloaded_data', '*')): shutil.copy( filename, os.path.join(os.path.relpath(data_path), 'downloaded_data')) if os.path.exists(os.path.join(old_folder, 'filters')): for filename in glob.glob( os.path.join(os.path.relpath(old_folder), 'filters', '*')): shutil.copy(filename, os.path.join(os.path.relpath(filters_path))) # First we register the Database before commiting the last # pending modifications shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db'), os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db')) # We commit the last pending modifications self.project.saveModifications() os.mkdir(properties_path) shutil.copy( os.path.join(os.path.relpath(old_folder), 'properties', 'properties.yml'), os.path.relpath(properties_path)) # We copy the Database with all the modifications commited in # the new project os.mkdir(os.path.relpath(database_path)) shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db'), os.path.relpath(database_path)) # We remove the Database with all the modifications saved in # the old project os.remove( os.path.join(os.path.relpath(old_folder), 'database', 'mia.db')) # We reput the Database without the last modifications # in the old project shutil.copy( os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db'), os.path.join(os.path.relpath(old_folder), 'database', 'mia.db')) os.remove( os.path.join(os.path.relpath(old_folder), 'database', 'mia_before_commit.db')) self.remove_raw_files_useless() # We remove the useless files from the old project # Removing the old project from the list of # currently opened projects config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) # project updated everywhere self.project = Project(self.exPopup.relative_path, False) self.project.setName(os.path.basename(self.exPopup.relative_path)) self.project.setDate(datetime.now().strftime('%d/%m/%Y %H:%M:%S')) self.project.saveModifications() self.update_project(file_name, call_update_table=False) # project updated everywhere # If some files have been set in the pipeline editors, # display a warning message if self.pipeline_manager.pipelineEditorTabs.has_pipeline_nodes(): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("This action moves the current database. " "All pipelines will need to be initialized " "again before they can run.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() def saveChoice(self): """Check if the project needs to be 'saved as' or just 'saved'.""" if self.project.isTempProject: self.save_project_as() else: self.project.saveModifications() def see_all_projects(self): """Open a pop-up to show the recent projects.""" # Ui_Dialog() is defined in pop_ups.py self.exPopup = PopUpSeeAllProjects(self.saved_projects, self) if self.exPopup.exec_(): file_path = self.exPopup.relative_path if not self.test: self.saved_projects_list = self.saved_projects.addSavedProject( file_path) self.update_recent_projects_actions() def software_preferences_pop_up(self): """Open the MIA2 preferences pop-up.""" self.pop_up_preferences = PopUpPreferences(self) self.pop_up_preferences.setGeometry(300, 200, 800, 600) self.pop_up_preferences.show() self.pop_up_preferences.use_clinical_mode_signal.connect( self.add_clinical_tags) # Modifying the options in the Pipeline Manager # (verify if clinical mode) self.pop_up_preferences.signal_preferences_change.connect( self.pipeline_manager.update_clinical_mode) self.pop_up_preferences.signal_preferences_change.connect( self.update_package_library_action) def switch_project(self, file_path, name): """Check if it's possible to open the selected project and quit the current one. :param file_path: raw file_path :param name: project name :return: Boolean """ # /!\ file_path and path are the same param # Switching project only if it's a different one if file_path != self.project.folder: # If the file exists if os.path.exists(os.path.join(file_path)): # If it is a MIA project if os.path.exists(os.path.join( file_path, "properties", "properties.yml")) \ and os.path.exists(os.path.join( file_path, "database", "mia.db")) \ and os.path.exists(os.path.join( file_path, "data", "raw_data")) \ and os.path.exists(os.path.join( file_path, "data", "derived_data")) \ and os.path.exists(os.path.join( file_path, "data", "downloaded_data")) \ and os.path.exists(os.path.join( file_path, "filters")): # We check for invalid scans in the project try: temp_database = Project(file_path, False) except IOError: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("project already opened") msg.setInformativeText("The project at " + str(file_path) + " is already opened in another " "instance of the software.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False problem_list = data_loader.verify_scans(temp_database) # Message if invalid files if problem_list: str_msg = "" for element in problem_list: str_msg += element + "\n\n" msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText( "These files have been modified or removed since " "they have been converted for the first time:") msg.setInformativeText(str_msg) msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() self.project.session.unsave_modifications() self.remove_raw_files_useless() # We remove the useless files from the old project # project removed from the opened projects list config = Config() opened_projects = config.get_opened_projects() opened_projects.remove(self.project.folder) config.set_opened_projects(opened_projects) self.project = temp_database # New Database self.update_project(file_path) # project updated everywhere return True # Not a MIA project else: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("The project selected isn't a " "valid MIA project") msg.setInformativeText("The project selected " + name + " isn't a MIA project" ".\nPlease select a " "valid one.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False # The project doesn't exist anymore else: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("The project selected doesn't exist anymore") msg.setInformativeText("The project selected " + name + " doesn't exist anymore." "\nPlease select " "another one.") msg.setWindowTitle("Warning") msg.setStandardButtons(QMessageBox.Ok) msg.buttonClicked.connect(msg.close) msg.exec() return False def tab_changed(self): """Update the window when the tab is changed""" if self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Data Browser': # data_browser refreshed after working with pipelines old_scans = self.data_browser.table_data.scans_to_visualize documents = self.project.session.get_documents_names( COLLECTION_CURRENT) self.data_browser.table_data.add_columns() self.data_browser.table_data.fill_headers() self.data_browser.table_data.add_rows(documents) self.data_browser.table_data.scans_to_visualize = documents self.data_browser.table_data.scans_to_search = documents self.data_browser.table_data.itemChanged.disconnect() self.data_browser.table_data.fill_cells_update_table() self.data_browser.table_data.itemChanged.connect( self.data_browser.table_data.change_cell_color) self.data_browser.table_data.update_visualized_rows(old_scans) # Advanced search + search_bar opened old_search = self.project.currentFilter.search_bar self.data_browser.reset_search_bar() self.data_browser.search_bar.setText(old_search) if len(self.project.currentFilter.nots) > 0: self.data_browser.frame_advanced_search.setHidden(False) self.data_browser.advanced_search.scans_list = \ self.data_browser.table_data.scans_to_visualize self.data_browser.advanced_search.show_search() self.data_browser.advanced_search.apply_filter( self.project.currentFilter) elif self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Pipeline Manager': # Pipeline Manager # The pending modifications must be saved before # working with pipelines (auto_commit) if self.project.hasUnsavedModifications(): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("Unsaved modifications in the Data Browser !") msg.setInformativeText( "There are unsaved modifications in the database, " "you need to save or remove them before working " "with pipelines.") msg.setWindowTitle("Warning") save_button = QPushButton("Save") save_button.clicked.connect(self.project.saveModifications) unsave_button = QPushButton("Not Save") unsave_button.clicked.connect(self.project.unsaveModifications) msg.addButton(save_button, QMessageBox.AcceptRole) msg.addButton(unsave_button, QMessageBox.AcceptRole) msg.exec() def undo(self): """Undo the last action made by the user.""" if self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Data Browser': # In Data Browser self.project.undo(self.data_browser.table_data) # Action reverted in the Database elif self.tabs.tabText(self.tabs.currentIndex()).replace( "&", "", 1) == 'Pipeline Manager': # In Pipeline Manager self.pipeline_manager.undo() def update_package_library_action(self): """Update the package library action depending on the mode.""" if Config().get_clinical_mode() == True: self.action_package_library.setDisabled(True) # self.action_install_processes.setDisabled(True) else: self.action_package_library.setEnabled(True) # self.action_install_processes.setEnabled(True) def update_project(self, file_path, call_update_table=True): """Update the project once the database has been updated. Update the database, the window title and the recent and saved projects menus. :param file_path: File name of the new project :param call_update_table: boolean, True if we need to call """ self.data_browser.update_database(self.project) # Database update data_browser self.pipeline_manager.update_project(self.project) if call_update_table: self.data_browser.table_data.update_table() # Table updated # Window name updated if self.project.isTempProject: self.projectName = 'Unnamed project' else: self.projectName = self.project.getName() self.setWindowTitle(self.windowName + self.projectName) # List of project updated if not self.test: self.saved_projects_list = self.saved_projects.addSavedProject( file_path) self.update_recent_projects_actions() def update_recent_projects_actions(self): """Update the list of recent projects.""" if self.saved_projects_list: for i in range( min(len(self.saved_projects_list), self.config.get_max_projects())): text = os.path.basename(self.saved_projects_list[i]) self.saved_projects_actions[i].setText(text) self.saved_projects_actions[i].setData( self.saved_projects_list[i]) self.saved_projects_actions[i].setVisible(True)
def __init__(self, project, scan_list, main_window): """ Initialization of the IterationTable widget. :param project: current project in the software :param scan_list: list of the selected database files :param main_window: software's main_window """ QWidget.__init__(self) # Necessary for using MIA bricks ProcessMIA.project = project self.project = project if not scan_list: self.scan_list = self.project.session.get_documents_names( COLLECTION_CURRENT) else: self.scan_list = scan_list self.main_window = main_window self.iterated_tag = None # values_list will contain the different values of each selected tag self.values_list = [[], []] self.all_tag_values = [] # Checkbox to choose to iterate the pipeline or not self.check_box_iterate = QCheckBox("Iterate pipeline") self.check_box_iterate.stateChanged.connect( self.emit_iteration_table_updated) # Label "Iterate over:" self.label_iterate = QLabel("Iterate over:") # Label that displays the name of the selected tag self.iterated_tag_label = QLabel("Select a tag") # Push button to select the tag to iterate self.iterated_tag_push_button = QPushButton("Select") self.iterated_tag_push_button.clicked.connect( self.select_iteration_tag) # QComboBox self.combo_box = QComboBox() self.combo_box.currentIndexChanged.connect(self.update_table) # filter self.filter_button = QPushButton('Filter') self.filter_button.clicked.connect(self.filter_values) # QTableWidget self.iteration_table = QTableWidget() # Label tag self.label_tags = QLabel("Tags to visualize:") # Each push button will allow the user to visualize a tag in # the iteration browser push_button_tag_1 = QPushButton() push_button_tag_1.setText("SequenceName") push_button_tag_1.clicked.connect( lambda: self.select_visualized_tag(0)) push_button_tag_2 = QPushButton() push_button_tag_2.setText("AcquisitionDate") push_button_tag_2.clicked.connect( lambda: self.select_visualized_tag(1)) # The list of all the push buttons # (the user can add as many as he or she wants) self.push_buttons = [] self.push_buttons.insert(0, push_button_tag_1) self.push_buttons.insert(1, push_button_tag_2) # Labels to add/remove a tag (a push button) self.add_tag_label = ClickableLabel() self.add_tag_label.setObjectName('plus') sources_images_dir = Config().getSourceImageDir() add_tag_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "green_plus.png"))) add_tag_picture = add_tag_picture.scaledToHeight(15) self.add_tag_label.setPixmap(add_tag_picture) self.add_tag_label.clicked.connect(self.add_tag) self.remove_tag_label = ClickableLabel() remove_tag_picture = QPixmap( os.path.relpath(os.path.join(sources_images_dir, "red_minus.png"))) remove_tag_picture = remove_tag_picture.scaledToHeight(20) self.remove_tag_label.setPixmap(remove_tag_picture) self.remove_tag_label.clicked.connect(self.remove_tag) # Layout self.v_layout = QVBoxLayout() self.setLayout(self.v_layout) self.refresh_layout()
def __init__(self, project_root_folder, new_project): """Initialization of the project class. :param project_root_folder: project's path :param new_project: project's object """ if project_root_folder is None: self.isTempProject = True self.folder = os.path.relpath(tempfile.mkdtemp()) else: self.isTempProject = False self.folder = project_root_folder # Checks that the project is not already opened config = Config() opened_projects = config.get_opened_projects() if self.folder not in opened_projects: opened_projects.append(self.folder) config.set_opened_projects(opened_projects) else: raise IOError("The project at " + str(self.folder) + " is already opened " "in another instance " "of the software.") self.database = DatabaseMIA( 'sqlite:///' + os.path.join(self.folder, 'database', 'mia.db')) self.session = self.database.__enter__() if new_project: if not os.path.exists(self.folder): os.makedirs(self.folder) if not os.path.exists(os.path.join(self.folder, "database")): os.makedirs(os.path.join(self.folder, "database")) if not os.path.exists(os.path.join(self.folder, "filters")): os.makedirs(os.path.join(self.folder, "filters")) if not os.path.exists(os.path.join(self.folder, "data")): os.makedirs(os.path.join(self.folder, "data")) if not os.path.exists(os.path.join(self.folder, "data", "raw_data")): os.makedirs(os.path.join(self.folder, "data", "raw_data")) if not os.path.exists( os.path.join(self.folder, "data", "derived_data")): os.makedirs(os.path.join(self.folder, "data", "derived_data")) if not os.path.exists( os.path.join(self.folder, "data", "downloaded_data")): os.makedirs( os.path.join(self.folder, "data", "downloaded_data")) # Properties file created os.mkdir(os.path.join(self.folder, 'properties')) if self.isTempProject: name = "Unnamed project" else: name = os.path.basename(self.folder) properties = dict( name=name, date=datetime.now().strftime('%d/%m/%Y %H:%M:%S'), sorted_tag=TAG_FILENAME, sort_order=0) with open(os.path.join(self.folder, 'properties', 'properties.yml'), 'w', encoding='utf8') \ as propertyfile: yaml.dump(properties, propertyfile, default_flow_style=False, allow_unicode=True) # Adding current and initial collections self.session.add_collection(COLLECTION_CURRENT, TAG_FILENAME, True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_collection(COLLECTION_INITIAL, TAG_FILENAME, True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_collection(COLLECTION_BRICK, BRICK_ID, False, TAG_ORIGIN_BUILTIN, None, None) # Tags manually added self.session.add_field(COLLECTION_CURRENT, TAG_CHECKSUM, FIELD_TYPE_STRING, "Path checksum", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_INITIAL, TAG_CHECKSUM, FIELD_TYPE_STRING, "Path checksum", False, TAG_ORIGIN_BUILTIN, None, None) # TODO Maybe remove checksum tag from populse_mia.itial table self.session.add_field(COLLECTION_CURRENT, TAG_TYPE, FIELD_TYPE_STRING, "Path type", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_INITIAL, TAG_TYPE, FIELD_TYPE_STRING, "Path type", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_CURRENT, TAG_EXP_TYPE, FIELD_TYPE_STRING, "Path exp type", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_INITIAL, TAG_EXP_TYPE, FIELD_TYPE_STRING, "Path exp type", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_CURRENT, TAG_BRICKS, FIELD_TYPE_LIST_STRING, "Path bricks", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_INITIAL, TAG_BRICKS, FIELD_TYPE_LIST_STRING, "Path bricks", True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_NAME, FIELD_TYPE_STRING, "Brick name", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_INPUTS, FIELD_TYPE_JSON, "Brick input(s)", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_OUTPUTS, FIELD_TYPE_JSON, "Brick output(s)", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_INIT, FIELD_TYPE_STRING, "Brick init status", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_INIT_TIME, FIELD_TYPE_DATETIME, "Brick init time", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_EXEC, FIELD_TYPE_STRING, "Brick exec status", False, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_BRICK, BRICK_EXEC_TIME, FIELD_TYPE_DATETIME, "Brick exec time", False, TAG_ORIGIN_BUILTIN, None, None) # Adding default tags for the clinical mode if config.get_clinical_mode() == True: for clinical_tag in CLINICAL_TAGS: if clinical_tag == "Age": field_type = FIELD_TYPE_INTEGER else: field_type = FIELD_TYPE_STRING self.session.add_field(COLLECTION_CURRENT, clinical_tag, field_type, clinical_tag, True, TAG_ORIGIN_BUILTIN, None, None) self.session.add_field(COLLECTION_INITIAL, clinical_tag, field_type, clinical_tag, True, TAG_ORIGIN_BUILTIN, None, None) self.session.save_modifications() # Base modifications, do not count for unsaved modifications self.properties = self.loadProperties() self._unsavedModifications = False self.undos = [] self.redos = [] self.init_filters()
def __init__(self, project, scans_list, process, node_name, plug_name, node_controller, main_window): """ Initialization of the PlugFilter widget :param project: current project in the software :param scans_list: list of database files to filter :param process: process instance of the selected node :param node_name: name of the current node :param plug_name: name of the selected node plug :param node_controller: parent node controller :param main_window: parent main window """ super(PlugFilter, self).__init__(None) from populse_mia.user_interface.data_browser.rapid_search import \ RapidSearch from populse_mia.data_manager.project import \ COLLECTION_CURRENT self.project = project self.node_controller = node_controller self.main_window = main_window self.process = process self.plug_name = plug_name # If the filter is saved in the node plug (not the case now) # if hasattr(self.process, 'filters'): # if self.plug_name in self.process.filters.keys(): # print("Already a filter for {0} plug of {1} process".format( # self.plug_name, self.process.name)) # # TODO: fill the advanced search with the corresponding # filter:orphan: # Verifying that the scan names begin not with a "/" or a "\" if scans_list: scans_list_copy = [] for scan in scans_list: scan_no_pfolder = scan.replace(self.project.folder, "") if scan_no_pfolder[0] in ["\\", "/"]: scan_no_pfolder = scan_no_pfolder[1:] scans_list_copy.append(scan_no_pfolder) self.scans_list = scans_list_copy # If there is no element in scans_list, this means that all the scans # of the database needs to be taken into account else: self.scans_list = self.project.session.get_documents_names( COLLECTION_CURRENT) self.setWindowTitle("Filter - " + node_name + " - " + plug_name) # Graphical components self.table_data = TableDataBrowser(self.project, self, self.node_controller.visibles_tags, False, True, link_viewer=False) # Reducing the list of scans to selection all_scans = self.table_data.scans_to_visualize self.table_data.scans_to_visualize = self.scans_list self.table_data.scans_to_search = self.scans_list self.table_data.update_visualized_rows(all_scans) search_bar_layout = QHBoxLayout() self.rapid_search = RapidSearch(self) self.rapid_search.textChanged.connect(partial(self.search_str)) sources_images_dir = Config().getSourceImageDir() self.button_cross = QToolButton() self.button_cross.setStyleSheet('background-color:rgb(255, 255, 255);') self.button_cross.setIcon( QIcon(os.path.join(sources_images_dir, 'gray_cross.png'))) self.button_cross.clicked.connect(self.reset_search_bar) search_bar_layout.addWidget(self.rapid_search) search_bar_layout.addWidget(self.button_cross) self.advanced_search = AdvancedSearch( self.project, self, self.scans_list, self.node_controller.visibles_tags, from_pipeline=True) self.advanced_search.show_search() push_button_tags = QPushButton("Visualized tags") push_button_tags.clicked.connect(self.update_tags) self.push_button_tag_filter = QPushButton(TAG_FILENAME) self.push_button_tag_filter.clicked.connect(self.update_tag_to_filter) push_button_ok = QPushButton("OK") push_button_ok.clicked.connect(self.ok_clicked) push_button_cancel = QPushButton("Cancel") push_button_cancel.clicked.connect(self.close) # Layout buttons_layout = QHBoxLayout() buttons_layout.addWidget(push_button_tags) buttons_layout.addWidget(self.push_button_tag_filter) buttons_layout.addStretch(1) buttons_layout.addWidget(push_button_ok) buttons_layout.addWidget(push_button_cancel) main_layout = QVBoxLayout() main_layout.addLayout(search_bar_layout) main_layout.addWidget(self.advanced_search) main_layout.addWidget(self.table_data) main_layout.addLayout(buttons_layout) self.setLayout(main_layout) screen_resolution = QApplication.instance().desktop().screenGeometry() width, height = screen_resolution.width(), screen_resolution.height() self.setMinimumWidth(0.6 * width) self.setMinimumHeight(0.8 * height)
def __init__(self, project, test=False, deleted_projects=None): """Main window class, initializes the software appearance and defines interactions with the user. :Parameter: - :project: current project in the software - :test: boolean if the widget is launched from unit tests or not - :deleted_projects: projects that have been deleted """ super(MainWindow, self).__init__() QApplication.restoreOverrideCursor() # We associate these methods and the instance to be able to call them # from anywhere. QCoreApplication.instance().title = self.windowTitle QCoreApplication.instance().set_title = self.setWindowTitle if deleted_projects is not None and deleted_projects: self.msg = PopUpDeletedProject(deleted_projects) self.config = Config() self.config.setSourceImageDir( os.path.join( os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "sources_images")) self.windowName = "MIA - Multiparametric Image Analysis" self.projectName = "Unnamed project" self.project = project self.test = test self.saved_projects = SavedProjects() self.saved_projects_list = self.saved_projects.pathsList self.saved_projects_actions = [] # Define main window view self.create_view_window() # Initialize menu self.menu_file = self.menuBar().addMenu('File') self.menu_edition = self.menuBar().addMenu('Edit') self.menu_help = self.menuBar().addMenu('Help') self.menu_about = self.menuBar().addMenu('About') self.menu_more = self.menuBar().addMenu('More') self.menu_install_process = QMenu('Install processes', self) self.menu_saved_projects = QMenu('Saved projects', self) # Initialize tabs self.tabs = QTabWidget() self.data_browser = DataBrowser(self.project, self) self.image_viewer = QLabel("Coming soon...") self.pipeline_manager = PipelineManagerTab(self.project, [], self) self.centralWindow = QWidget() # Initialize menu actions sources_images_dir = Config().getSourceImageDir() self.action_save_project = self.menu_file.addAction("Save project") self.action_save_project_as = self.menu_file.addAction("Save " "project as") self.action_create = QAction('New project', self) self.action_open = QAction('Open project', self) self.action_save = QAction('Save', self) self.action_save_as = QAction('Save as', self) self.action_import = QAction( QIcon(os.path.join(sources_images_dir, 'Blue.png')), 'Import', self) self.action_see_all_projects = QAction('See all projects', self) self.action_project_properties = QAction('Project properties', self) self.action_software_preferences = QAction('MIA preferences', self) self.action_package_library = QAction('Package library manager', self) self.action_exit = QAction( QIcon(os.path.join(sources_images_dir, 'exit.png')), 'Exit', self) self.action_undo = QAction('Undo', self) self.action_redo = QAction('Redo', self) self.action_documentation = QAction('Documentation', self) self.action_install_processes_folder = QAction('From folder', self) self.action_install_processes_zip = QAction('From zip file', self) # Connect actions & menus views self.create_view_actions() self.create_view_menus() # Create Tabs self.create_tabs() self.setCentralWidget(self.centralWindow) self.showMaximized()
class MiniViewer(QWidget): """MiniViewer that allows to rapidly visualize scans either with a single image per scan with cursors to move in five dimensions or with all images of the greater dimension of the scan. When the latter is selected, the displayed images depends on their dimension: - 3D: display all the slices. - 4D: display the middle slice of the third dimension for each time of the fourth dimension. - 5D: display the middle slice of the third dimension for the first time of the fourth dimension for each time of the fifth dimension. Note: - idx corresponds to the index of the displayed image - idx in [0, self.max_scans] - most of the class's attributes are lists of 0 to self.max_scans elements .. Methods: - __init__: initialise the MiniViewer object - boxSlider: create sliders, their connections and thumbnail labels for a selected index - changePosValue: change the value of a cursor for the selected index - check_box_cursors_state_changed: updates the config file - clearLayouts: clear the final layout - create_slider: create a slider - createDimensionLabels: create the dimension labels for the selected index - createFieldValue: create a field where will be displayed the position of a cursor - createLayouts: create the layouts - displayPosValue: display the position of each cursor for the selected index - enableSliders: enable each slider of the selected index - image_to_pixmap: create a 2D pixmap from a N-D Nifti image - image2DModifications: apply modifications to the image to display it correctly - indexImage: update the sliders values depending on the size of the selected image - navigImage: display the 2D image for the selected index - openTagsPopUp: opens a pop-up to select the legend of the thumbnails - setThumbnail: set the thumbnail tag value under the image frame - show_slices: create the thumbnails from the selected file paths - update_nb_slices: update the config file and the thumbnails - update_visualization_method: update the config file and the thumbnails - verify_slices: verify the number of selected documents """ def __init__(self, project): """Initialise the MiniViewer object :param project: current project in the software """ super().__init__() self.project = project self.first_time = True # The MiniViewer is set hidden to give more space to the data_browser self.setHidden(True) # When multiple selection, limiting the number of thumbnails to # max_scans self.max_scans = 4 # Config that allows to read the software preferences self.config = Config() # Initializing some components of the MiniViewer self.labels = QWidget() self.frame = QFrame() self.scroll_area = QScrollArea() self.scroll_area.setWidget(self.frame) self.frame_final = QFrame() self.label_nb_slices = QLabel() self.label_nb_slices.setText("Maximum number of slices: ") self.line_edit_nb_slices = QLineEdit() self.line_edit_nb_slices.setText(str(self.config.getNbAllSlicesMax())) self.line_edit_nb_slices.returnPressed.connect(self.update_nb_slices) # All these objects are depending on the number of scans to visualize self.im_2D = [] self.slider_3D = [] self.slider_4D = [] self.slider_5D = [] self.txt_slider_3D = [] self.txt_slider_4D = [] self.txt_slider_5D = [] self.label3D = [] self.label4D = [] self.label5D = [] self.imageLabel = [] self.img = [] self.label_description = [] # Layouts self.createLayouts() self.setLayout(self.v_box_final) # Checkboxes self.check_box_slices = QCheckBox('Show all slices (no cursors)') if self.config.getShowAllSlices() == True: self.check_box_slices.setCheckState(Qt.Checked) else: self.check_box_slices.setCheckState(Qt.Unchecked) self.check_box_slices.stateChanged.connect( self.update_visualization_method) self.check_box_cursors = QCheckBox('Chain cursors') self.check_box_cursors.setToolTip("Allows to connect all cursors " "when selecting multiple documents") if self.config.getChainCursors() == True: self.check_box_cursors.setCheckState(Qt.Checked) else: self.check_box_cursors.setCheckState(Qt.Unchecked) self.check_box_cursors.stateChanged.connect( self.check_box_cursors_state_changed) self.file_paths = "" def boxSlider(self, idx): """Define horizontal sliders connections and thumbnail labels. :param idx: the selected index """ self.slider_3D.insert(idx, self.create_slider(0, 0, 0)) self.slider_4D.insert(idx, self.create_slider(0, 0, 0)) self.slider_5D.insert(idx, self.create_slider(0, 0, 0)) self.slider_3D[idx].valueChanged.connect( partial(self.changePosValue, idx, 1)) self.slider_4D[idx].valueChanged.connect( partial(self.changePosValue, idx, 2)) self.slider_5D[idx].valueChanged.connect( partial(self.changePosValue, idx, 3)) self.txt_slider_3D.insert(idx, self.createFieldValue()) self.txt_slider_4D.insert(idx, self.createFieldValue()) self.txt_slider_5D.insert(idx, self.createFieldValue()) def check_box_cursors_state_changed(self): """Update the config file. Called when the state of the checkbox to chain the cursors changes. """ if self.check_box_cursors.checkState() == Qt.Checked: self.config.setChainCursors(True) elif self.check_box_cursors.checkState() == Qt.Unchecked: self.config.setChainCursors(False) def clearLayouts(self): """Clear the final layout""" for i in reversed(range(self.v_box_final.count())): if self.v_box_final.itemAt(i).widget() is not None: self.v_box_final.itemAt(i).widget().setParent(None) def changePosValue(self, idx, cursor_to_change): """ Change the value of a cursor for the selected index. :param idx: the selected index :param cursor_to_change: the cursor to change (1, 2 or 3) """ # If the "Chain cursors" mode is not selected, there is nothing to do if self.check_box_cursors.checkState() == Qt.Unchecked: self.navigImage(idx) else: # Checking with cursor has been modified if cursor_to_change == 1: cursor = self.slider_3D elif cursor_to_change == 2: cursor = self.slider_4D else: cursor = self.slider_5D # Loop on the thumbnails for idx_loop in range(min(self.max_scans, len(self.file_paths))): # Disconnecting the connection when changing other cursors # values cursor[idx_loop].valueChanged.disconnect() # Do something only when the cursor is not the one that has # been changed by the user if idx_loop != idx: if cursor[idx].value() == cursor[idx].maximum(): value = cursor[idx_loop].maximum() elif cursor[idx].value() == cursor[idx].minimum(): value = cursor[idx_loop].minimum() else: # Updating the new value as the value of the cursor # that has been changed by the user value = round((cursor[idx_loop].maximum() + 1) * (cursor[idx].value() + 1) / max(1, cursor[idx].maximum() + 1)) value = min(cursor[idx_loop].maximum(), value - 1) value = max(0, int(value)) cursor[idx_loop].setValue(value) # Changing the image to show self.navigImage(idx_loop) # Reconnecting cursor[idx_loop].valueChanged.connect( partial(self.changePosValue, idx_loop, cursor_to_change)) def createDimensionLabels(self, idx): """Create the dimension labels for the selected index. :param idx: the selected index """ font = QFont() font.setPointSize(9) self.label3D.insert(idx, QLabel()) self.label4D.insert(idx, QLabel()) self.label5D.insert(idx, QLabel()) self.label3D[idx].setFont(font) self.label4D[idx].setFont(font) self.label5D[idx].setFont(font) self.label3D[idx].setText('3D: ') self.label4D[idx].setText('4D: ') self.label5D[idx].setText('5D: ') def createFieldValue(self): """Create a field where will be displayed the position of a cursor. :return: the corresponding field """ fieldValue = QLineEdit() fieldValue.setEnabled(False) fieldValue.setFixedWidth(50) fieldValue.setAlignment(Qt.AlignCenter) fieldValue.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) font = QFont() font.setPointSize(9) fieldValue.setFont(font) return fieldValue def createLayouts(self): """Create the layouts.""" self.h_box_images = QHBoxLayout() self.h_box_images.setSpacing(10) self.v_box = QVBoxLayout() self.v_box_final = QVBoxLayout() self.h_box_slider_3D = QHBoxLayout() self.h_box_slider_4D = QHBoxLayout() self.h_box_slider_5D = QHBoxLayout() self.v_box_sliders = QVBoxLayout() self.h_box = QHBoxLayout() self.h_box_check_box = QHBoxLayout() self.v_box_thumb = QVBoxLayout() self.h_box_thumb = QHBoxLayout() def create_slider(self, maxm=0, minm=0, pos=0): """Generate an horizontal slider. :param maxm: slider's maximum :param minm: slider's minimum :param pos: slider's initial value :return: the slider object """ slider = QSlider(Qt.Horizontal) slider.setFocusPolicy(Qt.StrongFocus) slider.setTickInterval(1) slider.setMaximum(maxm) slider.setMinimum(minm) slider.setValue(pos) slider.setEnabled(False) return slider def displayPosValue(self, idx): """Display the position of each cursor for the selected index. :param idx: the selected index """ self.txt_slider_3D[idx].setText( str(self.slider_3D[idx].value() + 1) + ' / ' + str(self.slider_3D[idx].maximum() + 1)) self.txt_slider_4D[idx].setText( str(self.slider_4D[idx].value() + 1) + ' / ' + str(self.slider_4D[idx].maximum() + 1)) self.txt_slider_5D[idx].setText( str(self.slider_5D[idx].value() + 1) + ' / ' + str(self.slider_5D[idx].maximum() + 1)) def enableSliders(self, idx): """Enable all the horizontal slider. :param idx: the slider's index """ self.slider_3D[idx].setEnabled(True) self.slider_4D[idx].setEnabled(True) self.slider_5D[idx].setEnabled(True) def image_to_pixmap(self, im, i): """Create a 2D pixmap from a N-D Nifti image. :param im: Nifti image :param i: index of the slide :return: the corresponding pixmap """ # The image to display depends on the dimension of the image # In the 3D case, each slice is displayed if len(im.shape) == 3: im_2D = im.get_data()[:, :, i].copy() # In the 4D case, each middle slice of the 3D dimension is displayed # for each time in the 4D dimension elif len(im.shape) == 4: im_3D = im.get_data()[:, :, :, i].copy() middle_slice = int(im_3D.shape[2] / 2) im_2D = im_3D[:, :, middle_slice] # In the 5D case, each first time of the 4D dimension and # its middle slice of the 3D dimension is displayed elif len(im.shape) == 5: im_4D = im.get_data()[:, :, :, :, i].copy() im_3D = im_4D[:, :, :, 1] middle_slice = int(im_3D.shape[2] / 2) im_2D = im_3D[:, :, middle_slice] else: im_2D = [0] # Making some pixel modification to display the image correctly im_2D = self.image2DModifications(0, im_2D) w, h = im_2D.shape im_Qt = QImage(im_2D.data, w, h, QImage.Format_Indexed8) pixm = QPixmap.fromImage(im_Qt) return pixm def image2DModifications(self, idx, im2D=None): """Apply modifications to display the image correctly. :param idx: the selected index :param im2D: image to modify """ display_size = (128, 128) display_type = np.uint8 # this MUST be an integer data type display_pctl = 0.5 # percentile (0.5%) of values to clip at the low and high end of intensities. display_max = np.iinfo(display_type).max display_min = np.iinfo(display_type).min im2d_provided = im2D is not None if not im2d_provided: im2D = self.im_2D[idx] # Resize image first, for three reasons: # 1 - it may slightly changes the intensity scale, so re-scaling should be done after this # 2 - rescaling before rotation is slightly faster, specially for large images (> display_size). # 3 - rescaling may alter the occurrence of nan or infinite values (e.g. an image may become all-nan) # anti_aliasing keyword is defined in skimage since version 0.14.0 if verCmp(sk.__version__, '0.14.0', 'sup'): im2D = resize(im2D, display_size, mode='constant', anti_aliasing=False) else: im2D = resize(im2D, display_size, mode='constant') # Rescale image while handling Nans and infinite values im_mask = np.isfinite(im2D) if np.any(im_mask): # if we have any finite value to work with im2D -= np.percentile( im2D[im_mask], display_pctl) # shift the lower percentile chosen to 0.0 im_max = np.percentile( im2D[im_mask], 100.0 - display_pctl) # determine max from upper percentile if im_max > 0: # avoid dividing by zero im2D *= (display_max - display_min) / im_max # re-scale to display range im2D += display_min # shift lowest value to lower limit of display range np.clip( im2D, display_min, display_max, im2D) # clip all values to display range, remove infinite values im2D = im2D.astype( display_type ) # convert to integer display data type. NaNs get converted to 0. im2D = np.rot90(im2D, 3).copy( ) # Rotate. Copy array to avoid negative strides (Qt doesn't handle that) if im2d_provided: return im2D else: self.im_2D[idx] = im2D def indexImage(self, idx): """Update all slider values according to the size of the current image. :param idx: the selected index """ # Getting the sliders value sl3D = self.slider_3D[idx].value() sl4D = self.slider_4D[idx].value() sl5D = self.slider_5D[idx].value() # Depending on the dimension, reading the image data and # changing the cursors maximum if len(self.img[idx].shape) == 3: self.im_2D.insert(idx, self.img[idx].get_data()[:, :, sl3D].copy()) self.slider_3D[idx].setMaximum(self.img[idx].shape[2] - 1) self.slider_4D[idx].setMaximum(0) self.slider_5D[idx].setMaximum(0) if len(self.img[idx].shape) == 4: self.im_2D.insert( idx, self.img[idx].get_data()[:, :, sl3D, sl4D].copy()) self.slider_3D[idx].setMaximum(self.img[idx].shape[2] - 1) self.slider_4D[idx].setMaximum(self.img[idx].shape[3] - 1) self.slider_5D[idx].setMaximum(0) if len(self.img[idx].shape) == 5: self.im_2D.insert( idx, self.img[idx].get_data()[:, :, sl3D, sl4D, sl5D].copy()) self.slider_3D[idx].setMaximum(self.img[idx].shape[2] - 1) self.slider_4D[idx].setMaximum(self.img[idx].shape[3] - 1) self.slider_5D[idx].setMaximum(self.img[idx].shape[4] - 1) def navigImage(self, idx): """Display the 2D image for the selected index. :param idx: the selected index """ self.indexImage(idx) self.displayPosValue(idx) self.image2DModifications(idx) w, h = self.im_2D[idx].shape image = QImage(self.im_2D[idx].data, w, h, QImage.Format_Indexed8) pixm = QPixmap.fromImage(image) self.imageLabel[idx].setPixmap(pixm) def openTagsPopUp(self): """Open a pop-up to select the legend of the thumbnails.""" self.popUp = PopUpSelectTag(self.project) self.popUp.setWindowTitle("Select the image viewer tag") if self.popUp.exec_(): self.verify_slices(self.file_paths) def setThumbnail(self, file_path_base_name, idx): """Set the thumbnail tag value under the image frame. :param file_path_base_name: basename of the selected path :param idx: index of the image """ # Looking for the tag value to display as a legend of the thumbnail for scan in self.project.session.get_documents_names( COLLECTION_CURRENT): if scan == file_path_base_name: value = self.project.session.get_value( COLLECTION_CURRENT, scan, self.config.getThumbnailTag()) if value is not None: self.label_description[idx].setText( str(value)[:self.nb_char_max]) else: self.label_description[idx].setText( data_browser.not_defined_value[:self.nb_char_max]) self.label_description[idx].setToolTip( os.path.basename(self.config.getThumbnailTag())) def show_slices(self, file_paths): """Creates the thumbnails from the selected file paths. :param file_paths: the selected file paths """ # If it's the first time that this function is called, the MiniViewer # has to be shown if self.first_time: self.setHidden(False) self.first_time = False # If the user has willingly hidden the MiniViewer, the Processes are # not made if self.isHidden(): pass else: self.do_nothing = [False] * len(file_paths) self.file_paths = file_paths self.max_scans = len(file_paths) self.setMinimumHeight(220) self.clearLayouts() self.frame = QFrame(self) self.frame_final = QFrame(self) # Limiting the legend of the thumbnails self.nb_char_max = 60 font = QFont() font.setPointSize(9) # Reading the images from the file paths for idx, file_path in enumerate(self.file_paths.copy()): try: self.img.insert(idx, nib.load(file_path)) except nib.filebasedimages.ImageFileError: print("Error while trying to display the image " + file_path) self.file_paths.remove(file_path) except FileNotFoundError: print("File " + file_path + " not existing") self.file_paths.remove(file_path) # If we are in the "cursors" display mode if self.check_box_slices.checkState() == Qt.Unchecked: # Layout to aligne each thumbnail (image + cursors) self.h_box_thumb = QHBoxLayout() # idx represents the index of the selected image for idx in range(min(self.max_scans, len(self.file_paths))): if not self.do_nothing[idx]: # Creating sliders and labels self.boxSlider(idx) self.enableSliders(idx) self.createDimensionLabels(idx) # Getting the sliders value and reading the image data self.indexImage(idx) # Making some pixel modification to display the image # correctly self.image2DModifications(idx) self.displayPosValue(idx) w, h = self.im_2D[idx].shape im_Qt = QImage(self.im_2D[idx].data, w, h, QImage.Format_Indexed8) pixm = QPixmap.fromImage(im_Qt) file_path_base_name = os.path.basename( self.file_paths[idx]) # imageLabel is the label where the image is displayed # (as a pixmap) self.imageLabel.insert(idx, QLabel(self)) self.imageLabel[idx].setPixmap(pixm) self.imageLabel[idx].setToolTip(file_path_base_name) self.label_description.insert(idx, ClickableLabel()) self.label_description[idx].setFont(font) self.label_description[idx].clicked.connect( self.openTagsPopUp) # Looking for the tag value to display as a # legend of the thumbnail file_path_base_name = os.path.relpath( self.file_paths[idx], self.project.folder) self.setThumbnail(file_path_base_name, idx) # Layout that corresponds to the 3D dimension self.h_box_slider_3D = QHBoxLayout() self.h_box_slider_3D.addWidget(self.label3D[idx]) self.h_box_slider_3D.addWidget(self.txt_slider_3D[idx]) self.h_box_slider_3D.addWidget(self.slider_3D[idx]) # Layout that corresponds to the 4D dimension self.h_box_slider_4D = QHBoxLayout() self.h_box_slider_4D.addWidget(self.label4D[idx]) self.h_box_slider_4D.addWidget(self.txt_slider_4D[idx]) self.h_box_slider_4D.addWidget(self.slider_4D[idx]) # Layout that corresponds to the 5D dimension self.h_box_slider_5D = QHBoxLayout() self.h_box_slider_5D.addWidget(self.label5D[idx]) self.h_box_slider_5D.addWidget(self.txt_slider_5D[idx]) self.h_box_slider_5D.addWidget(self.slider_5D[idx]) # Layout for the three sliders self.v_box_sliders = QVBoxLayout() self.v_box_sliders.addLayout(self.h_box_slider_3D) self.v_box_sliders.addLayout(self.h_box_slider_4D) self.v_box_sliders.addLayout(self.h_box_slider_5D) # Layout that corresponds to the image + the sliders self.h_box = QHBoxLayout() self.h_box.addWidget(self.imageLabel[idx]) self.h_box.addLayout(self.v_box_sliders) # Layout that corresponds to the image and sliders + # the description self.v_box_thumb = QVBoxLayout() self.v_box_thumb.addLayout(self.h_box) self.v_box_thumb.addWidget(self.label_description[idx]) # Layout that will contain all the thumbnails self.h_box_thumb.addLayout(self.v_box_thumb) self.frame.setLayout(self.h_box_thumb) # If we are in the "all slices" display mode else: self.h_box_images = QHBoxLayout() self.h_box_images.setSpacing(10) self.v_box_scans = QVBoxLayout() # idx represents the index of the selected image for idx in range(len(self.file_paths)): file_path_base_name = os.path.relpath( self.file_paths[idx], self.project.folder) self.label_description.insert(idx, ClickableLabel()) self.label_description[idx].setFont(font) self.label_description[idx].clicked.connect( self.openTagsPopUp) # Looking for the tag value to display as a legend # of the thumbnail self.setThumbnail(file_path_base_name, idx) # Depending of the dimension of the image, # the legend of each image and the number of images to # display will change if not self.do_nothing[idx]: if len(self.img[idx].shape) == 3: nb_slices = self.img[idx].shape[2] txt = "Slice n°" elif len(self.img[idx].shape) == 4: nb_slices = self.img[idx].shape[3] txt = "Time n°" elif len(self.img[idx].shape) == 5: nb_slices = self.img[idx].shape[4] txt = "Study n°" else: nb_slices = 0 # Limiting the number of images to the number # chosen by the user for i in range( min(nb_slices, int(self.line_edit_nb_slices.text()))): pixm = self.image_to_pixmap(self.img[idx], i) self.v_box = QVBoxLayout() # label corresponds to the label where one image # is displayed label = QLabel(self) label.setPixmap(pixm) label.setToolTip( os.path.basename(self.file_paths[idx])) # Legend of the image (depends on the number # of dimensions) label_info = QLabel() label_info.setFont(font) label_info.setText(txt + str(i + 1)) label_info.setAlignment(QtCore.Qt.AlignCenter) self.v_box.addWidget(label) self.v_box.addWidget(label_info) # This layout allows to chain each image self.h_box_images.addLayout(self.v_box) self.v_box_scans.addLayout(self.h_box_images) self.v_box_scans.addWidget(self.label_description[idx]) self.frame.setLayout(self.v_box_scans) # Adding a scroll area if the thumbnails are too large self.scroll_area = QScrollArea() self.scroll_area.setWidget(self.frame) self.h_box_check_box = QHBoxLayout() if self.check_box_slices.isChecked(): self.h_box_check_box.addStretch(1) self.label_nb_slices.setHidden(False) self.line_edit_nb_slices.setHidden(False) self.h_box_check_box.addWidget(self.label_nb_slices) self.h_box_check_box.addWidget(self.line_edit_nb_slices) self.check_box_cursors.setHidden(True) else: self.check_box_cursors.setHidden(False) self.h_box_check_box.addWidget(self.check_box_cursors) self.h_box_check_box.addStretch(1) self.label_nb_slices.setHidden(True) self.line_edit_nb_slices.setHidden(True) self.h_box_check_box.addWidget(self.check_box_slices) self.v_box_final.addLayout(self.h_box_check_box) self.v_box_final.addWidget(self.scroll_area) def update_nb_slices(self): """Update the config file and the thumbnails. Called when the number of slices to visualize changes. """ nb_slices = self.line_edit_nb_slices.text() self.config.setNbAllSlicesMax(nb_slices) self.verify_slices(self.file_paths) def update_visualization_method(self): """Update the config file and the thumbnails. Called when the state of the checkbox to show all slices changes. """ if self.check_box_slices.checkState() == Qt.Checked: self.config.setShowAllSlices(True) elif self.check_box_slices.checkState() == Qt.Unchecked: self.config.setShowAllSlices(False) self.verify_slices(self.file_paths) def verify_slices(self, file_paths): """Make 'Show all slices' checkbox unclickable if len(file_paths) > 1. :param file_paths: the selected documents """ # Updating the config self.config = Config() if len(file_paths) > 1: self.config.setShowAllSlices(False) self.check_box_slices.setCheckState(Qt.Unchecked) self.check_box_slices.setCheckable(False) else: self.check_box_slices.setCheckable(True) self.show_slices(file_paths)