diff --git a/.gitignore b/.gitignore index 4a46d8f1d..d94baf7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -.idea -.pyc -.DS_Store +*.idea +*.pyc +*.DS_Store +*.tmp +**/__pycache__/ diff --git a/README.md b/README.md index 3e49d309a..551201263 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,24 @@ -pyOpenMS Extra -============= - -pyOpenMS are the Python bindings to the OpenMS open-source C++ library for -LC-MS data management and analyses. The Python bindings cover a large part of -the OpenMS API to enable rapid algorithm development and workflow development. -pyOpenMS supports the Proteomics Standard Initiative (PSI) formats for MS data. - -These utils contain documentation, installation instructions and example code -that show the different functions of pyOpenMS. - -Installation -============= - -Installation is best done through [PyPI](https://pypi.python.org/pypi/pyopenms) -(the Python package index) where binary packages are provided for the release -versions of OpenMS, covering Linux/Mac/Windows. - -Documentation -============= -The pyOpenMS documentation is also contained in this repository, see [docs/README.md](docs/README.md) +# pyOpenMS Extra + +============= + +pyOpenMS are the Python bindings to the OpenMS open-source C++ library for +LC-MS data management and analyses. The Python bindings cover a large part of +the OpenMS API to enable rapid algorithm development and workflow development. +pyOpenMS supports the Proteomics Standard Initiative (PSI) formats for MS data. + +These utils contain documentation, installation instructions and example code +that show the different functions of pyOpenMS. + +## Installation + +============= + +Installation is best done through [PyPI](https://pypi.python.org/pypi/pyopenms) +(the Python package index) where binary packages are provided for the release +versions of OpenMS, covering Linux/Mac/Windows. + +## Documentation + +============= +The pyOpenMS documentation is also contained in this repository, see [documentation/README.md](docs/README.md) diff --git a/src/apps/FLASHDeconvViewer.py b/src/apps/FLASHDeconvViewer.py index 14a14fccc..6f2741f77 100644 --- a/src/apps/FLASHDeconvViewer.py +++ b/src/apps/FLASHDeconvViewer.py @@ -39,7 +39,8 @@ QDialogButtonBox, ) from matplotlib import cm -from pyqtgraph import PlotWidget +from pyqtgraph import PlotWidget # noqa: E402 + sys.path.insert(0, "../view/") from SpecViewer import ScanBrowserWidget, App diff --git a/src/apps/IDViewer.py b/src/apps/IDViewer.py index aed23cb74..20c174fd7 100644 --- a/src/apps/IDViewer.py +++ b/src/apps/IDViewer.py @@ -14,7 +14,7 @@ ) sys.path.insert(0, "../view") -from ControllerWidget import ControllerWidget +from ControllerWidget import ControllerWidget # noqa: E402 # structure for annotation (here for reference) PeakAnnoStruct = namedtuple( diff --git a/src/apps/Icons/IconOpenMS.png b/src/apps/Icons/IconOpenMS.png new file mode 100644 index 000000000..b10846696 Binary files /dev/null and b/src/apps/Icons/IconOpenMS.png differ diff --git a/src/apps/Icons/load_icon.png b/src/apps/Icons/load_icon.png new file mode 100644 index 000000000..b33b8b508 Binary files /dev/null and b/src/apps/Icons/load_icon.png differ diff --git a/src/apps/Icons/open.svg b/src/apps/Icons/open.svg new file mode 100644 index 000000000..adca95fdf --- /dev/null +++ b/src/apps/Icons/open.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/apps/Icons/run.svg b/src/apps/Icons/run.svg new file mode 100644 index 000000000..5c491a1b9 --- /dev/null +++ b/src/apps/Icons/run.svg @@ -0,0 +1,10 @@ + + + + + + + + >RUN + + diff --git a/src/apps/Icons/run_icon.png b/src/apps/Icons/run_icon.png new file mode 100644 index 000000000..3ceb75e1d Binary files /dev/null and b/src/apps/Icons/run_icon.png differ diff --git a/src/apps/Icons/save.svg b/src/apps/Icons/save.svg new file mode 100644 index 000000000..43c514656 --- /dev/null +++ b/src/apps/Icons/save.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/apps/Icons/save_icon.png b/src/apps/Icons/save_icon.png new file mode 100644 index 000000000..1564ebdfa Binary files /dev/null and b/src/apps/Icons/save_icon.png differ diff --git a/src/apps/ProteinQuantification.py b/src/apps/ProteinQuantification.py new file mode 100644 index 000000000..ad3220d09 --- /dev/null +++ b/src/apps/ProteinQuantification.py @@ -0,0 +1,691 @@ +import sys, os, glob, platform +from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, \ + QTabWidget, QAction, QInputDialog, QMessageBox, QFileDialog, \ + QWidget, QLabel, QVBoxLayout, QCheckBox, QHBoxLayout +from PyQt5.QtGui import QIcon, QPalette, QColor, QFont, QPixmap +from PyQt5.QtCore import Qt +sys.path.append(os.getcwd()+'/../view') +from GUI_FastaViewer import GUI_FastaViewer # noqa E402 +from ConfigView import ConfigView # noqa E402 +from mzMLTableView import mzMLTableView # noqa E402 +from MultipleSpecView import MultipleSpecView # noqa E402 +from mzTabTableWidget import Window as mzTabTableWidget # noqa E402 +sys.path.append(os.getcwd() + '/../model') +from tableDataFrame import TableDataFrame as Tdf # noqa E402 +sys.path.append(os.getcwd() + '/../controller') +from filehandler import FileHandler as fh # noqa E402 +sys.path.insert(0, '../examples') +from descriptions import Descriptions as desc + +class ProteinQuantification(QMainWindow): + """ + Application to use different Widgets in one Window: + First Tab: Welcome tab with information about how the GUI works. + Second Tab: Config view - here the .ini file can be viewed and edited + Third Tab: mzMLTable view - the experimental design can be + viewed and edited. + Fourth Tab: Fasta view - fasta files can be loaded and inspected + Fifth Tab: Spec view - ms spectras from loaded mzML files can be seen + and inspected + Sixth Tab: mzTabTable view - The result of the ProteomicsLFQ + is displayed + """ + + def __init__(self): + QMainWindow.__init__(self) + self.initUI() + self.initVars() + # flag for themetoggle + self.flag = False + self.setPalette(self.palette) + self.setTheme() + self.setAcceptDrops(True) + + def initUI(self): + ''' + Sets the window with all applications and widgets. + ''' + descriptions = desc.descriptions + widgetlist = {"Welcome":[QWidget(),"welcome"],"XML-Viewer":[ConfigView(),"cview"],"Experimental-Design":[mzMLTableView(),"tview"],"Fasta-Viewer":[GUI_FastaViewer(),"fview"],"Spec-Viewer":[MultipleSpecView(),"sview"],"mzTab-Viewer":[mzTabTableWidget(),"xview"]} + self.view = QTabWidget() + for wname in widgetlist: + a = widgetlist[wname][0] + setattr(self, widgetlist[wname][1],a ) + self.view.addTab(a, wname) + + self.view.setTabEnabled(5, False) + + self.palette = QPalette() + + menubar = self.menuBar() + menubar.setNativeMenuBar(False) + projectMenu = menubar.addMenu('Project') + parametersMenu = menubar.addMenu('Parameters') + loadAction = QAction(QIcon("Icons/load_icon.png"), + "&Load Project", self) + loadAction.setShortcut("Ctrl+O") + saveAction = QAction(QIcon("Icons/save_icon.png"), + "&Save Project", self) + saveAction.setShortcut("Ctrl+S") + runAction = QAction(QIcon("Icons/run_icon.png"), + "&Run in Terminal", self) + runAction.setShortcut("Ctrl+R") + Threads = QAction("&Adjust the Threadnumber", self) + FDR = QAction("&Adjust the protein FDR", self) + Out = QAction("&Choose outputfiles", self) + + projectMenu.addAction(loadAction) + projectMenu.addAction(saveAction) + projectMenu.addAction(runAction) + parametersMenu.addAction(Threads) + parametersMenu.addAction(FDR) + parametersMenu.addAction(Out) + + runAction.triggered.connect(self.runFunction) + FDR.triggered.connect(self.adjustFDR) + Threads.triggered.connect(self.adjustThreads) + Out.triggered.connect(self.chooseOutputfiles) + + saveAction.triggered.connect(self.saveFunction) + loadAction.triggered.connect(self.loadFunction) + + #themeswitcher + settingsMenu = menubar.addMenu('Settings') + switchThemeAction = QAction('Change Theme', self) + settingsMenu.addAction(switchThemeAction) + switchThemeAction.triggered.connect(self.switchTheme) + + # Welcome Tab + normalFont = QFont("Helvetica", 11) + welcome = QLabel() + welcome.setText(descriptions["welcome"]) + welcome.setFont(normalFont) + welcome_layout = QVBoxLayout() + welcome_layout.addWidget(welcome, 2, Qt.AlignTop) + + iconOpenMs = QPixmap("Icons/IconOpenMS.png") + iconLabel = QLabel() + iconLabel.setPixmap(iconOpenMs) + + welcome_layout.addWidget(iconLabel, 4, Qt.AlignTop) + center_layout = QVBoxLayout() + view= self.view + + for i in range(1,view.count()): + print(view.tabText(i)) + label = QLabel() + label.setText(descriptions[view.tabText(i)]) + label.setFont(normalFont) + if i==1: + center_layout.addWidget(label,4,Qt.AlignTop) + else: + center_layout.addWidget(label,4) + + central_layout = QHBoxLayout() + central_layout.addLayout(welcome_layout, 5) + central_layout.addLayout(center_layout, 5) + + self.welcome.setLayout(central_layout) + + self.setCentralWidget(self.view) + self.resize(1280, 720) + self.center() + self.setWindowTitle('Protein Quantification') + self.show() + # print(self.view.count()) + self.view.currentChanged.connect(self.onChange) + + def initVars(self): + """ + initiates usable variables + """ + self.ini_loaded = False + self.tablefile_loaded = False + self.fasta_loaded = False + self.mztab_loaded = False + self.cxml_out = True + self.msstats_out = True + + self.loaded_dir = "" + self.loaded_ini = "" + self.loaded_table = "" + self.loaded_fasta = "" + self.loaded_mztab = "" + + self.threads = 1 + self.fdr = 0.3 + self.procdone = False + + def center(self): + """ + centers the widget to the screen + """ + qr = self.frameGeometry() + cp = QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + def chooseOutputfiles(self): + """ + Opens a popup window to choose the outputfiles + If output is generated checkbox is checked. + """ + # Popup + self.outputCheckBoxWindow = PopupWindow() + mainWidget = QWidget() + mztabCheckbox = QCheckBox("Generate a mzTab outputfile") + cxmlCheckbox = QCheckBox("Generate a cxml outputfile") + msstatsCheckbox = QCheckBox("Generate a msstats outputfile") + layout = QVBoxLayout() + layout.addWidget(mztabCheckbox) + layout.addWidget(cxmlCheckbox) + layout.addWidget(msstatsCheckbox) + mainWidget.setLayout(layout) + self.outputCheckBoxWindow.setCentralWidget(mainWidget) + self.outputCheckBoxWindow.setTitle("Choose Outputfiles") + + # Checkboxstates + mztabCheckbox.setChecked(True) + mztabCheckbox.setEnabled(False) + + if self.cxml_out: + cxmlCheckbox.setChecked(True) + + if self.msstats_out: + msstatsCheckbox.setChecked(True) + + # Change Checkbox + cxmlCheckbox.clicked.connect(self.togglecxml) + msstatsCheckbox.clicked.connect(self.togglemsstats) + + def togglecxml(self): + if self.cxml_out: + self.cxml_out = False + else: + self.cxml_out = True + + def togglemsstats(self): + if self.msstats_out: + self.msstats_out = False + else: + self.msstats_out = True + + def adjustFDR(self): + """ + The user is allowed to change th FDR + """ + newfdr, ok = QInputDialog.getDouble( + self, "Adjust the value for the protein FDR", + "Please specify a double as new FDR value") + if ok: + if newfdr > 0: + if newfdr <= 1: + self.fdr = newfdr + else: + QMessageBox.about(self, "Warning", + "Please specify a value" + + "between 0.0 and 1.0 for the FDR.") + else: + QMessageBox.about(self, "Warning", + "Please specify a positive" + + "value for the FDR.") + + def adjustThreads(self): + """ + The user is allowed to change the number of threads + """ + newthreads, ok = QInputDialog.getInt( + self, "Adjust the number of threads", + "Please specify the number of threads for the processing") + if ok: + if newthreads > 0: + self.threads = newthreads + else: + QMessageBox.about(self, "Warning", + "Please specify a positive" + + "number of threads.") + + def runFunction(self): + """ + runs the processing from the GUI in a Terminal + based on the ProteomicsLFQ command of OpenMS + """ + self.view.setTabEnabled(5, True) + self.saveFunction() + self.procdone = False + outfileprefix, ok = QInputDialog.getText(self, + "Prefix for outputfiles", + "Please specify a prefix " + + "for the outputfiles") + if ok: + + projectfolder = self.loaded_dir + + mzMLExpLayout = self.tview.getDataFrame() + try: + mzMLfiles = mzMLExpLayout['Spectra_Filepath'] + idXMLfiles = [] + for mzML in mzMLfiles: + temp = mzML.split(".") + idXML = temp[0] + ".idXML" + idXMLfiles.append(idXML) + mzMLidXMLdefined = True + except KeyError: + QMessageBox.about(self, "Warning", "Please load or " + + "create an Experimental Design first") + mzMLidXMLdefined = False + + expdesign = self.loaded_table + dbfasta = self.loaded_fasta + inifile = self.loaded_ini + + if mzMLidXMLdefined: + runcall = "ProteomicsLFQ " + mzMLs = "-in " + " ".join(mzMLfiles) + idXMLs = " -ids " + " ".join(idXMLfiles) + design = " -design " + expdesign + " " + refdb = "-fasta " + dbfasta + " " + configini = "-ini " + inifile + " " + threads = "-threads " + str(self.threads) + " " + fdr = "-proteinFDR " + str(self.fdr) + " " + out = "" + if self.cxml_out: + out += "-out_cxml " + outfileprefix + ".consensusXML.tmp " + if self.msstats_out: + out += "-out_msstats " + outfileprefix + ".csv.tmp " + + out += "-out " + outfileprefix + ".mzTab.tmp" + + command = (runcall + mzMLs + idXMLs + design + + refdb + configini + threads + fdr + out) + os.chdir(projectfolder) + os.system(command) + self.procdone = True + QMessageBox.about(self, "Information", "Processing has been " + + "performed and outputfiles saved to " + + "projectfolder") + mztabfile = outfileprefix + ".mzTab.tmp" + print(mztabfile) + try: + self.xview.readFile(mztabfile) + self.loaded_mztab = mztabfile + self.mztab_loaded = True + self.view.setCurrentWidget(self.xview) + except FileNotFoundError: + QMessageBox.about(self, "Warning", "Some Error occurred " + + "and no mzTab could be found.") + + + def saveFunction(self): + """ + saves all work from the GUI in chosen folder + the prefix of the outputfiles can be choosen + """ + dlg = QFileDialog(self) + filePath = dlg.getExistingDirectory() + + if filePath: + filePath = filePath + "/" + tablePath = "" + ok = False + table_empty = self.tview.table.rowCount() <= 0 + + if table_empty: + tablePath = "" + self.tablefile_loaded = False + self.tview.tablefile_loaded = False + + if self.tablefile_loaded is False and table_empty is False \ + and self.tview.tablefile_loaded is False: + prefix, ok = QInputDialog.getText( + self, "Prefix for outputfiles", + "Please specify a prefix " + + "for the outputfiles") + if ok: + tablePath = filePath + prefix + "_design.tsv" + self.loaded_table = prefix + "_design.tsv" + self.tablefile_loaded = True + + if self.tablefile_loaded and self.tview.tablefile_loaded is False: + tablePath = filePath + self.loaded_table + + if self.tview.tablefile_loaded: + tablePath = filePath + self.tview.loaded_table + + if (ok or self.tablefile_loaded or self.tview.tablefile_loaded) \ + and table_empty is False: + df = Tdf.getTable(self.tview) + fh.exportTable(self.tview, df, tablePath, "tsv") + + xmlPath = filePath + self.loaded_ini + try: + self.cview.tree.write(xmlPath) + except TypeError: + print("No Config loaded to be saved!") + + if self.loaded_ini != "" and tablePath.split("/")[-1] != "": + QMessageBox.about(self, "Successfully saved!", + "Files have been saved as: " + + self.loaded_ini + ", " + + tablePath.split("/")[-1]) + elif self.loaded_ini != "": + QMessageBox.about(self, "Successfully saved!", + "ini has been saved as: " + + self.loaded_ini) + elif tablePath.split("/")[-1] != "": + QMessageBox.about(self, "Successfully saved!", + "Table has been saved as: " + + tablePath.split("/")[-1]) + + def loadFunction(self, filePath: str=""): + """ + loads all files (.tsv .ini, .fasta) from a given + directory. + If .tsv file is not present the experimental design is + filled with mzMl files + + If no .ini file is present default ini file is written + and is loaded automatically + """ + if not filePath: + dlg = QFileDialog(self) + filePath = dlg.getExistingDirectory() + self.loaded_dir = filePath + self.sview.fillTable(filePath) + if filePath: + try: + self.tsvfiles = glob.glob('*.tsv') + if len(self.tsvfiles) > 1: + QMessageBox.about(self, "Sorry!", + "There are multiple '.tsv-'" + "files in the specified folder. " + "Please choose the one you intent " + "to use.") + dial = QFileDialog(self) + newFilePath = dial.getOpenFileName(self, + "Choose .tsv", + filePath, + "Tables (*.tsv)") + if newFilePath[0] != '': + newFile = newFilePath[0].split("/")[-1] + self.tsvfiles = [newFile] + else: + QMessageBox.about(self, "Sorry!", + "Nothing was choosen. " + "Therefore no '.tsv'-file was " + "loaded. ") + self.tsvfiles = [] + for file in self.tsvfiles: + df = fh.importTable(self.tview, file) + Tdf.setTable(self.tview, df) + self.tview.drawTable() + self.loaded_table = file + self.tablefile_loaded = True + + except TypeError: + "No '.tsv' or '.csv'-file could be loaded." + + if self.tablefile_loaded is False: + try: + self.tview.loadDir(filePath) + self.tablefile_loaded = True + except TypeError: + print("Could not load '.mzMl'-files.") + + try: + self.iniFiles = glob.glob('*.ini') + if len(self.iniFiles) > 1: + QMessageBox.about(self, "Sorry!", + "There are multiple '.ini'-" + "files in the specified folder. " + "Please choose the one you intent " + "to use.") + dial = QFileDialog(self) + newFilePath = dial.getOpenFileName(self, + "Choose .ini", + filePath, + "Config (*.ini)") + if newFilePath[0] != '': + newFile = newFilePath[0].split("/")[-1] + self.iniFiles = [newFile] + else: + QMessageBox.about(self, "Sorry!", + "Nothing was choosen. " + "Therefore no '.ini'-file was " + "loaded. ") + self.iniFiles = [] + for file in self.iniFiles: + self.cview.generateTreeModel(file) + self.loaded_ini = file + self.ini_loaded = True + except TypeError: + print("Could not load .ini file.") + + if self.ini_loaded is False: + try: + runcall = "ProteomicsLFQ " + writeIniFile = "-write_ini " + out = "Config.ini" + command = (runcall + writeIniFile + out) + os.chdir(filePath) + os.system(command) + iniFiles = glob.glob('*.ini') + for file in iniFiles: + self.cview.generateTreeModel(file) + self.loaded_ini = file + self.ini_loaded = True + except TypeError: + print("Could not write and load default '.ini'-file.") + + try: + self.fastafiles = glob.glob('*fasta') + if len(self.fastafiles) > 1: + QMessageBox.about(self, "Sorry!", + "There are multiple '.fasta'-" + "files in the specified folder. " + "Please choose the one you intent " + "to use.") + dial = QFileDialog(self) + newFilePath = dial.getOpenFileName(self, + "Choose .fasta", + filePath, + "Proteindata (*.fasta)") + if newFilePath[0] != '': + newFile = newFilePath[0].split("/")[-1] + self.fastafiles = [newFile] + else: + QMessageBox.about(self, "Sorry!", + "Nothing was choosen. " + "Therefore, no '.fasta'-file " + "was loaded. ") + self.fastafiles = [] + for file in self.fastafiles: + self.fview.loadFile(file) + self.loaded_fasta = file + self.fasta_loaded = True + except TypeError: + print("Could not load '.fasta'-file.") + + def setTheme(self): + """ + Sets theme based on flag state, light or dark modes are possible. + Default is light theme. + """ + p = self.palette + if not self.flag: + # lightmode + p.setColor(QPalette.Window, Qt.white) + p.setColor(QPalette.Background, Qt.white) + p.setColor(QPalette.WindowText, Qt.black) + p.setColor(QPalette.Base, Qt.white) + p.setColor(QPalette.AlternateBase, Qt.white) + p.setColor(QPalette.ToolTipBase, Qt.black) + p.setColor(QPalette.ToolTipText, Qt.black) + p.setColor(QPalette.Text, Qt.black) + p.setColor(QPalette.Button, Qt.white) + p.setColor(QPalette.ButtonText, Qt.black) + p.setColor(QPalette.BrightText, Qt.red) + p.setColor(QPalette.Link, QColor(213, 125, 37)) + p.setColor(QPalette.Highlight, QColor(213, 125, 37)) + p.setColor(QPalette.HighlightedText, Qt.white) + else: + # darkmode + p.setColor(QPalette.Window, QColor(53, 53, 53)) + p.setColor(QPalette.WindowText, Qt.white) + p.setColor(QPalette.Base, QColor(25, 25, 25)) + p.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) + p.setColor(QPalette.ToolTipBase, Qt.white) + p.setColor(QPalette.ToolTipText, Qt.white) + p.setColor(QPalette.Text, Qt.white) + p.setColor(QPalette.Button, QColor(53, 53, 53)) + p.setColor(QPalette.ButtonText, Qt.white) + p.setColor(QPalette.BrightText, Qt.red) + p.setColor(QPalette.Link, QColor(42, 130, 218)) + p.setColor(QPalette.Highlight, QColor(42, 130, 218)) + p.setColor(QPalette.HighlightedText, Qt.black) + self.setPalette(p) + + def dragEnterEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + + if urls and urls[0].scheme() == "file": + e.acceptProposedAction() + else: + e.ignore() + + def dragMoveEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + + if urls and urls[0].scheme() == "file": + e.acceptProposedAction() + else: + e.ignore() + + def dropEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + + if urls and urls[0].scheme() == "file": + #welcome page + if self.view.currentIndex() == 0: + filetype = "directory" + filepath = self.urlHandler(urls[0].path()) + if os.path.isdir(filepath): + self.loadFunction(filepath) + else: + self.displayDragNDropError(filetype) + #xmlviewer + elif self.view.currentIndex() == 1: + files = [self.urlHandler(u.path()) for u in urls] + self.cview.dragDropEvent(files) + #experimental design + elif self.view.currentIndex() == 2: + filetype = ["mzML","tsv","csv"] + filepath = self.urlHandler(urls[0].path()) + if filepath[-4:] == filetype[0]: + self.tview.loadFile(filepath) + elif (filepath[-3:] == filetype[1]) or (filepath[-3:] ==filetype[2]): + self.tview.importBtn(filepath) + else: + self.displayDragNDropError("",filetype) + #fasta viewer + elif self.view.currentIndex() == 3: + filepath = self.urlHandler(urls[0].path()) + filetype = "fasta" + if filepath[-5:] == filetype: + self.fview.loadFile(filepath) + else: + self.displayDragNDropError(filetype) + + #specviewer + elif self.view.currentIndex() == 4: + filetype = "mzML" + filepath = self.urlHandler(urls[0].path()) + if filepath[-4:] == filetype: + self.sview.sview.openFileDialog(filepath) + else: + self.displayDragNDropError(filetype) + else: + e.ignore() + def displayDragNDropError(self, filetype:str, mul:list=[]): + """ + displays an error message in a messagebox detailing what went wrong + """ + message = "" + if not mul: + if filetype == "directory": + message = "a directory to load a project" + else: + message = "'."+filetype +"'-files" + else: + message += "'."+mul[0]+"'" + for file in mul[1:-1]: + message += ", '."+file +"'" + message += " or "+mul[-1] + "'-files" + dialog = QMessageBox() + dialog.setWindowTitle("Error: Invalid File") + dialog.setText("Please only use " + message) + dialog.setIcon(QMessageBox.Warning) + dialog.exec_() + + + def urlHandler(self, url): + opsys = platform.system() + if(opsys == "Linux"): + return str(url) + if(opsys == "Windows"): + return str(url)[1:] + if(opsys == "Darwin"): + return str(url) # to be tested + + def onChange(self): + """ + this function detects if a tab has been changed. + for debugging purposes. + """ + print(self.view.currentIndex()) + + def switchTheme(self): + """ + Toggles between dark and light theme. + """ + + self.flag = not self.flag + self.setTheme() + + +class PopupWindow(QMainWindow): + """ + Popup window with checkboxes for outputfiles. + """ + + def __init__(self): + QMainWindow.__init__(self) + self.resize(300, 100) + self.center() + self.show() + + def center(self): + """ + Centers the widget to the screen. + """ + qr = self.frameGeometry() + cp = QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + def setTitle(self, title: str): + self.setWindowTitle(title) + + +if __name__ == '__main__': + + app = QApplication(sys.argv) + ex = ProteinQuantification() + app.setStyle("Fusion") + sys.exit(app.exec_()) diff --git a/src/apps/SpecViewer.py b/src/apps/SpecViewer.py index cba916291..82459924d 100644 --- a/src/apps/SpecViewer.py +++ b/src/apps/SpecViewer.py @@ -1,5 +1,16 @@ import sys -import pyopenms +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, \ + QHBoxLayout, QWidget, QDesktopWidget, \ + QAction, QFileDialog, QTableView, QSplitter, \ + QMenu, QAbstractItemView, QPushButton +from PyQt5.QtCore import Qt, QAbstractTableModel, pyqtSignal, \ + QItemSelectionModel, QSortFilterProxyModel, QSignalMapper, QPoint, QRegExp + +import pyqtgraph as pg +from pyqtgraph import PlotWidget + +import numpy as np + from collections import namedtuple import pyqtgraph as pg @@ -13,8 +24,9 @@ QFileDialog, ) -sys.path.insert(0, "../view/") -from ScanBrowserWidget import ScanBrowserWidget +sys.path.insert(0, "../view") +from ScanBrowserWidget import ScanBrowserWidget # noqa: E402 + # structure for annotation (here for reference) PeakAnnoStruct = namedtuple( @@ -28,11 +40,12 @@ text_label_list color", ) -pg.setConfigOption("background", "w") # white background -pg.setConfigOption("foreground", "k") # black peaks +pg.setConfigOption('background', 'w') # white background +pg.setConfigOption('foreground', 'k') # black peaks + +class Specviewer(QMainWindow): -class App(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.resize(1000, 700) # window size @@ -42,11 +55,23 @@ def initUI(self): self.setWindowTitle("pyOpenMSViewer") # self.center() - # layout - self.setMainMenu() self.centerWidget = QWidget(self) self.setCentralWidget(self.centerWidget) - self.windowLay = QVBoxLayout(self.centerWidget) + # main + mainLayout = QVBoxLayout(self.centerWidget) + + # buttons + buttons = QVBoxLayout() + loadbtn = QPushButton('Load') + loadbtn.setMaximumWidth(80) + loadbtn.clicked.connect(self.openFileDialog) + buttons.addWidget(loadbtn) + + mainLayout.addLayout(buttons) + + # graph + self.windowLay = QVBoxLayout() + mainLayout.addLayout(self.windowLay) # default widget <- per spectrum self.setScanBrowserWidget() @@ -57,42 +82,43 @@ def setScanBrowserWidget(self): self.scanbrowser = ScanBrowserWidget(self) self.windowLay.addWidget(self.scanbrowser) - def setMainMenu(self): - mainMenu = self.menuBar() - mainMenu.setNativeMenuBar(False) - - self.titleMenu = mainMenu.addMenu("PyOpenMS") - self.fileMenu = mainMenu.addMenu("File") - # helpMenu = mainMenu.addMenu('Help') - self.toolMenu = mainMenu.addMenu("Tools") - - self.setTitleMenu() - self.setFileMenu() - self.setToolMenu() - - def setTitleMenu(self): - self.setExitButton() - - def setFileMenu(self): - # open mzml file - mzmlOpenAct = QAction("Open file", self) - mzmlOpenAct.setShortcut("Ctrl+O") - mzmlOpenAct.setStatusTip("Open new file") - mzmlOpenAct.triggered.connect(self.openFileDialog) - self.fileMenu.addAction(mzmlOpenAct) - - def setToolMenu(self): - # for overriding - return + # def setMainMenu(self): + # mainMenu = self.menuBar() + # mainMenu.setNativeMenuBar(False) + # + # self.titleMenu = mainMenu.addMenu('PyOpenMS') + # self.fileMenu = mainMenu.addMenu('File') + # # helpMenu = mainMenu.addMenu('Help') + # self.toolMenu = mainMenu.addMenu('Tools') + # + # self.setTitleMenu() + # self.setFileMenu() + # self.setToolMenu() + # + # def setTitleMenu(self): + # self.setExitButton() + # + # def setFileMenu(self): + # # open mzml file + # mzmlOpenAct = QAction('Open file', self) + # mzmlOpenAct.setShortcut('Ctrl+O') + # mzmlOpenAct.setStatusTip('Open new file') + # mzmlOpenAct.triggered.connect(self.openFileDialog) + # self.fileMenu.addAction(mzmlOpenAct) + # + # def setToolMenu(self): + # # for overriding + # return def clearLayout(self, layout): for i in reversed(range(layout.count())): layout.itemAt(i).widget().setParent(None) - def openFileDialog(self): - fileName, _ = QFileDialog.getOpenFileName( - self, "Open File ", "", "mzML Files (*.mzML)" - ) + def openFileDialog(self, fileName: str = ""): + if not fileName: + fileName, _ = QFileDialog.getOpenFileName(self, + "Open File ", "", "mzML Files (*.mzML)") + if fileName: print("opening...", fileName) self.setScanBrowserWidget() @@ -122,7 +148,7 @@ def closeEvent(self, event): event.accept() -if __name__ == "__main__": +if __name__ == '__main__': app = QApplication(sys.argv) ex = App() ex.show() diff --git a/src/apps/TableEditor.py b/src/apps/TableEditor.py new file mode 100644 index 000000000..8b383751d --- /dev/null +++ b/src/apps/TableEditor.py @@ -0,0 +1,82 @@ +import sys, os +import timeit +from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, \ + QMessageBox +sys.path.append(os.getcwd()+'/../view') +from mzMLTableView import mzMLTableView # noqa E402 + + +class TableEditor(QMainWindow): + + def __init__(self): + self.testForTime = False + if self.testForTime: + starttime = timeit.default_timer() + print("Starttime of overall Initiation : ", starttime) + QMainWindow.__init__(self) + self.initUI() + self.setAcceptDrops(True) + + def initUI(self): + ''' + sets the window with all applications and widgets + ''' + self.view = mzMLTableView(self) + + self.setCentralWidget(self.view) + + self.resize(1280, 720) + self.center() + self.setWindowTitle('ExperimentalDesign') + self.show() + + def center(self): + """ + centers the widget to the screen + """ + qr = self.frameGeometry() + cp = QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + def dragEnterEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + if urls and urls[0].scheme() == "file": + e.acceptProposedAction() + else: + e.ignore() + + def dragMoveEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + if urls and urls[0].scheme() == "file": + e.acceptProposedAction() + else: + e.ignore() + + def dropEvent(self, event): + e = event + data = e.mimeData() + urls = data.urls() + if urls and urls[0].scheme() == "file": + filepath = str(urls[0].path())[1:] + if filepath[-4:] == "mzML": + self.view.loadFile(filepath) + else: + dialog = QMessageBox() + dialog.setWindowTitle("Error: Invalid File") + dialog.setText("Please only use .mzML files") + dialog.setIcon(QMessageBox.Warning) + dialog.exec_() + else: + e.ignore() + + +if __name__ == '__main__': + + app = QApplication(sys.argv) + ex = TableEditor() + sys.exit(app.exec_()) diff --git a/src/apps/XMLViewer.py b/src/apps/XMLViewer.py new file mode 100644 index 000000000..8a8cac671 --- /dev/null +++ b/src/apps/XMLViewer.py @@ -0,0 +1,56 @@ +import sys +import os +from PyQt5.QtWidgets import QApplication, QMainWindow, \ + QDesktopWidget +sys.path.append(os.getcwd()+'/../view') +from ConfigView import ConfigView # noqa E402 + + +class XMLViewer(QMainWindow): + """ + Widget for visualizing configuration data + """ + + def __init__(self): + QMainWindow.__init__(self) + self.initUI() + self.setAcceptDrops(True) + + def initUI(self): + ''' + sets the window with all applications and widgets + which are loaded from the ConfigView.py file + ''' + self.cview = ConfigView() + + self.setCentralWidget(self.cview) + self.resize(800, 1000) + self.center() + self.setWindowTitle('ini File Viewer') + self.show() + + def center(self): + """ + centers the widget to the screen + """ + qr = self.frameGeometry() + cp = QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + files = [u.path() for u in event.mimeData().urls()] + self.cview.dragDropEvent(files) + + +if __name__ == '__main__': + + app = QApplication(sys.argv) + ex = XMLViewer() + sys.exit(app.exec_()) diff --git a/src/controller/descriptions.py b/src/controller/descriptions.py new file mode 100644 index 000000000..612982377 --- /dev/null +++ b/src/controller/descriptions.py @@ -0,0 +1,90 @@ +import os +import sys +import glob +import pandas as pd +import re + + +class Descriptions: + """ + Provides Descriptions for proteinquantification + """ + def __init__(self, descriptions): + self.descriptions = descriptions + + descriptions = {"welcome": + """ +

Welcome: Get started with your + Proteinquantifcation!

+
This application performs a + ProteomicsLFQ - Proteinquantification.
+ The files, needed to + run the application have to be loaded into +
XML-Viewer, + Experimental-Design and Fasta-Viewer-Tabs, + respectively.
Finally, the mzTab-Viewer-Tab + will show the result. +
+ You can look at the spectra used for the + quantification
in the Spec-Viewer-Tab.
+ """, + "XML-Viewer": + """ +
XML Viewer:
In this + tab you can load, edit + and save your experiments config-file.
+ If no config-file is provided, + a default file will be generated,
+ which you can modify for the quantification.
+ This application only accepts '.ini'-files as + config-files.
+ """, + "Experimental-Design": + """ +
Experimental + Design:
The Experimental-Design-Tab + enables you to load and edit an + experimental design.
+ You can edit specifics like number
+ of files, fractions or groups.
'.tsv' + -files can be directly loaded as a + table.
You can also add single + '.mzml'-files to the table,
either + via drag'n'drop, or with the 'add file' + -button.
You can filter the files + using the textfield in the upper right + corner.
+ """, + "Fasta-Viewer": + """ +
Fasta Viewer:
In + this tab you can load a fasta file
and search + for information about the sequences such + as
protein-IDs and accession-numbers.
+ """, + "Spec-Viewer": + """ +
Spectrum Viewer:
This + tab enables you to look at
the mzML-spectra + your Mass-Spectrometer measured for your +
samples.
It loads all spectra files, + which are + located in your project folder.
+ """, + "mzTab-Viewer": + """ +
MzTab Viewer:
The + results of your analysis is visualized + in the mzTab-Viewer.
+ Information about the identified sequences, such as + retention time,
charge or mass to charge ratio + are listed in the table.
+ """, + } + + def getDescription(self, widgetname:str) -> str: + """ + Scans a provided directory and returns a list of all + the mzML files. + """ + return self.descriptions.widgetname \ No newline at end of file diff --git a/src/controller/filehandler.py b/src/controller/filehandler.py new file mode 100644 index 000000000..f5f030205 --- /dev/null +++ b/src/controller/filehandler.py @@ -0,0 +1,136 @@ +import os +import sys +import glob +import pandas as pd +import re + +files = [] + + +class FileHandler: + """ + Filehandler class that provides support for file and directory interaction. + """ + def __init__(self, path: str, files: list, delimiter: str): + self.path = path + self.files = files + self.delimiter = delimiter + + def getFiles(self, path: str) -> list: + """ + Scans a provided directory and returns a list of all + the mzML files. + """ + os.chdir(path) + files = glob.glob('*.mzML') + return files + + def tagfiles(self, files: list, delimiter: str = "_"): + """ + Parses filenames in a list of files by a provided delimiter. + Default delimiter is _ . + Returns a list of tags. + """ + tagproperty = {} + for file in files: + name = file + tags = [] + file = file.split(".")[0] + tags = file.split(delimiter) + tagproperty[name] = tags + return tagproperty + + def importTable(self, file: str) -> pd.DataFrame: + """ + Imports a csv or tsv file and returns a panda-dataframe. + Returns false if filetype is not tsv or csv. + """ + ftype = file.split(".")[1] + + if ftype == "csv": + fimport = pd.read_csv(file) + + elif ftype == "tsv": + fimport = pd.read_csv(file, sep='\t') + else: + return False + return fimport + + def exportTable(self, table: pd.DataFrame, + filename: str, ftype: str = 'csv') -> bool: + """ + Exports a panda-dataframe to the specified filetype. + builds a path from a provided filename, a provided path, + and the provided filetype. + + Defaults: + If no path is specified, default is the current working directory. + If no filetype is specified, default is csv. + + Returns: + Returns false if specified filetype is not supported. + Returns true if file was successfully written. + Returns an error if writing the file failed. + """ + + fullpath = filename + encodingOption = 'utf-8' + if ftype == 'tsv': + separator = "\t" + elif ftype == 'csv': + separator = "," + else: + return False + try: + with open(fullpath, 'w') as fileToWrite: + fileToWrite.write(table.to_csv( + sep=separator, index=False, encoding=encodingOption)) + return True + except IOError: + e = sys.exc_info()[0] + return e + + def createRawTable(self, tagdict: dict, inputdir: str): + """ + Creates pandas-dataframe from prepared files dictionary, which + contains the filename and its tags (delimiter is _). Dataframes index + is integer based, header are the column names. Searches the + tags for regular expressions to automatically include fraction and + fraction group from filename. + Default regex: + Fraction-Group: FG or G + Fraction: F + """ + columnregex = ["FG[0-9]+", "G[0-9]+", "F[0-9]+"] + header = ['Fraction_Group', 'Fraction', + 'Spectra_Filepath', 'Label', 'Sample'] + index = [] + rows = [] + filenames = tagdict.keys() + for file in filenames: + index.append(file) + filtered_tags = {'Fraction_Group': 0, 'Fraction': 0, + 'Spectra_Filepath': str(inputdir + "/" + file), + 'Label': 0, 'Sample': 0} + filetags = tagdict[file] + for tag in filetags: + if re.match(columnregex[0], tag): + filtered_tags['Fraction_Group'] = tag.split('FG')[1] + elif re.match(columnregex[1], tag): + filtered_tags['Fraction_Group'] = tag.split('G')[1] + elif re.match(columnregex[2], tag): + filtered_tags['Fraction'] = tag.split('F')[1] + else: + continue + rows.append([filtered_tags[i]for i in header]) + rawtable = pd.DataFrame(rows, columns=header) + return rawtable + + +# just for testing +# i = input("filepath: ") +# delimiters = ["_"] +# files = getFiles(i) +# preparedfiles = tagfiles(files, delimiters[0]) +# rawtable = createRawTable(preparedfiles, i) +# print(rawtable) diff --git a/src/examples/GUI_ErrorWidget.py b/src/examples/GUI_ErrorWidget.py index 2361cb33e..8f142cfa6 100644 --- a/src/examples/GUI_ErrorWidget.py +++ b/src/examples/GUI_ErrorWidget.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from ErrorWidget import ErrorWidget +from ErrorWidget import ErrorWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_MS1MapWidget.py b/src/examples/GUI_MS1MapWidget.py index 9c631c2b2..273c81e26 100644 --- a/src/examples/GUI_MS1MapWidget.py +++ b/src/examples/GUI_MS1MapWidget.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from MS1MapWidget import MS1MapWidget +from MS1MapWidget import MS1MapWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_ScanBrowserWidget.py b/src/examples/GUI_ScanBrowserWidget.py index 723cb7d8b..55e3f634a 100644 --- a/src/examples/GUI_ScanBrowserWidget.py +++ b/src/examples/GUI_ScanBrowserWidget.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from ScanBrowserWidget import ScanBrowserWidget +from ScanBrowserWidget import ScanBrowserWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_ScanTableWidget.py b/src/examples/GUI_ScanTableWidget.py index 10046746d..6b698754f 100644 --- a/src/examples/GUI_ScanTableWidget.py +++ b/src/examples/GUI_ScanTableWidget.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from ScanTableWidget import ScanTableWidget +from ScanTableWidget import ScanTableWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_SequenceIonsWidget.py b/src/examples/GUI_SequenceIonsWidget.py index 4c363227a..1885211d0 100644 --- a/src/examples/GUI_SequenceIonsWidget.py +++ b/src/examples/GUI_SequenceIonsWidget.py @@ -2,7 +2,7 @@ from GUI_EXAMPLE_BASE import GUI_EXAMPLE_BASE from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from SequenceIonsWidget import SequenceIonsWidget +from SequenceIonsWidget import SequenceIonsWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_SpectrumWidget.py b/src/examples/GUI_SpectrumWidget.py index 4fed25a2e..807562ef1 100644 --- a/src/examples/GUI_SpectrumWidget.py +++ b/src/examples/GUI_SpectrumWidget.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from SpectrumWidget import SpectrumWidget +from SpectrumWidget import SpectrumWidget # noqa: E402 if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/src/examples/GUI_TICWidget.py b/src/examples/GUI_TICWidget.py index 2bfdc657f..b7f056f1e 100644 --- a/src/examples/GUI_TICWidget.py +++ b/src/examples/GUI_TICWidget.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QApplication sys.path.insert(0, "../view") -from TICWidget import TICWidget +from TICWidget import TICWidget # noqa: E402 # mock object to test the mouse click signal by TICWidget diff --git a/src/examples/PhosphoScoring.py b/src/examples/PhosphoScoring.py index b1a951ed7..dc0e88d05 100644 --- a/src/examples/PhosphoScoring.py +++ b/src/examples/PhosphoScoring.py @@ -29,7 +29,6 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import re - import pyopenms @@ -124,7 +123,7 @@ def simplisticBinnedScoring(self, phit, spectrum): possibilities.append([comp_score, new_aaseq]) # Sort the result by score, return the best scoring result - possibilities.sort(lambda x, y: -cmp(x[0], y[0])) + possibilities.sort(lambda x, y: -cmp(x[0], y[0])) # noqa return possibilities[0] def compare_binnedSpectra(self, sp1, sp2): diff --git a/src/examples/data/OpenPepXLLF_input2.ini b/src/examples/data/OpenPepXLLF_input2.ini new file mode 100644 index 000000000..34e89df0a --- /dev/null +++ b/src/examples/data/OpenPepXLLF_input2.ini @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/model/tableDataFrame.py b/src/model/tableDataFrame.py new file mode 100644 index 000000000..83b71106f --- /dev/null +++ b/src/model/tableDataFrame.py @@ -0,0 +1,103 @@ +import pandas as pd + + +class TableDataFrame(): + + def __init__(self, df: pd.DataFrame): + self.df = df + + def getTable(self) -> pd.DataFrame: + """ + Returns the current dataframe + """ + return self.df + + def setTable(self, df: pd.DataFrame): + """ + Sets the dataframe to the given + """ + self.df = df + + def modifyGroup(self, rows: list, groupnum: int): + """ + Let the user set the group for a list of selected rows. + Needs a list of selected rows and the integer to which + the group is set. + """ + for row in rows: + self.df.at[row, 'Fraction_Group'] = groupnum + + def modifyFraction(self, rows: list, *argv: int): + """ + Let the user set the fraction for a list of selected rows. + when only one number is given in argv one fraction is set + to all selected entries, else it will count to from min to max + and sets the group accordingly. + Takes a list of selected rows and the number(s) how the fractions + should be set. + """ + if len(argv) == 1: + for row in rows: + self.df.at[row, 'Fraction'] = argv[0] + elif len(argv) == 2: + count = 0 + for row in rows: + fracnum = argv[0] + count + self.df.at[row, 'Fraction'] = fracnum + if fracnum >= argv[1]: + count = 0 + else: + count += 1 + + def modifyLabelSample(self, labelnum: int, continuous: bool): + """ + Let the user change the multiplicity of the selected rows + continuous should be boolean if true samplenumber counts + through for all fraction groups otherwise it will start at + 1 for each fraction group + Takes the number of labels and a boolean, which sets the option + of continuing the samplenumber over groups. + """ + # generate new dataframe from rows and copied rows of labels + ndf = [] + for row in self.df.index: + for cl in range(labelnum): + label = cl+1 + self.df.at[row, 'Label'] = label + dfrow = self.df.loc[[row]] + ndf.append(dfrow) + ndf = pd.concat(ndf, ignore_index=True) + + # associate the samplenum to the label + if continuous: + prevsample = 0 + for row in ndf.index: + if (row == 0): + samplenum = 1 + ndf.at[row, 'Sample'] = samplenum + elif(ndf.at[row, 'Fraction_Group'] != + ndf.at[row-1, 'Fraction_Group']): + prevsample = ndf.at[row-1, 'Sample'] + samplenum = ndf.at[row, 'Label'] + prevsample + ndf.at[row, 'Sample'] = samplenum + else: + samplenum = ndf.at[row, 'Label'] + prevsample + ndf.at[row, 'Sample'] = samplenum + else: + for row in ndf.index: + samplenum = ndf.at[row, 'Label'] + ndf.at[row, 'Sample'] = samplenum + + self.df = ndf + + def rmvRow(self, rows: list): + """ + Let the user remove a list of rows. + Takes list of selected rows. + """ + self.df.drop(rows, inplace=True) + self.df.reset_index(drop=True, inplace=True) + + def modifyField(self, row: int, column: int, newvalue: int): + col = self.df.columns[column] + self.df.at[row, col] = newvalue diff --git a/src/view/ConfigView.py b/src/view/ConfigView.py new file mode 100644 index 000000000..c434bae1d --- /dev/null +++ b/src/view/ConfigView.py @@ -0,0 +1,394 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTreeWidget, \ + QTreeWidgetItem, QFileDialog, QPushButton, QHBoxLayout, \ + QPlainTextEdit, QCheckBox, QHeaderView, QMessageBox, \ + QInputDialog +from PyQt5.QtCore import Qt +import xml.etree.ElementTree as ET +from defusedxml.ElementTree import parse +from functools import partial + + +class ConfigView(QWidget): + def __init__(self, *args): + QWidget.__init__(self, *args) + self.initTreeModel() + self.initTreeWidget() + self.initUI() + + def initTreeModel(self): + """ + Initialise the tree model using a fixed header + Columns are defined as constants + """ + self.tree = ET.ElementTree + self.header = ['Name', 'Value', 'Type', 'Restrictions'] + self.NAMECOL = 0 + self.VALUECOL = 1 + self.TYPECOL = 2 + self.RESTRICTIONCOL = 3 + self.descriptions = {} + self.drawTree = False + + def initTreeWidget(self): + """ + Initialise the TreeWidget, with corresponding header + A change listener is implemented and initialised here too + """ + self.treeWidget = QTreeWidget(self) + self.treeWidget.setHeaderLabels(self.header) + + self.header = self.treeWidget.header() + self.header.resizeSection(0, 150) + self.header.resizeSection(1, 150) + self.header.resizeSection(2, 150) + + self.treeWidget.itemSelectionChanged.connect(self.loadDescription) + self.changeListener() + + def initUI(self): + """ + Initialise the GUI with buttons, checkbox, textbox and treewidget + """ + self.loadbtn = QPushButton('Load') + self.loadbtn.setMaximumWidth(80) + self.loadbtn.clicked.connect(self.openXML) + self.loadbtn.setToolTip("Load a .ini file to display and " + + "modify the configuration of your processing.") + + self.savebtn = QPushButton('Save') + self.savebtn.setMaximumWidth(80) + self.savebtn.clicked.connect(self.saveFile) + self.savebtn.setToolTip("Save the modified " + + "configuration as .ini file.") + + self.checkbox = QCheckBox('Show advanced parameters') + self.checkbox.setChecked(True) + self.checkbox.stateChanged.connect(self.drawTreeInit) + self.checkbox.setToolTip("Shows or hides parameters, " + + "which are tagged as advance in " + + "the .ini configuration file.") + + self.textbox = QPlainTextEdit(self) + self.textbox.setReadOnly(True) + + btnlayout = QHBoxLayout() + layout = QVBoxLayout() + + btns = QWidget(self) + btnlayout.addWidget(self.loadbtn) + btnlayout.addWidget(self.savebtn) + btnlayout.addWidget(self.checkbox) + btns.setLayout(btnlayout) + btns.setFixedWidth(500) + + layout.addWidget(self.treeWidget, 6) + layout.addWidget(self.textbox, 1) + layout.addWidget(btns, 0.5) + + self.setLayout(layout) + self.resize(500, 720) + + def dragDropEvent(self, files: list): + """ + Gets the input from the main Application. + The function chooses if the dragged files are + valid for the ConfigView Widget + """ + if len(files) > 1: + QMessageBox.about(self, "Warning", + "Please only use one file") + else: + if ".ini" not in files[0]: + QMessageBox.about(self, "Warning", + "Please only use .ini files.") + else: + file = str(files[0]) + self.generateTreeModel(file) + + def openXML(self): + """ + Loads a XML file with .ini tag, parses the xml into ET.ElementTree + calls the drawTree function to draw a Tree with the loaded xml + """ + file, _ = QFileDialog.getOpenFileName( + self, "QFileDialog.getOpenFileName()", "", + "All Files (*);;ini (*.ini)") + if file: + self.generateTreeModel(file) + + def generateTreeModel(self, file: str): + """ + Function to parse the xml .ini file to a tree model + Also initialises the TreeWidget + """ + self.tree = parse(file) + self.root = self.tree.getroot() + self.drawTreeInit() + self.header.setSectionResizeMode(QHeaderView.ResizeToContents) + + def generateTreeWidgetItem(self, item: ET.Element) -> QTreeWidgetItem: + """ + generates a QTreeWidgetItem with each column for an + ET.Element (e.g. root) + """ + treeitem = QTreeWidgetItem() + treeitem.setFlags(treeitem.flags() | Qt.ItemIsEditable) + try: + treeitem.setText(0, item.attrib['name']) + except KeyError: + pass + try: + treeitem.setText(1, item.attrib['value']) + except KeyError: + pass + try: + treeitem.setText(2, item.attrib['type']) + except KeyError: + pass + try: + treeitem.setText(3, item.attrib['restrictions']) + except KeyError: + pass + try: + self.descriptions[item.attrib['name']] = item.attrib['description'] + except KeyError: + pass + + return treeitem + + def drawTreeInit(self): + """ + Initialises the treewidget, add the top level item + and starts the main recursion + """ + self.drawTreeActive = True + self.additembtns = {} + self.treeWidget.clear() + root = self.tree.getroot() + for child in root: + if self.checkbox.isChecked(): + childitem = self.generateTreeWidgetItem(child) + self.treeWidget.addTopLevelItem(childitem) + if len(child.getchildren()) > 0: + self.drawTreeRecursive(childitem, child) + else: + try: + if child.attrib['advanced'] == 'false': + childitem = self.generateTreeWidgetItem(child) + self.treeWidget.addTopLevelItem(childitem) + if len(child.getchildren()) > 0: + self.drawTreeRecursive(childitem, child) + except KeyError: + childitem = self.generateTreeWidgetItem(child) + self.treeWidget.addTopLevelItem(childitem) + if len(child.getchildren()) > 0: + self.drawTreeRecursive(childitem, child) + self.treeWidget.expandAll() + self.header.setSectionResizeMode(QHeaderView.ResizeToContents) + for btn in self.additembtns.keys(): + self.additembtns[btn].clicked.connect( + partial(self.addItemToItemList, btn)) + self.drawTreeActive = False + + def drawTreeRecursive(self, nodeitem: QTreeWidgetItem, node: ET.Element): + """ + Draws a tree for the loaded XML file + The checkbox "show advanced options" is implemented here as well + it will only draw those items, which have the according advanced flag + """ + for subnode in node: + if self.checkbox.isChecked(): + subitem = self.generateTreeWidgetItem(subnode) + nodeitem.addChild(subitem) + if len(subnode.getchildren()) > 0: + self.drawTreeRecursive(subitem, subnode) + else: + try: + if subnode.attrib['advanced'] == 'false': + subitem = self.generateTreeWidgetItem(subnode) + nodeitem.addChild(subitem) + if len(subnode.getchildren()) > 0: + self.drawTreeRecursive(subitem, subnode) + except KeyError: + subitem = self.generateTreeWidgetItem(subnode) + nodeitem.addChild(subitem) + if len(subnode.getchildren()) > 0: + self.drawTreeRecursive(subitem, subnode) + + if subnode.tag == "ITEMLIST": + newbtn = QPushButton('Add New') + newbtn.setFixedSize(100, 20) + newbtn.setToolTip("Add new Item to the Itemlist, " + + "according to type and restrictions.") + listname = subnode.attrib['name'] + self.additembtns[listname] = newbtn + self.treeWidget.setItemWidget(subitem, 1, + self.additembtns[listname]) + + def loadDescription(self): + """ + Shows the description of the configuration parameter in the textbox + """ + getSelected = self.treeWidget.selectedItems() + if getSelected: + try: + node = getSelected[0].text(0) + self.textbox.setPlainText(self.descriptions[node]) + except KeyError: + node = getSelected[0].parent().text(0) + self.textbox.setPlainText(self.descriptions[node]) + + def changeListener(self): + """ + Change Listener for the tree widget + to connect the model with the widget + """ + self.treeWidget.itemChanged.connect(self.editField) + + def editField(self): + """ + Fields in model are edited upon change of treewidget + Edit is only allowed if restrictions are checked and type is correct + """ + if not self.drawTreeActive: + if self.treeWidget.currentColumn() == self.VALUECOL: + itemchanged = self.treeWidget.currentItem() + itemparent = itemchanged.parent() + changeditemindex = itemparent.indexOfChild(itemchanged) + parentname = itemparent.text(self.NAMECOL) + parentres = itemparent.text(self.RESTRICTIONCOL) + parenttype = itemparent.text(self.TYPECOL) + itemname = itemchanged.text(self.NAMECOL) + newvalue = itemchanged.text(self.VALUECOL) + restrictions = itemchanged.text(self.RESTRICTIONCOL) + types = itemchanged.text(self.TYPECOL) + + parentcheck = False + for itemlist in self.tree.iter('ITEMLIST'): + if str(itemlist.attrib['name']) == str(parentname): + parentcheck = True + + if parentcheck: + reschecked = self.checkRestrictionString( + newvalue, parentres) + typechecked = self.checkTypeRestrictions( + newvalue, parenttype) + else: + reschecked = self.checkRestrictionString( + newvalue, restrictions) + typechecked = self.checkTypeRestrictions( + newvalue, types) + + if reschecked and typechecked: + for parent in self.tree.iter('NODE'): + if parent.attrib['name'] == parentname: + for child in parent: + if child.attrib['name'] == itemname: + child.attrib['value'] = newvalue + for parent in self.tree.iter('ITEMLIST'): + if parent.attrib['name'] == parentname: + for child, childindex in zip(parent, + range(len(parent))): + if childindex == changeditemindex: + child.attrib['value'] = newvalue + elif typechecked and not reschecked: + QMessageBox.about(self, "Warning", "Please only, " + + "modify according to Restrictions") + else: + QMessageBox.about(self, "Warning", "Please only, " + + "modify according to Typerestrictions") + else: + QMessageBox.about(self, "Warning", "Please only, " + + "modify the Column: value") + + self.drawTreeInit() + + def checkRestrictionString(self, + newvalue: type, restrictions: str) -> bool: + """ + Checks the restrictions of a item are matched when edited + """ + if restrictions != "": + if newvalue not in restrictions: + reschecked = False + else: + reschecked = True + else: + reschecked = True + + return reschecked + + def checkTypeRestrictions(self, newvalue: type, types: str) -> bool: + """ + Checks if the edit is corresponding to the type + """ + if types != "": + if "-file" in types: + typechecked = True + else: + try: + float(newvalue) + if len(newvalue.split('.')) == 2: + valtype = "double" + else: + valtype = "int" + except ValueError: + valtype = "string" + if valtype not in types: + typechecked = False + else: + typechecked = True + else: + typechecked = True + + return typechecked + + def addItemToItemList(self, parentnodename: str): + """ + Adds new Item to a ItemList parent, both in etree model and QTreeWidget + """ + newdata, ok = QInputDialog.getText(self, "Add new row to List", + "Please input the new Parameter," + + "which should be added.") + if ok: + if newdata != "": + for itemlist in self.tree.iter('ITEMLIST'): + if itemlist.attrib['name'] == parentnodename: + try: + restrictions = itemlist.attrib['restrictions'] + reschecked = self.checkRestrictionString( + newdata, restrictions) + except KeyError: + reschecked = True + try: + types = itemlist.attrib['type'] + typechecked = self.checkTypeRestrictions( + newdata, types) + except KeyError: + typechecked = True + if reschecked and typechecked: + newelement = ET.Element( + "LISTITEM", {'value': newdata}) + itemlist.append(newelement) + elif typechecked and not reschecked: + QMessageBox.about( + self, "Warning", "Please only, " + + "modify according to Restrictions") + else: + QMessageBox.about( + self, "Warning", "Please only, " + + "modify according to Typerestrictions") + self.drawTreeInit() + + def saveFile(self): + """ + Saves current tree model as .ini file + """ + file, _ = QFileDialog.getSaveFileName( + self, "QFileDialog.getSaveFileName()", "", + "All Files (*);;ini (*.ini)") + if file: + temp = file.split(".") + if len(temp) < 2: + file = file + ".ini" + self.tree.write(file) diff --git a/src/view/MultipleSpecView.py b/src/view/MultipleSpecView.py new file mode 100644 index 000000000..d7e5a35ce --- /dev/null +++ b/src/view/MultipleSpecView.py @@ -0,0 +1,57 @@ +import os +import sys +import glob +from PyQt5.QtWidgets import QWidget, QTableWidget, \ + QTableWidgetItem, QVBoxLayout, QHeaderView +sys.path.append(os.getcwd()+'/../view') +from SpecViewer import Specviewer # noqa E402 + + +class MultipleSpecView(QWidget): + """ + Displays the SpecviewerWidget and additionally a + table containing all files from working dir + """ + + def __init__(self, *args): + QWidget.__init__(self, *args) + self.table = QTableWidget() + self.sview = Specviewer() + self.table.setRowCount(0) + self.table.setSortingEnabled(True) + self.table.setColumnCount(1) + self.header = ['Filename'] + self.table.setHorizontalHeaderLabels(self.header) + self.header = self.table.horizontalHeader() + self.header.setSectionResizeMode(0, QHeaderView.Stretch) + self.table.selectionModel().selectionChanged.connect(self.loadmzML) + + layout = QVBoxLayout() + layout.addWidget(self.sview, 10) + layout.addWidget(self.table, 4) + + self.setLayout(layout) + self.resize(1280, 720) + + def fillTable(self, projectdir: str): + """ + Uses the project directory to access all mzML files + Loads all mzML files in the table + """ + directory = projectdir + os.chdir(directory) + files = glob.glob('*.mzML') + numfiles = len(files) + self.table.setRowCount(numfiles) + if numfiles > 0: + for file, row in zip(files, range(numfiles)): + self.table.setItem(row, 0, QTableWidgetItem(file)) + + def loadmzML(self): + """ + By selection of a mzML file the specviewer updates his file + """ + selected = self.table.selectedItems()[0] + selectedFile = selected.text() + self.sview.setScanBrowserWidget() + self.sview.scanbrowser.loadFile(selectedFile) diff --git a/src/view/SpectrumWidget.py b/src/view/SpectrumWidget.py index 7dfac8bba..4be20ec33 100644 --- a/src/view/SpectrumWidget.py +++ b/src/view/SpectrumWidget.py @@ -194,6 +194,7 @@ def _clear_ladder_item(self, key): anno.clear() for pos in self._ladder_anno_labels[key]: pos.setPos(0, 0) + del self._ladder_anno_lines[key] del self._ladder_anno_labels[key] diff --git a/src/view/mzMLTableView.py b/src/view/mzMLTableView.py new file mode 100644 index 000000000..9b0c99a6a --- /dev/null +++ b/src/view/mzMLTableView.py @@ -0,0 +1,468 @@ +import os +import sys +import timeit +import pandas as pd +import math +from PyQt5 import Qt +from PyQt5.QtWidgets import QHBoxLayout, QWidget, QFileDialog, \ + QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, \ + QVBoxLayout, QInputDialog, QLineEdit, QMessageBox, \ + QAbstractItemView +sys.path.append(os.getcwd() + '/../controller') +from filehandler import FileHandler as fh # noqa E402 +sys.path.append(os.getcwd() + '/../model') +from tableDataFrame import TableDataFrame as Tdf # noqa E402 + + +class mzMLTableView(QWidget): + """ + Main Widget of the TableEditor app + """ + + def __init__(self, *args): + # set variable self.testForTime to True to see Runtimes + # the following 2 if constructs can be used to determine + # timing + # just put them around whatever should be timed + + # self.testForTime = False + # if self.testForTime: + # starttime = timeit.default_timer() + # print("Starttime of overall Initiation : ", starttime) + + # if self.testForTime: + # rt = timeit.default_timer() - starttime + # print("Runtime of overall Initiation was : ", rt) + + QWidget.__init__(self, *args) + + self.df = pd.DataFrame() + self.tdf = Tdf + self.drawtableactive = False + + self.initTable() + self.initButtons() + self.changeListener() + + """ + Layout for the entire View + """ + layout = QVBoxLayout() + layout.addWidget(self.buttons) + layout.addWidget(self.table) + + self.setLayout(layout) + self.resize(1280, 720) + + def initTable(self): + """ + initializes Table + """ + self.tablefile_loaded = False + self.loaded_table = "" + self.table = QTableWidget() + self.table.setRowCount(0) + self.table.setSortingEnabled(True) + self.header = ['Group', 'Fraction', + 'Spectra Filepath', 'Label', 'Sample'] + self.table.setColumnCount(len(self.header)) + self.table.setHorizontalHeaderLabels(self.header) + self.header = self.table.horizontalHeader() + + for col in range(len(self.header)): + if col != 2: + self.header.setSectionResizeMode(col, + QHeaderView.ResizeToContents) + else: + self.header.setSectionResizeMode(col, QHeaderView.Stretch) + + def initButtons(self): + """ + initializes Buttons + """ + self.buttons = QWidget() + self.textbox = QLineEdit(self) + self.textbox.move(20, 20) + self.textbox.setFixedHeight(20) + self.textbox.setToolTip("Filter the experimental layout " + + "according to Spectra Filepath " + + "column. It will be dynamically \n" + + "updated as soon as 2 characters " + + "are inserted.") + + Buttons = [QPushButton('Load Project'), QPushButton('Load Table'), + QPushButton('Save Table'), QPushButton('Add File'), + QPushButton('Remove File'), QPushButton('Group'), + QPushButton('Fraction'), QPushButton('Label'), + QPushButton('Select All')] + + # Buttonlayout + buttonlayout = QHBoxLayout() + for button in Buttons: + buttonlayout.addWidget(button) + + buttonlayout.addWidget(self.textbox) + self.buttons.setLayout(buttonlayout) + + # Connections for Buttons and their apropriate functions + Buttons[0].clicked.connect(self.loadBtnFn) + Buttons[0].setToolTip("Load a directory with .mzML files to " + + "generate your own eperimental layout. " + + "For mzML filenames, \"F\" is the regular \n" + + "expression for fraction, while \"G\" or " + + "\"FG\"is the regular expression for the " + + "fraction groups.") + Buttons[1].clicked.connect(self.importBtn) + Buttons[1].setToolTip("Load an existing experimental layout, as " + + ".csv or .tsv to display and modify it.") + Buttons[2].clicked.connect(self.exportBtn) + Buttons[2].setToolTip("Save the experimental layout as .csv or " + + ".tsv file. .csv is the default option") + Buttons[3].clicked.connect(self.loadFile) + Buttons[3].setToolTip("Load an additional single .mzML file " + + "to the experimental layout.") + Buttons[4].clicked.connect(self.RemoveBtn) + Buttons[4].setToolTip("Remove one or more selected .mzML " + + "files from the experimental layout.") + Buttons[5].clicked.connect(self.GroupBtn) + Buttons[5].setToolTip("Set the fraction group of selected rows " + + "to a given number.") + Buttons[6].clicked.connect(self.FractionBtn) + Buttons[6].setToolTip("Set the fraction of selected rows to a " + + "specific number or use a range to define " + + "multiple fractions. This function is \n" + + "also able to work over multiple fraction " + + "groups and sets the group according to the " + + "fraction number.") + Buttons[7].clicked.connect(self.LabelBtn) + Buttons[7].setToolTip("Set the number of labels, the program will " + + "generate the necessary rows and will also " + + "define the samplenumber for you. You can \n" + + "apply the option to continue samplenumbers " + + "over mutliple fraction groups to combine " + + "two sample preparations.") + Buttons[8].clicked.connect(self.SelectAllBtn) + + # init changelistener on textbox + self.textbox.textChanged[str].connect(self.filterTable) + + def getDataFrame(self): + return self.tdf.getTable(self) + + def drawTable(self): + """ + draws a table with the dataframe table model in tableDataFrame + """ + self.drawtableactive = True + + tabledf = Tdf.getTable(self) + # print(tabledf) # For debugging + rowcount = len(tabledf.index) + colcount = len(tabledf.columns) + self.table.setRowCount(rowcount) + for r in range(rowcount): + row = tabledf.index[r] + for c in range(colcount): + col = tabledf.columns[c] + if col == 'Spectra_Filepath': + path = tabledf.at[row, col].split("/") + name = path[len(path)-1] + self.table.setItem(r, c, QTableWidgetItem(name)) + else: + item = str(tabledf.at[row, col]) + self.table.setItem(r, c, QTableWidgetItem(item)) + + self.drawtableactive = False + + def importBtn(self, file: str = ""): + """ + Imports table files, currently working are csv and tsv + """ + options = QFileDialog.Options() + if not file: + file, _ = QFileDialog.getOpenFileName( + self, "QFileDialog.getOpenFileName()", "", + "All Files (*);;tsv (*.tsv);; csv (*.csv)", options=options) + + if file: + df = fh.importTable(self, file) + Tdf.setTable(self, df) + self.drawTable() + self.tablefile_loaded = True + file = file.split("/")[-1] + self.loaded_table = file + + def exportBtn(self): + """ + Exports the table to csv or tsv;default is csv + """ + options = QFileDialog.Options() + file, _ = QFileDialog.getSaveFileName( + self, "QFileDialog.getSaveFileName()", "", + "All Files (*);;tsv (*.tsv);; csv (*.csv)", options=options) + + if file: + self.tablefile_loaded = True + fpath = file.split("/")[-1] + self.loaded_table = fpath + df = Tdf.getTable(self) + temp = file.split("/") + fileName = temp[len(temp)-1] + length = len(fileName) + if length < 4: + ftype = "csv" + file = file + ".csv" + elif fileName.find('.csv', length-4) != -1: + ftype = "csv" + elif fileName.find('.tsv', length-4) != -1: + ftype = "tsv" + else: + ftype = "csv" + file = file + ".csv" + + fh.exportTable(self, df, file, ftype) + + def loadBtnFn(self): + """ + provides a dialog to get the path for a directory + and load the directory into the table. + """ + dlg = QFileDialog(self) + filePath = dlg.getExistingDirectory() + + if filePath != '': + self.loadDir(filePath) + + def loadDir(self, filepath: str): + Files = fh.getFiles(self, filepath) + delimiters = ["_"] + preparedFiles = fh.tagfiles(self, Files, delimiters[0]) + rawTable = fh.createRawTable(self, preparedFiles, filepath) + Tdf.setTable(self, rawTable) + self.drawTable() + + def loadFile(self, file: str = ""): + """ + provides a filedialog to load an additional file to the dataframe + """ + options = QFileDialog.Options() + if not file: + file, _ = QFileDialog.getOpenFileName( + self, "QFileDialog.getOpenFileName()", "", + "All Files (*);;mzML Files (*.mzML)", options=options) + + if file: + cdf = Tdf.getTable(self) + filelist = [] + filePath = file.rsplit("/", 1)[0] + temp = file.split("/") + fileName = temp[len(temp)-1] + if file: + # print(file) + filelist.append(fileName) + tagged_file = fh.tagfiles(self, filelist) + df = fh.createRawTable(self, tagged_file, filePath) + + ndf = cdf.append(df, ignore_index=True) + + Tdf.setTable(self, ndf) + self.drawTable() + else: + return False + + def getSelRows(self) -> list: + """ + Function which returns a list of the Indexes of selected Rows + todo: needs to be adjusted to fit the datamodel: + so far index of table is only matching index of dataframe + in first iteration, as soon as remove is called twice it crashes. + """ + selindexes = self.table.selectionModel().selectedRows() + selrows = [] + for index in sorted(selindexes): + row = index.row() + selrows.append(row) + return selrows + + def GroupBtn(self): + """ + Enables the user to change the group of selected rows to a given + number. + """ + selrows = self.getSelRows() + + groupnum, ok = QInputDialog.getInt(self, + "Group Number", + "Enter Integer Groupnumber") + + if ok: + Tdf.modifyGroup(self, selrows, groupnum) + self.drawTable() + + def RemoveBtn(self): + """ + Enables the user to remove selected rows + """ + selrows = self.getSelRows() + Tdf.rmvRow(self, selrows) + self.drawTable() + + def FractionBtn(self): + """ + Enables the user to change the Fraction of selected rows to a given + number or give a range. + """ + selrows = self.getSelRows() + + # first inputdialog + fracmin, ok = QInputDialog.getInt(self, + "Fraction", + "Enter minimal Fractionnumber " + + "or single Fractionnumber") + # second inputdialog if first is accepted + if ok: + fracmax, ok = QInputDialog.getInt(self, + "Fraction", + "Enter maximal Fractionnumber " + + "or 0 for single Fractionnumber") + if ok: + # decision if multiple fractions are set or just one + if fracmax != 0: + if fracmax > fracmin: + # third messagedialog + rep = QMessageBox.question(self, "Fraction Group?", + "Do you want to infer a " + + "Fraction Group from the " + + "given range?", + (QMessageBox.Yes | + QMessageBox.No), + QMessageBox.No) + + # when confirmed the fraction froup is set + # when max fraction is reached. + if rep == QMessageBox.Yes: + Tdf.modifyFraction(self, selrows, fracmin, fracmax) + fractions = fracmax-fracmin + 1 + numgroups = math.ceil(len(selrows)/fractions) + splicelist = [0] + for g in range(1, numgroups+1): + splicelist.append(g*fractions) + splicelist.append(len(selrows)) + for group in range(1, numgroups+1): + indexa = splicelist[group-1] + indexb = splicelist[group] + subrows = selrows[indexa:indexb] + Tdf.modifyGroup(self, subrows, group) + else: + Tdf.modifyFraction(self, selrows, fracmin, fracmax) + + elif fracmax == fracmin: + Tdf.modifyFraction(self, selrows, fracmin) + + else: + QMessageBox.warning(self, "Error", "Please use " + + "a higher integer " + + "number for the maximum " + + "fractionnumber.") + + else: + Tdf.modifyFraction(self, selrows, fracmin) + self.drawTable() + + def LabelBtn(self): + """ + Let the user choose the number of labels, it will generate + the labels for the copied rows and also links the sample to + the label. Gives an option to continue the samplecount + over fraction groups. + """ + labelnum, ok = QInputDialog.getInt(self, "Label", + "Please specify the multiplicity " + + "of the selected rows") + if ok: + rep = QMessageBox.question(self, "Continuous Sample", + "Does the samplenumber " + + "continue over multiple " + + "fraction groups?", + (QMessageBox.Yes | + QMessageBox.No), + QMessageBox.No) + if rep == QMessageBox.Yes: + try: + Tdf.modifyLabelSample(self, labelnum, True) + except ValueError: + QMessageBox.about(self, "Warning", "Unfortunaly, " + + "your Number was <1") + else: + try: + Tdf.modifyLabelSample(self, labelnum, False) + except ValueError: + QMessageBox.about(self, "Warning", "Unfortunaly, " + + "your Number was <1") + self.drawTable() + + def SelectAllBtn(self): + """ + Selects all Rows of the Table + """ + self.table.setSelectionMode(QAbstractItemView.MultiSelection) + + for i in range(self.table.rowCount()): + selected = self.getSelRows() + + for j in range(len(selected)): + if i == selected[j]: + self.table.selectRow(i) + + self.table.selectRow(i) + + self.table.setSelectionMode(QAbstractItemView.ExtendedSelection) + + def updateTableView(self, rows): + tabledf = Tdf.getTable(self) + rowcount = len(tabledf.index) + for i in range(rowcount): + self.table.setRowHidden(i, True) + for i in rows: + self.table.setRowHidden(i, False) + + def filterTable(self): + """ + get changes from textbox and update table when + more than 3 characters are given. + then update table with the rows that + contain the input in the give column. + """ + tb = self.textbox + givencolumn = "Spectra_Filepath" + tbinput = tb.text() + ft = Tdf.getTable(self) + validDf = not(ft.empty or ft.dropna().empty) + # print(validDf) # for debugging + # print(type(ft)) # for debugging + if len(tbinput) >= 2: + rowstoshow = ft[ft[givencolumn].str.contains(tbinput)] + self.updateTableView(rowstoshow.index) + else: + self.updateTableView(ft.index) + + def changeListener(self): + self.table.itemChanged.connect(self.editField) + + def editField(self): + if len(self.table.selectedItems()) == 1: + if not self.drawtableactive: + itemchanged = self.table.currentItem() + newvalue = itemchanged.text() + row = itemchanged.row() + column = itemchanged.column() + if column != 2: + Tdf.modifyField(self, row, column, newvalue) + self.drawTable() + else: + QMessageBox.about(self, "Warning", "Please only, " + + "modify attribute columns," + + "not the filepath.\n" + + "To change the filepath," + + "use remove and add file.") + self.drawTable()