# -*- coding: utf-8 -*- """ GEPARD - Gepard-Enabled PARticle Detection Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Polymerforschung Dresden e. V. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, see COPYING. If not, see . """ import logging import logging.handlers import traceback import os from io import StringIO from typing import List from PyQt5 import QtCore, QtWidgets, QtGui from .sampleview import SampleView from .gui.scalebar import ScaleBar from .instrumentcom.instrumentConfig import defaultPath from .instrumentcom.lightModeSwitch import LightModeSwitch from .gui.colorlegend import ColorLegend from .gepardlogging import setDefaultLoggingConfig from .workmodes import ModeHandler from .unittests.test_gepard import testGepard from .helperfunctions import getAppFolder class GEPARDMainWindow(QtWidgets.QMainWindow): def __init__(self, logger): super(GEPARDMainWindow, self).__init__() self.setWindowTitle("GEPARD") self.fname: str = '' self.resize(900, 700) self.scalebar = ScaleBar(self) self.legend = ColorLegend(self) self.lightModeSwitch = LightModeSwitch(self) self.view = SampleView(self, logger) self.view.ScalingChanged.connect(self.scalingChanged) self.view.ScalingChanged.connect(self.scalebar.updateScale) mdiarea = QtWidgets.QMdiArea(self) mdiarea.addSubWindow(self.scalebar) mdiarea.addSubWindow(self.legend) mdiarea.addSubWindow(self.lightModeSwitch) self.legend.hide() self.lightModeSwitch.hide() subview = mdiarea.addSubWindow(self.view) subview.showMaximized() subview.setWindowFlags(QtCore.Qt.FramelessWindowHint) mdiarea.setOption(QtWidgets.QMdiArea.DontMaximizeSubWindowOnActivation) self.setCentralWidget(mdiarea) self.createActions() self.createMenus() self.createToolBar() self.modeHandler: ModeHandler = ModeHandler(self) self.modeHandler.modeSwitched.connect(self.view.viewItemHandler.switchToNewMode) self.updateModes() def resizeEvent(self, event): self.scalebar.move(0, self.height()-self.scalebar.height()-30) self.legend.move(self.width()-self.legend.width()-130, 10) self.lightModeSwitch.move(5, 5) def closeEvent(self, event): self.view.closeEvent(event) def forceCloseAll(self) -> None: closeAll() @QtCore.pyqtSlot(float) def scalingChanged(self): self.zoomInAct.setEnabled(self.view.scaleFactor < 20.0) self.zoomOutAct.setEnabled(self.view.scaleFactor > .01) self.normalSizeAct.setEnabled(self.view.scaleFactor != 1.) @QtCore.pyqtSlot() def open(self, fileName=False): if fileName is False: fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Open Project", defaultPath, "*.pkl")[0] if fileName: self.fname = str(fileName) self.view.open(self.fname) self.scalingChanged() @QtCore.pyqtSlot() def importProject(self, fileName=False): if fileName is False: fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Import Zeiss Zen Project", defaultPath, "*.xml")[0] if fileName: self.fname = str(fileName) self.view.importProject(self.fname) self.scalingChanged() @QtCore.pyqtSlot() def new(self, fileName=False): if fileName is False: fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project", defaultPath, "*.pkl")[0] if fileName: isValid, msg = self.testFilename(fileName) if isValid: self.fname = str(fileName) self.view.new(self.fname) self.scalingChanged() else: QtWidgets.QMessageBox.critical(self, "Error", msg) @QtCore.pyqtSlot() def testFilename(self, fileName): if self.view.instrctrl.name == 'RenishawCOM': # the renishawCom does not allow Spaces within filePath if fileName.find(' ') == 0: return False, "File path must not contain spaces." else: return True, "" else: return True, "" @QtCore.pyqtSlot() def about(self): QtWidgets.QMessageBox.about(self, 'GEPARD', "Developed by Complex Fiber Structures GmbH " "on behalf of Leibniz-IPF Dresden") def getParticleRelevantActs(self) -> List[QtWidgets.QAction]: return [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct] def disableParticleRelevantActs(self) -> None: for act in self.getParticleRelevantActs(): act.setDisabled(True) def enableParticleRelevantActs(self) -> None: for act in self.getParticleRelevantActs(): act.setEnabled(True) def updateModes(self, active=None, maxenabled=None): ose, osc, pde, pdc, rse, rsc = [False]*6 if maxenabled == "OpticalScan": ose = True elif maxenabled == "ParticleDetection": ose, pde = True, True elif maxenabled == "SpectrumScan": ose, pde, rse = True, True, True if active == "OpticalScan" and ose: osc = True elif active == "ParticleDetection" and pde: pdc = True elif active == "SpectrumScan" and rse: rsc = True self.opticalScanAct.setEnabled(ose) self.opticalScanAct.setChecked(osc) self.detectParticleAct.setEnabled(pde) self.detectParticleAct.setChecked(pdc) self.specScanAct.setEnabled(rse) self.specScanAct.setChecked(rsc) def activateMaxMode(self, loadnew=False) -> None: self.modeHandler.activateMaxMode(loadnew) def getCurrentMode(self) -> str: mode: str = 'None' if self.modeHandler.activeMode is not None: mode = self.modeHandler.activeMode.name return mode def unblockUI(self, connected): self.openAct.setEnabled(True) self.importAct.setEnabled(True) self.newAct.setEnabled(True) self.updateConnected(connected) self.exitAct.setEnabled(True) def blockUI(self): self.openAct.setEnabled(False) self.importAct.setEnabled(False) self.newAct.setEnabled(False) self.connectRamanAct.setEnabled(False) self.disconnectRamanAct.setEnabled(False) self.exitAct.setEnabled(False) self.opticalScanAct.setEnabled(False) self.detectParticleAct.setEnabled(False) self.specScanAct.setEnabled(False) def updateConnected(self, connected): if connected: self.connectRamanAct.setEnabled(False) self.disconnectRamanAct.setEnabled(True) else: self.connectRamanAct.setEnabled(True) self.disconnectRamanAct.setEnabled(False) def createActions(self): def runGepardTest(gebbard): return lambda: testGepard(gebbard) fname = os.path.join(os.path.split(__file__)[0], os.path.join('data', 'brand.png')) self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname), "About Particle Measurment", self) self.aboutAct.triggered.connect(self.about) self.openAct = QtWidgets.QAction("&Open Project...", self) self.openAct.setShortcut("Ctrl+O") self.openAct.triggered.connect(self.open) self.importAct = QtWidgets.QAction("&Import Zeiss Project...", self) self.importAct.setShortcut("Ctrl+I") self.importAct.triggered.connect(self.importProject) self.newAct = QtWidgets.QAction("&New Measurement...", self) self.newAct.setShortcut("Ctrl+N") self.newAct.triggered.connect(self.new) self.exitAct = QtWidgets.QAction("E&xit", self) self.exitAct.setShortcut("Ctrl+Q") self.exitAct.triggered.connect(self.close) self.zoomInAct = QtWidgets.QAction("Zoom &In (25%)", self) self.zoomInAct.setShortcut("Ctrl++") self.zoomInAct.setEnabled(False) self.zoomInAct.triggered.connect(self.view.zoomIn) self.zoomOutAct = QtWidgets.QAction("Zoom &Out (25%)", self) self.zoomOutAct.setShortcut("Ctrl+-") self.zoomOutAct.setEnabled(False) self.zoomOutAct.triggered.connect(self.view.zoomOut) self.normalSizeAct = QtWidgets.QAction("&Normal Size", self) self.normalSizeAct.setShortcut("Ctrl+S") self.normalSizeAct.setEnabled(False) self.normalSizeAct.triggered.connect(self.view.normalSize) self.fitToWindowAct = QtWidgets.QAction("&Fit to Window", self) self.fitToWindowAct.setShortcut("Ctrl+E") self.fitToWindowAct.setEnabled(True) self.fitToWindowAct.triggered.connect(self.view.fitToWindow) self.connectRamanAct = QtWidgets.QAction("Connect to Microscope", self) self.connectRamanAct.setEnabled(True) self.connectRamanAct.triggered.connect(self.view.connectRaman) self.disconnectRamanAct = QtWidgets.QAction("Release Microscope", self) self.disconnectRamanAct.setEnabled(False) self.disconnectRamanAct.triggered.connect(self.view.disconnectRaman) self.opticalScanAct = QtWidgets.QAction("Optical Scan", self) self.opticalScanAct.setEnabled(False) self.opticalScanAct.setCheckable(True) self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("OpticalScan"))) self.detectParticleAct = QtWidgets.QAction("Detect Particles", self) self.detectParticleAct.setEnabled(False) self.detectParticleAct.setCheckable(True) self.detectParticleAct.triggered.connect( QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("ParticleDetection"))) self.specScanAct = QtWidgets.QAction("Spectrum Scan", self) self.specScanAct.setEnabled(False) self.specScanAct.setCheckable(True) self.specScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("SpectrumScan"))) self.snapshotAct = QtWidgets.QAction("&Save Screenshot", self) self.snapshotAct.triggered.connect(self.view.takeScreenshot) self.snapshotAct.setDisabled(True) self.configRamanCtrlAct = QtWidgets.QAction("&Configure Raman Control", self) self.configRamanCtrlAct.triggered.connect(self.view.configureInstrumentControl) if self.view.simulatedRaman: self.configRamanCtrlAct.setDisabled(True) self.testAct: QtWidgets.QAction = QtWidgets.QAction("&Run Automated Gepard Test") self.testAct.setShortcut("Ctrl+T") self.testAct.triggered.connect(runGepardTest(self)) self.noOverlayAct = QtWidgets.QAction("&No Overlay", self) self.noOverlayAct.setShortcut("1") self.noOverlayAct.setCheckable(True) self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self) # TODO: Is that needed?? self.selOverlayAct.setShortcut("2") self.selOverlayAct.setCheckable(True) self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self) self.fullOverlayAct.setShortcut("3") self.fullOverlayAct.setCheckable(True) self.transpAct = QtWidgets.QAction("&Transparent Overlay", self) self.transpAct.setShortcut("T") self.transpAct.setCheckable(True) self.transpAct.setChecked(False) self.transpAct.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility) self.hideLabelAct = QtWidgets.QAction('&Hide Spectra Numbers', self) self.hideLabelAct.setShortcut("H") self.darkenAct = QtWidgets.QAction("&Darken Image", self) self.darkenAct.setShortcut("D") self.seedAct = QtWidgets.QAction("&Set Color Seed", self) self.disableParticleRelevantActs() def createMenus(self): self.fileMenu = QtWidgets.QMenu("&File", self) self.fileMenu.addAction(self.newAct) self.fileMenu.addAction(self.importAct) self.fileMenu.addAction(self.openAct) self.fileMenu.addSeparator() self.fileMenu.addAction(self.exitAct) self.viewMenu = QtWidgets.QMenu("&View", self) self.viewMenu.addAction(self.zoomInAct) self.viewMenu.addAction(self.zoomOutAct) self.viewMenu.addAction(self.normalSizeAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.fitToWindowAct) self.modeMenu = QtWidgets.QMenu("&Mode", self) self.modeMenu.addAction(self.opticalScanAct) self.modeMenu.addAction(self.detectParticleAct) self.toolsMenu = QtWidgets.QMenu("&Tools") self.toolsMenu.addAction(self.snapshotAct) self.toolsMenu.addAction(self.configRamanCtrlAct) self.toolsMenu.addAction(self.testAct) self.dispMenu = QtWidgets.QMenu("&Display", self) self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu) self.overlayActGroup.setExclusive(True) self.overlayActGroup.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility) for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct]: self.dispMenu.addAction(act) self.overlayActGroup.addAction(act) self.fullOverlayAct.setChecked(True) self.dispMenu.addSeparator() self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct]) self.helpMenu = QtWidgets.QMenu("&Help", self) self.helpMenu.addAction(self.aboutAct) self.menuBar().addMenu(self.fileMenu) self.menuBar().addMenu(self.viewMenu) self.menuBar().addMenu(self.modeMenu) self.menuBar().addMenu(self.toolsMenu) self.menuBar().addMenu(self.dispMenu) self.menuBar().addMenu(self.helpMenu) def createToolBar(self): self.toolbar = QtWidgets.QToolBar("Tools") self.toolbar.setIconSize(QtCore.QSize(100,50)) self.toolbar.addAction(self.aboutAct) self.toolbar.addAction(self.newAct) self.toolbar.addAction(self.importAct) self.toolbar.addAction(self.openAct) self.toolbar.addSeparator() self.toolbar.addAction(self.connectRamanAct) self.toolbar.addAction(self.disconnectRamanAct) self.toolbar.addSeparator() self.toolbar.addAction(self.opticalScanAct) self.toolbar.addAction(self.detectParticleAct) self.toolbar.addAction(self.specScanAct) self.toolbar.addSeparator() self.toolbar.addAction(self.exitAct) self.toolbar.setOrientation(QtCore.Qt.Vertical) self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar) if __name__ == '__main__': import sys from time import localtime, strftime def closeAll() -> None: """ Closes the app and, with that, all windows. Josef: I implemented this, as with the simulated microscope stage it was difficult to find a proper way to ONLY close it at the end of running the program. Closing it on disconnect of the instrctrl is not suitable, as it should be opened also in disconnected stage (e.g., when another instance is running in optical or raman scan, but the UI (disconnected) should still update what's going on. """ app.closeAllWindows() def excepthook(excType, excValue, tracebackobj): """ Global function to catch unhandled exceptions. @param excType exception type @param excValue exception value @param tracebackobj traceback object :return: """ tbinfofile = StringIO() traceback.print_tb(tracebackobj, None, tbinfofile) tbinfofile.seek(0) tbinfo = tbinfofile.read() logging.critical("Fatal error in program excecution!") logging.critical(tbinfo) from .errors import showErrorMessageAsWidget showErrorMessageAsWidget(tbinfo) sys.excepthook = excepthook logger = logging.getLogger(__name__) app = QtWidgets.QApplication(sys.argv) logpath = getAppFolder() if logpath != "": if not os.path.exists(logpath): os.mkdir(logpath) logname = os.path.join(logpath, 'logfile.txt') logger.addHandler( logging.handlers.RotatingFileHandler( logname, maxBytes=5*(1 << 20), backupCount=10) ) logger.setLevel(logging.DEBUG) setDefaultLoggingConfig(logger) logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime())) gepard = GEPARDMainWindow(logger) gepard.showMaximized() ret = app.exec_()