def init_ui(self): self.w = Ui_MainWindow() self.setWindowState(Qt.WindowMaximized) self.w.setupUi(self) self.w.version = __version__ self.status_bar = self.statusBar() self.statusBar = progressBar(self.status_bar) self.statusBar.initProgressBar(self.status_bar) self.progress_bar = self.statusBar.progressBar self.m = PandasTableModel(self) self.tree_widget = self.w.tree_widget # The nav tree widget. self.site_tree_widget = self.w.treeWidget_sitesToApply # site selection tree widget in "all records view" self.settings = settingsWindow(self) # settingsWindow self.associatedTaxaWindow = associatedTaxaMainWindow(self) # associatedTaxaWindow self.associatedTaxaWindow.setWindowModality(Qt.ApplicationModal) self.lineEdit_sciName = self.w.lineEdit_sciName self.form_view = self.w.formView self.table_view = self.w.table_view self.table_view.setItemDelegate(editorDelegate(self.table_view)) # use flipped proxy delegate self.form_view.init_ui(self, self.w) self.tax = taxonomicVerification(self.settings, self) # taxonomic verifier self.p = LabelPDF(self.settings) self.p.initLogoCanvas() # alter this to happen based on settings changes self.pdf_preview = self.w.pdf_preview self.pdf_preview.initViewer(self) self.m.new_Records(True) self.table_view.setModel(self.m) self.locality = locality(self) self.w.action_Open.triggered.connect(self.m.open_CSV) self.w.action_Save_As.triggered.connect(self.m.save_CSV) self.w.action_New_Records.triggered.connect(self.m.new_Records) self.w.action_undo.triggered.connect(self.m.undo) self.w.action_redo.triggered.connect(self.m.redo) self.w.action_Exit.triggered.connect(lambda: sys.exit(app.exec_())) self.w.action_Settings.triggered.connect(self.toggleSettings) self.w.button_associatedTaxa.clicked.connect(self.toggleAssociated) self.w.action_Reverse_Geolocate.triggered.connect(self.m.geoRef) self.w.toolButton_reverseGeolocate.clicked.connect(self.m.geoRef) self.w.action_Verify_Taxonomy.triggered.connect(self.m.verifyTaxButton) self.w.toolButton_verifyTaxonomy.clicked.connect(self.m.verifyTaxButton) self.w.action_Verify_All.triggered.connect(self.m.verifyAllButton) self.w.action_Export_Labels.triggered.connect(self.exportLabels) self.w.pushButton_newSite.clicked.connect(self.m.addNewSite) self.w.pushButton_newSpecimen.clicked.connect(self.m.addNewSpecimen) self.w.pushButton_newSpecimen_2.clicked.connect(self.m.addNewSpecimen) # copy of above, except placed on specimen view self.w.pushButton_duplicateSpecimen.clicked.connect(self.m.duplicateSpecimen) self.w.pushButton_deleteSite.clicked.connect(self.m.deleteSite) self.w.pushButton_deleteRecord.clicked.connect(self.m.deleteSpecimen) self.w.toolButton_sitesToApply_SelectNone.clicked.connect(self.clearSitesToApply) self.w.toolButton_sitesToApply_SelectAll.clicked.connect(self.selectAllSitesToApply) self.w.action_Export_Records.triggered.connect(self.exportRecords) #self.w.actionTestFunction.triggered.connect(self.timeitTest) # a test function button for debugging or time testing #self.w.actionTestFunction.triggered.connect(self.m.assignCatalogNumbers) self.w.actionTestFunction.triggered.connect(self.userSciNameInput) # update the preview window as dataframe changes self.m.dataChanged.connect(self.updatePreview) self.updateAutoComplete() self.versionCheck()
class MyWindow(QMainWindow): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.w = Ui_MainWindow() self.setWindowState(Qt.WindowMaximized) self.w.setupUi(self) self.w.version = __version__ self.status_bar = self.statusBar() self.statusBar = progressBar(self.status_bar) self.statusBar.initProgressBar(self.status_bar) self.progress_bar = self.statusBar.progressBar self.m = PandasTableModel(self) self.tree_widget = self.w.tree_widget # The nav tree widget. self.site_tree_widget = self.w.treeWidget_sitesToApply # site selection tree widget in "all records view" self.settings = settingsWindow(self) # settingsWindow self.associatedTaxaWindow = associatedTaxaMainWindow(self) # associatedTaxaWindow self.associatedTaxaWindow.setWindowModality(Qt.ApplicationModal) self.lineEdit_sciName = self.w.lineEdit_sciName self.form_view = self.w.formView self.table_view = self.w.table_view self.table_view.setItemDelegate(editorDelegate(self.table_view)) # use flipped proxy delegate self.form_view.init_ui(self, self.w) self.tax = taxonomicVerification(self.settings, self, apiKeys.tropicos_API_key) # taxonomic verifier self.p = LabelPDF(self.settings) self.p.initLogoCanvas() # alter this to happen based on settings changes self.pdf_preview = self.w.pdf_preview self.pdf_preview.initViewer(self) self.m.new_Records(True) self.table_view.setModel(self.m) self.locality = locality(self, apiKeys.google_API_key) self.w.action_Open.triggered.connect(self.m.open_CSV) self.w.action_Save_As.triggered.connect(self.m.save_CSV) self.w.action_New_Records.triggered.connect(self.m.new_Records) self.w.action_undo.triggered.connect(self.m.undo) self.w.action_redo.triggered.connect(self.m.redo) self.w.action_About_collBook.triggered.connect(self.callAboutDialog) self.w.action_Context_Help.triggered.connect(QWhatsThis.enterWhatsThisMode) self.w.action_Exit.triggered.connect(lambda: sys.exit(app.exec_())) self.w.action_Settings.triggered.connect(self.toggleSettings) self.w.button_associatedTaxa.clicked.connect(self.toggleAssociated) self.w.action_Reverse_Geolocate.triggered.connect(self.m.geoRef) self.w.toolButton_reverseGeolocate.clicked.connect(self.m.geoRef) self.w.action_Verify_Taxonomy.triggered.connect(self.m.verifyTaxButton) self.w.toolButton_verifyTaxonomy.clicked.connect(self.m.verifyTaxButton) self.w.action_Verify_All.triggered.connect(self.m.verifyAllButton) self.w.action_Export_Labels.triggered.connect(self.exportLabels) self.w.pushButton_newSite.clicked.connect(self.m.addNewSite) self.w.pushButton_newSpecimen.clicked.connect(self.m.addNewSpecimen) self.w.pushButton_newSpecimen_2.clicked.connect(self.m.addNewSpecimen) # copy of above, except placed on specimen view self.w.pushButton_duplicateSpecimen.clicked.connect(self.m.duplicateSpecimen) self.w.pushButton_deleteSite.clicked.connect(self.m.deleteSite) self.w.pushButton_deleteRecord.clicked.connect(self.m.deleteSpecimen) self.w.toolButton_sitesToApply_SelectNone.clicked.connect(self.clearSitesToApply) self.w.toolButton_sitesToApply_SelectAll.clicked.connect(self.selectAllSitesToApply) self.w.action_Export_Records.triggered.connect(self.exportRecords) #self.w.actionTestFunction.triggered.connect(self.timeitTest) # a test function button for debugging or time testing #self.w.actionTestFunction.triggered.connect(self.m.assignCatalogNumbers) self.w.actionTestFunction.triggered.connect(self.userSciNameInput) # update the preview window as dataframe changes self.m.dataChanged.connect(self.updatePreview) self.updateAutoComplete() self.versionCheck() def versionCheck(self): """ checks the github repo's latest release version number against local and offers the user to visit the new release page if different""" # be sure to only do this once a day. today = str(date.today()) lastChecked = self.settings.get('date_versionCheck', today) self.w.date_versionCheck = today if today != lastChecked: import requests from requests.exceptions import ConnectionError import webbrowser apiURL = 'https://api.github.com/repos/CapPow/collBook/releases/latest' try: apiCall = requests.get(apiURL) status = str(apiCall) except ConnectionError: # if no internet, don't bother the user. return result = apiCall.json() if '200' in status: # if the return looks bad, don't bother user url = result['html_url'] version = result['tag_name'] if version.lower() != self.w.version.lower(): message = f'A new version ( {version} ) of collBook has been released. Would you like to visit the release page?' title = 'collBook Version' answer = self.userAsk(message, title, inclHalt = False) if answer:# == QMessageBox.Yes: link=url self.showMinimized() # hide the app about to pop up. # instead display a the new release webbrowser.open(link,autoraise=1) self.settings.saveSettings() # save the new version check date def callAboutDialog(self): ab = aboutDialog() ab.exec_() #result = dialog.exec_() def toggleSettings(self): if self.settings.isHidden(): self.settings.show() else: self.settings.hide() def toggleAssociated(self): if self.associatedTaxaWindow.isHidden(): self.associatedTaxaWindow.populateAssociatedTaxa() selType, siteNum, specimenNum = self.getTreeSelectionType() self.associatedTaxaWindow.setWindowTitle('Associated taxa') self.associatedTaxaWindow.associatedMainWin.label_UserMsg.setText(f'Select associated taxa for site {siteNum}') self.associatedTaxaWindow.show() else: self.associatedTaxaWindow.associatedList.clear() self.associatedTaxaWindow.close() def getTreeSelectionType(self): """ checks the tree_widget's type of selection """ # TODO alter selType to become a custom attribute of mainWindow, setting it upon changes. This should reduce checks with this function try: itemSelected = self.tree_widget.currentItem() text = itemSelected.text(0).split('(')[0].strip() except AttributeError as e: # if not force "All Records" text = "All Records" siteNum = None specimenNum = None if text == "All Records": selType = 'allRec' elif "Site" in text: selType = 'site' siteNum = text.replace('Site ','').strip() elif "-" in text: selType = 'specimen' siteNum, specimenNum = text.split('-') return selType, siteNum, specimenNum def updateTableView(self): """ updates the table_view, and form_view's current tab called after tree_widget's selection change """ # TODO rename this, as it does more than upates tableview. Basically alters scope of user's view selType, siteNum, specimenNum = self.getTreeSelectionType() rowsToHide = self.m.getRowsToHide(selType, siteNum, specimenNum) rowNums = range(self.m.rowCount()) for row in rowNums: if row not in rowsToHide: self.table_view.showRow(row) else: self.table_view.hideRow(row) if selType != 'allRec': topVisible = [x for x in rowNums if x not in rowsToHide] try: #TODO make consideration for avoiding this if the last action was an edit. topVisible = min(topVisible) self.table_view.selectRow(topVisible) except ValueError: self.table_view.clearSelection() self.updatePreview() if selType == 'site': self.statusBar.label_status.setText(" Site View ") self.form_view.setCurrentIndex(1) #swap to site tab elif selType == 'specimen': self.statusBar.label_status.setText("Specimen View") self.form_view.setCurrentIndex(2) #swap to specimen tab else: # probably all records self.statusBar.label_status.setText(" All Records ") self.form_view.setCurrentIndex(0) #all records self.form_view.fillFormFields() def selectTreeWidgetItemByIndex(self, i): """ helper function called when form_view's tab index is clicked Is intended to change the user's view to all records when the all records tab is clicked """ if i == 0: self.selectTreeWidgetItemByName('All Records') def selectTreeWidgetItemByName(self, name): """ selects an item on the nav tree_widget. Permits site selection without the parenthetical (n) value. ie: 'Site 5' would find 'Site 5 (12)' """ iterator = QTreeWidgetItemIterator(self.tree_widget, QTreeWidgetItemIterator.All) if name[:5] == 'Site ': # handle changing record counts at set siteNumbers name = name.split('(')[0].strip() while iterator.value(): item = iterator.value() if name in item.text(0): self.tree_widget.setCurrentItem(item,1) break iterator +=1 def expandCurrentTreeWidgetItem(self): """ expands the currently selected tree_widget item """ itemSelected = self.tree_widget.currentItem() selectionIndex = self.tree_widget.indexFromItem(itemSelected) self.tree_widget.expand(selectionIndex) def setTreeSelectionByType(self, selType, siteNum, specimenNum): """ sets tree selection using the returned values of getTreeSelectionType called by pandastablemodel when redoing or undoing other df states """ if selType == 'allRec': text = "All Records" elif selType == 'site': text = f'Site {siteNum}' elif selType == 'specimen': text = f'{siteNum}-{specimenNum}' self.selectTreeWidgetItemByName(text) def timeitTest(self): """ debugging / improving space for testing various functions or their timings """ from datetime import datetime a = datetime.now() iterCount = 10000 for i in range(iterCount): listComp = [x for x in range(0, self.m.rowCount()) if not self.table_view.isRowHidden(x)] b = datetime.now() listCompTime = b - a listCompTime = int((listCompTime.total_seconds() / iterCount) * 1000000) # microseconds print(f'listComp = {listCompTime} (µs)') a = datetime.now() for i in range(iterCount): treeSel = self.m.getRowsToProcess(*self.getTreeSelectionType()) b = datetime.now() treeSelTime = b - a treeSelTime = int((treeSelTime.total_seconds() / iterCount) * 1000000) # microseconds print(f'treeSel = {treeSelTime} (µs)') def getVisibleRows(self): """ returns a list of indicies which are visible """ visibleRows = [x for x in range(0, self.m.rowCount()) if not self.table_view.isRowHidden(x)] return visibleRows def getVisibleRowData(self): """ queries the table_view for selected rows and returns associated rowData """ rowsVisible = self.getVisibleRows() if rowsVisible: rowData = self.m.retrieveRowData(rowsVisible) rowData = self.m.getSelectedLabelDict(rowData) else: rowData = None return rowData def userSciNameInput(self, title = "", message = ""): """ opens a cusotm user dialog and requests a scientificName """ dlg = sciNameDialog() res = dlg.textBox(self.wordList, message, title) return res # TODO for simplicity, move all userASK and userNOTIFY functions into mainWindow and alter calls in other modules to use it. def userAsk(self, text, title='', inclHalt=True, retry=False, detailText=None): """ a general user dialog with yes / cancel options""" msg = QMessageBox() msg.setIcon(QMessageBox.Question) msg.setText(text) msg.setWindowTitle(title) if retry: msg.setIcon(QMessageBox.Warning) msg.setStandardButtons(QMessageBox.Retry | QMessageBox.Cancel) else: msg.setStandardButtons(QMessageBox.No | QMessageBox.Yes) if detailText != None: msg.setDetailedText(detailText) if inclHalt: halt = msg.addButton('Halt Process', QtWidgets.QMessageBox.ResetRole) halt.clicked.connect(self.statusBar.flipCancelSwitch) msg.setDefaultButton(QMessageBox.No) reply = msg.exec_() if reply == QMessageBox.Yes: return True elif reply == QMessageBox.No: return False elif reply == QMessageBox.Cancel: return False elif reply == QMessageBox.Retry: return True else: return "cancel" def userNotice(self, text, title='', detailText = None, retry=False, inclHalt=True): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText(text) if detailText != None: msg.setDetailedText(detailText) #msg.setInformativeText("This is additional information") msg.setWindowTitle(title) if retry: msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Retry) else: msg.setStandardButtons(QMessageBox.Ok) if inclHalt: halt = msg.addButton('Halt Process', QtWidgets.QMessageBox.ResetRole) halt.clicked.connect(self.statusBar.flipCancelSwitch) reply = msg.exec_() return reply def exportRecords(self): """ saves a pdf file of the labels AND a csv of the records prepared for SERNEC upload. """ saved = False # have both files successfully saved? (not yet) # show the dialog while saved is False: if not self.testRunLabels(): return False chosenFileName, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Export Labels", "", "Label PDFs (*.pdf)") if chosenFileName == "": # The user probably pressed Cancel return None else: fileName = chosenFileName fileExtension = Path(chosenFileName).suffix if fileExtension != '': fileName = fileName.replace(fileExtension,'') csvFileName = f'{fileName}.csv' pdfFileName = f'{fileName}.pdf' if Path(csvFileName).is_file(): message = f'Record file named: "{csvFileName}" already exist! OVERWRITE the Record (csv) file?' title = 'Export Records' answer = self.userAsk(message, title) if answer: readyToSave = True # have we settled on the fileName(S)? else: readyToSave = False # have we settled on the fileName(S)? else: readyToSave = True # have we settled on the fileName(S)? if readyToSave: # if fileName(s) are settled... self.m.assignCatalogNumbers() # the assignCatalogNumber function checks user settings before applying rowsToProcess = self.m.getRowsToProcess(*self.getTreeSelectionType()) # Permission flag handles cases where pdf preview is open, yet user wants to overwrite the open file. permission = False while permission == False: try: labelSuccess = self.exportLabels(fileName = pdfFileName) if labelSuccess: outDF = self.m.datatable.iloc[rowsToProcess, ] outDF = outDF.loc[outDF['specimenNumber'] != '#'] csvName = csvFileName self.m.export_CSV(df = outDF, fileName = csvFileName) saved = True permission = True except PermissionError: # Note, under this condition the catalog numbers might be assigned yet not used. title = "Permission Error" msg = f"Attempting to overwrite a file which is open in another program. Export cannot proceed until you close that program." details = f'One or both of the following files may be open in another program. It must be closed to proceed with the export.\n{pdfFileName}\n{csvFileName}' askReply = self.userAsk(msg, title, retry=True, inclHalt=False, detailText=details) if askReply: permission = False else: # even though not saved or permissed, do this to break loops permission = True saved = True def testRunLabels(self): """ Tests generating the labels to ensure the contents all fit. Returns True or False depending on test's results.""" try: # generate test labels and toss them out. to test for oversize warnings self.p.genLabelPreview(self.getVisibleRowData()) return True except LayoutError: msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("""The content of one or more of your labels is too large for the label dimentions. Alter your settings, and try again.""") msg.setWindowTitle('Label Generation Error') msg.setStandardButtons(QMessageBox.Ok) msg.exec_() return False def exportLabels(self, fileName = None): """ bundles records up and passes them to printlabels.genPrintLabelPDFs() """ try: rowsToProcess = self.m.getRowsToProcess(*self.getTreeSelectionType()) outDF = self.m.datatable.iloc[rowsToProcess, ] outDict = self.m.getSelectedLabelDict(outDF) self.p.genPrintLabelPDFs(outDict, defaultFileName = fileName) return True except LayoutError: from PyQt5.QtWidgets import QMessageBox msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText("""The content of one or more of your labels is too large for the label dimentions. Alter your settings, and try again.""") msg.setWindowTitle('Label Export Error') msg.setStandardButtons(QMessageBox.Ok) msg.exec_() return False def updatePreview(self): """ updates the pdf preview window """ #TODO modify this to be called from within the pdfviewer class rowData = self.getVisibleRowData() selType, siteNum, specimenNum = self.getTreeSelectionType() errorType = False if (isinstance(rowData, list)) & (selType != 'allRec'): if (selType == 'site') & (len(rowData) > 1): rowData = [rowData[1]] # only want first row, but other functions expect a list else: rowData = [rowData[0]] try: pdfBytes = self.p.genLabelPreview(rowData) # retrieves the pdf in Bytes except LayoutError: # Not enough space on label for the content pdfBytes = None errorType = 'oversize' else: # there is not appropriate row data to preview pdfBytes = None errorType = 'preview' # display generic "Preview window text" self.pdf_preview.load_preview(pdfBytes, errorType) # starts the loading display process #self.settings.setMaxZoom() def updatePreviewZoom(self, val): """ changes the value_Zoom setting stored in self.settings, used by pdfviewer.py to determine the size of the preview window. Additionally, updates the label_zoomLevel's text in MainWindow """ try: self.settings.setMaxZoom() self.updatePreview() # update the pdfPreview (this could get cpu intensive) except AttributeError: pass # It gets called too early on start up, this skips it def updateAutoComplete(self): """ updates the Completer's reference text based on the kingdom """ value_Kingdom = self.settings.get('value_Kingdom', 'Plantae') if value_Kingdom == 'Plantae': nameCol = 'complete_name' if value_Kingdom == 'Fungi': nameCol = 'normalized_name' stream = QFile(f':/rc_/{value_Kingdom}_Reference.csv') if stream.open(QFile.ReadOnly): df = StringIO(str(stream.readAll(), 'utf-8')) stream.close() # completer.setCompletionMode(QCompleter.InlineCompletion) # completer.maxVisibleItems=10 # completer.setCaseSensitivity(Qt.CaseInsensitive) # make the completer selection also erase the text edit # completer.activated.connect(self.cleartext,type=Qt.QueuedConnection) wordList = pd.read_csv(df, encoding = 'utf-8', dtype = 'str') wordList = wordList[nameCol].str.capitalize().tolist() self.wordList = sorted(wordList) completer = QCompleter(self.wordList, self.lineEdit_sciName) completer.setCaseSensitivity(Qt.CaseInsensitive) self.lineEdit_sciName.setCompleter(completer) completerAssociated = QCompleter(self.wordList, self.associatedTaxaWindow.lineEdit_newAssociatedTaxa) completerAssociated .setCaseSensitivity(Qt.CaseInsensitive) self.associatedTaxaWindow.associatedMainWin.lineEdit_newAssociatedTaxa.setCompleter(completerAssociated) def getSelectSitesToApply(self): """ queries the site_tree_widget to determine which are checked """ fieldNumbers = self.m.getSiteSpecimens() siteNumbers = list(set([x[0] for x in fieldNumbers])) if self.w.radioButton_applyAllRecords.isChecked(): return siteNumbers else: siteNumbers = [] iterator = QTreeWidgetItemIterator(self.site_tree_widget, QTreeWidgetItemIterator.Checked) while iterator.value(): siteText = iterator.value().text(0) siteNum = siteText.split()[-1] siteNumbers.append(siteNum) iterator += 1 return siteNumbers def selectAllSitesToApply(self): """ checks all objects in site_tree_widget, may be useful if the user wants to select all but a few in the list""" iterator = QTreeWidgetItemIterator(self.site_tree_widget, QTreeWidgetItemIterator.NotChecked) while iterator.value(): obj = iterator.value() obj.setCheckState(0, Qt.Checked) iterator += 1 def clearSitesToApply(self): """ unchecks all objects in site_tree_widget """ iterator = QTreeWidgetItemIterator(self.site_tree_widget, QTreeWidgetItemIterator.Checked) while iterator.value(): obj = iterator.value() obj.setCheckState(0, Qt.Unchecked) iterator += 1 def populateTreeWidget(self): """ populates the navigation TreeWidget with the records nested within sites. Also populates site_tree_widget with site numbers""" # store the current selection(s) itemSelected = self.tree_widget.currentItem() sites_Selected = self.getSelectSitesToApply() sites_Selected = [f'Site {x}' for x in sites_Selected] try: text = itemSelected.text(0) except AttributeError: text = 'All Records' self.tree_widget.clear() self.site_tree_widget.clear() fieldNumbers = self.m.getSiteSpecimens() siteNumbers = list(set([x[0] for x in fieldNumbers])) try: siteNumbers.sort(key = int) except ValueError: pass # build a hierarchical structure of QTreeWidgetItem(s) to fill the tree with self.tree_widget.addTopLevelItem(QTreeWidgetItem(["All Records"])) for siteNum in siteNumbers: site_tree_text = f'Site {siteNum}' site_tree_item = QTreeWidgetItem([site_tree_text]) if site_tree_text in sites_Selected: site_tree_item.setCheckState(0, Qt.Checked) # update site_tree_widget else: site_tree_item.setCheckState(0, Qt.Unchecked) # update site_tree_widget self.site_tree_widget.addTopLevelItems([site_tree_item]) # fill in site_tree_widget # now start on the navigation "tree_widget" object. specimenNumbers = list(set([y for x, y in fieldNumbers if x == siteNum and y != '#'])) specimenNumbers.sort(key = int) site = QTreeWidgetItem([f'Site {siteNum} ({len(specimenNumbers)})']) siteChildren = [] for i in specimenNumbers: # where i is a specimen number for a site label = [f'{siteNum}-{i}'] child = QTreeWidgetItem(label) siteChildren.append(child) site.addChildren(siteChildren) # add the list of sites (with children) to the master list self.tree_widget.addTopLevelItems([site]) # return to the selection (if it exists) self.selectTreeWidgetItemByName(text)