__main__.py 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018  Lars Bittrich and Josef Brandt, Leibniz-Institut für 
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>    

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 <https://www.gnu.org/licenses/>.
"""
21
import logging
22
import logging.handlers
23
import traceback
24
import os
25
from io import StringIO
26
from typing import List
27
from PyQt5 import QtCore, QtWidgets, QtGui
28
from .sampleview import SampleView
29
from .gui.scalebar import ScaleBar
30 31
from .instrumentcom.instrumentConfig import defaultPath
from .instrumentcom.lightModeSwitch import LightModeSwitch
32
from .gui.colorlegend import ColorLegend
33
from .gepardlogging import setDefaultLoggingConfig
Josef Brandt's avatar
Josef Brandt committed
34 35
from .workmodes import ModeHandler
from .unittests.test_gepard import testGepard
36
from .helperfunctions import getAppFolder
Josef Brandt's avatar
Josef Brandt committed
37

38

JosefBrandt's avatar
 
JosefBrandt committed
39
class GEPARDMainWindow(QtWidgets.QMainWindow):
40
    def __init__(self, logger):
JosefBrandt's avatar
 
JosefBrandt committed
41
        super(GEPARDMainWindow, self).__init__()
42 43

        self.setWindowTitle("GEPARD")
44
        self.fname: str = ''
45 46
        self.resize(900, 700)
        self.scalebar = ScaleBar(self)
47
        self.legend = ColorLegend(self)
48
        self.lightModeSwitch = LightModeSwitch(self)
49 50
        self.view = SampleView(self, logger)
        self.view.ScalingChanged.connect(self.scalingChanged)
51 52 53 54
        self.view.ScalingChanged.connect(self.scalebar.updateScale)
        
        mdiarea = QtWidgets.QMdiArea(self)
        mdiarea.addSubWindow(self.scalebar)
55
        mdiarea.addSubWindow(self.legend)
56
        mdiarea.addSubWindow(self.lightModeSwitch)
57
        self.legend.hide()
58
        self.lightModeSwitch.hide()
59
        
60 61 62 63 64 65 66 67 68 69
        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()
Josef Brandt's avatar
Josef Brandt committed
70
        self.modeHandler: ModeHandler = ModeHandler(self)
71
        self.modeHandler.modeSwitched.connect(self.view.viewItemHandler.switchToNewMode)
72 73 74
        self.updateModes()
        
    def resizeEvent(self, event):
75
        self.scalebar.move(0, self.height()-self.scalebar.height()-30)
76
        self.legend.move(self.width()-self.legend.width()-130, 10)
77
        self.lightModeSwitch.move(5, 5)
78 79 80
        
    def closeEvent(self, event):
        self.view.closeEvent(event)
81 82

    def forceCloseAll(self) -> None:
Josef Brandt's avatar
Josef Brandt committed
83
        closeAll()
84 85
        
    @QtCore.pyqtSlot(float) 
86
    def scalingChanged(self):
87 88 89 90 91 92 93
        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:
Josef Brandt's avatar
Josef Brandt committed
94
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Open Project", defaultPath, "*.pkl")[0]
95 96 97
        if fileName:
            self.fname = str(fileName)
            self.view.open(self.fname)  
98
            self.scalingChanged()
99 100 101 102
    
    @QtCore.pyqtSlot()         
    def importProject(self, fileName=False):
        if fileName is False:
103
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Import Zeiss Zen Project", defaultPath, "*.xml")[0]
104 105 106
        if fileName:
            self.fname = str(fileName)
            self.view.importProject(self.fname)  
107
            self.scalingChanged()
108 109 110 111
            
    @QtCore.pyqtSlot() 
    def new(self, fileName=False):
        if fileName is False:
112
            fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project", defaultPath, "*.pkl")[0]
113
        if fileName:
114 115
            isValid, msg = self.testFilename(fileName)
            if isValid:
Hackmet's avatar
Hackmet committed
116
                self.fname = str(fileName)
117
                self.view.new(self.fname)  
118
                self.scalingChanged()
119 120
            else:
                QtWidgets.QMessageBox.critical(self, "Error", msg)
Josef Brandt's avatar
 
Josef Brandt committed
121 122
            
    @QtCore.pyqtSlot()  
123
    def testFilename(self, fileName):
124
        if self.view.instrctrl.name == 'RenishawCOM':  # the renishawCom does not allow Spaces within filePath
125 126 127 128 129 130
            if fileName.find(' ') == 0:
                return False, "File path must not contain spaces."
            else:
                return True, ""
        else:
            return True, ""
Josef Brandt's avatar
 
Josef Brandt committed
131
    
132 133
    @QtCore.pyqtSlot() 
    def about(self):
134 135
        QtWidgets.QMessageBox.about(self, 'GEPARD', "Developed by Complex Fiber Structures GmbH "
                                                    "on behalf of Leibniz-IPF Dresden")
136

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    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)
168 169
        self.specScanAct.setEnabled(rse)
        self.specScanAct.setChecked(rsc)
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

    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)
196
        self.specScanAct.setEnabled(False)
197 198 199 200 201 202 203 204 205
        
    def updateConnected(self, connected):
        if connected:
            self.connectRamanAct.setEnabled(False)
            self.disconnectRamanAct.setEnabled(True)
        else:
            self.connectRamanAct.setEnabled(True)
            self.disconnectRamanAct.setEnabled(False)

206
    def createActions(self):
207 208
        def runGepardTest(gebbard):
            return lambda: testGepard(gebbard)
Josef Brandt's avatar
Josef Brandt committed
209

210 211 212
        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)
Josef Brandt's avatar
Josef Brandt committed
213

214 215 216
        self.openAct = QtWidgets.QAction("&Open Project...", self)
        self.openAct.setShortcut("Ctrl+O")
        self.openAct.triggered.connect(self.open)
217

218
        self.importAct = QtWidgets.QAction("&Import Zeiss Project...", self)
219 220
        self.importAct.setShortcut("Ctrl+I")
        self.importAct.triggered.connect(self.importProject)
221

222 223 224 225 226 227 228 229 230
        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)
231
        self.zoomInAct.setShortcut("Ctrl++")
232 233 234 235
        self.zoomInAct.setEnabled(False)
        self.zoomInAct.triggered.connect(self.view.zoomIn)

        self.zoomOutAct = QtWidgets.QAction("Zoom &Out (25%)", self)
236
        self.zoomOutAct.setShortcut("Ctrl+-")
237 238 239 240 241 242 243 244 245 246 247 248
        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)
249

250 251 252
        self.connectRamanAct = QtWidgets.QAction("Connect to Microscope", self)
        self.connectRamanAct.setEnabled(True)
        self.connectRamanAct.triggered.connect(self.view.connectRaman)
253

254 255 256
        self.disconnectRamanAct = QtWidgets.QAction("Release Microscope", self)
        self.disconnectRamanAct.setEnabled(False)
        self.disconnectRamanAct.triggered.connect(self.view.disconnectRaman)
257

258 259 260
        self.opticalScanAct = QtWidgets.QAction("Optical Scan", self)
        self.opticalScanAct.setEnabled(False)
        self.opticalScanAct.setCheckable(True)
261
        self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("OpticalScan")))
262 263 264 265

        self.detectParticleAct = QtWidgets.QAction("Detect Particles", self)
        self.detectParticleAct.setEnabled(False)
        self.detectParticleAct.setCheckable(True)
266 267 268
        self.detectParticleAct.triggered.connect(
            QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("ParticleDetection")))

269 270 271 272
        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")))
273

274
        self.snapshotAct = QtWidgets.QAction("&Save Screenshot", self)
275 276
        self.snapshotAct.triggered.connect(self.view.takeScreenshot)
        self.snapshotAct.setDisabled(True)
277

278
        self.configRamanCtrlAct = QtWidgets.QAction("&Configure Raman Control", self)
279
        self.configRamanCtrlAct.triggered.connect(self.view.configureInstrumentControl)
280 281
        if self.view.simulatedRaman:
            self.configRamanCtrlAct.setDisabled(True)
282

283
        self.testAct: QtWidgets.QAction = QtWidgets.QAction("&Run Automated Gepard Test")
284
        self.testAct.setShortcut("Ctrl+T")
285 286
        self.testAct.triggered.connect(runGepardTest(self))

287
        self.noOverlayAct = QtWidgets.QAction("&No Overlay", self)
288
        self.noOverlayAct.setShortcut("1")
289 290
        self.noOverlayAct.setCheckable(True)
        self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self)  # TODO: Is that needed??
291
        self.selOverlayAct.setShortcut("2")
292
        self.selOverlayAct.setCheckable(True)
293
        self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self)
294
        self.fullOverlayAct.setShortcut("3")
295
        self.fullOverlayAct.setCheckable(True)
296
        self.transpAct = QtWidgets.QAction("&Transparent Overlay", self)
297
        self.transpAct.setShortcut("T")
298 299 300 301
        self.transpAct.setCheckable(True)
        self.transpAct.setChecked(False)
        self.transpAct.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility)

302
        self.hideLabelAct = QtWidgets.QAction('&Hide Spectra Numbers', self)
303
        self.hideLabelAct.setShortcut("H")
304
        self.darkenAct = QtWidgets.QAction("&Darken Image", self)
305
        self.darkenAct.setShortcut("D")
306
        self.seedAct = QtWidgets.QAction("&Set Color Seed", self)
307

308
        self.disableParticleRelevantActs()
309 310 311 312

    def createMenus(self):
        self.fileMenu = QtWidgets.QMenu("&File", self)
        self.fileMenu.addAction(self.newAct)
313
        self.fileMenu.addAction(self.importAct)
314 315 316
        self.fileMenu.addAction(self.openAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)
317
        
318 319 320 321 322 323 324 325 326 327 328
        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)
        
Hackmet's avatar
Hackmet committed
329
        self.toolsMenu = QtWidgets.QMenu("&Tools")
330 331
        self.toolsMenu.addAction(self.snapshotAct)
        self.toolsMenu.addAction(self.configRamanCtrlAct)
332
        self.toolsMenu.addAction(self.testAct)
333 334 335 336

        self.dispMenu = QtWidgets.QMenu("&Display", self)
        self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu)
        self.overlayActGroup.setExclusive(True)
337
        self.overlayActGroup.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility)
338 339 340 341
        
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct]:
            self.dispMenu.addAction(act)
            self.overlayActGroup.addAction(act)
342
        self.fullOverlayAct.setChecked(True)
343 344
        self.dispMenu.addSeparator()
        self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct])
345
        
346 347
        self.helpMenu = QtWidgets.QMenu("&Help", self)
        self.helpMenu.addAction(self.aboutAct)
348

349 350 351
        self.menuBar().addMenu(self.fileMenu)
        self.menuBar().addMenu(self.viewMenu)
        self.menuBar().addMenu(self.modeMenu)
352
        self.menuBar().addMenu(self.toolsMenu)
353
        self.menuBar().addMenu(self.dispMenu)
354
        self.menuBar().addMenu(self.helpMenu)
355

356 357 358 359 360
    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)
361
        self.toolbar.addAction(self.importAct)
362 363 364 365 366 367 368
        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)
369
        self.toolbar.addAction(self.specScanAct)
370 371 372 373 374
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.exitAct)
        self.toolbar.setOrientation(QtCore.Qt.Vertical)
        self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar)

Josef Brandt's avatar
Josef Brandt committed
375

376 377 378
if __name__ == '__main__':
    import sys
    from time import localtime, strftime
Josef Brandt's avatar
Josef Brandt committed
379 380 381 382 383

    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
384
        ONLY close it at the end of running the program. Closing it on disconnect of the instrctrl is not suitable,
Josef Brandt's avatar
Josef Brandt committed
385 386 387
        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.
        """
388
        app.closeAllWindows()
Josef Brandt's avatar
Josef Brandt committed
389

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    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__)
    
412
    app = QtWidgets.QApplication(sys.argv)
413
    logpath = getAppFolder()
414 415 416 417
    if logpath != "":
        if not os.path.exists(logpath):
            os.mkdir(logpath)
        logname = os.path.join(logpath, 'logfile.txt')
418 419 420 421
        logger.addHandler(
            logging.handlers.RotatingFileHandler(
                logname, maxBytes=5*(1 << 20), backupCount=10)
        )
Josef Brandt's avatar
Josef Brandt committed
422
    logger.setLevel(logging.DEBUG)
423
    setDefaultLoggingConfig(logger)
424 425 426
    logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime()))
    
    gepard = GEPARDMainWindow(logger)
JosefBrandt's avatar
 
JosefBrandt committed
427
    gepard.showMaximized()
428

429
    ret = app.exec_()