__main__.py 17.3 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
from .ramancom.ramancontrol import defaultPath
31
from .ramancom.lightModeSwitch import LightModeSwitch
32
from .gui.colorlegend import ColorLegend
33
from .gepardlogging import setDefaultLoggingConfig
Josef Brandt's avatar
Josef Brandt committed
34 35 36
from .workmodes import ModeHandler
from .unittests.test_gepard import testGepard

37

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

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

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

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 168 169 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 196 197 198 199 200 201 202 203 204
    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.ramanScanAct.setEnabled(rse)
        self.ramanScanAct.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.ramanScanAct.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)

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

209 210 211
        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
212

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

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

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

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

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

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

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

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

268 269 270
        self.ramanScanAct = QtWidgets.QAction("Raman Scan", self)
        self.ramanScanAct.setEnabled(False)
        self.ramanScanAct.setCheckable(True)
271 272
        self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("SpectrumScan")))

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

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

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

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

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

307
        self.disableParticleRelevantActs()
308 309 310 311

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

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

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

355 356 357 358 359
    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)
360
        self.toolbar.addAction(self.importAct)
361 362 363 364 365 366 367 368 369 370 371 372 373
        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.ramanScanAct)
        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
374

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

    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 ramanctrl 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.
        """
387
        app.closeAllWindows()
Josef Brandt's avatar
Josef Brandt committed
388

389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
    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__)
    
411
    app = QtWidgets.QApplication(sys.argv)
412
    app.setApplicationName("GEPARD")  # appname needed for logpath
413 414 415

    logpath = QtCore.QStandardPaths.writableLocation(
              QtCore.QStandardPaths.AppLocalDataLocation)
416
    fp = None
417 418 419 420
    if logpath != "":
        if not os.path.exists(logpath):
            os.mkdir(logpath)
        logname = os.path.join(logpath, 'logfile.txt')
421 422 423 424
        logger.addHandler(
            logging.handlers.RotatingFileHandler(
                logname, maxBytes=5*(1 << 20), backupCount=10)
        )
Josef Brandt's avatar
Josef Brandt committed
425
    logger.setLevel(logging.DEBUG)
426
    setDefaultLoggingConfig(logger)
427 428 429
    logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime()))
    
    gepard = GEPARDMainWindow(logger)
JosefBrandt's avatar
 
JosefBrandt committed
430
    gepard.showMaximized()
431

432
    ret = app.exec_()