main.py 29.1 KB
Newer Older
1
2
3
4
5
6
#!/usr/bin/env python
#
# fsleyes.py - Image viewer.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
7
"""This module provides the entry point to *FSLeyes*, the FSL image viewer.
8

9
10
11
12
13
Call the :func:`main` function to start the main FSLeyes application.

The :func:`embed` function can be called to open a :class:`.FSLeyesFrame`
within an existing application.

14
See the :mod:`fsleyes` package documentation for more details on ``fsleyes``.
15
16
17
18
19
20
21
22
23
24
25
26
27


.. note:: Even though ``fsleyes`` (this module) and :mod:`fsleyes.render` (the
          off-screen renderer) are intended to be separate applications, the
          current version of PyInstaller (3.x) does not support bundling of
          multiple executables
          (https://github.com/pyinstaller/pyinstaller/issues/1527).

          So at this point in time, :mod:`.fsleyes.render` can be invoked via
          ``fsleyes.main`` by passing ``'render'`` as the first argument,
          e.g.::

              python -m fsleyes.main render ...
28
29
30
"""


31
32
33
34
35
36
37
import functools as ft
import os.path   as op
import              os
import              sys
import              signal
import              logging
import              textwrap
38

39
import wx
Paul McCarthy's avatar
Paul McCarthy committed
40
import wx.adv
41

42
from   fsl.utils.platform import platform as fslplatform
43
import fsl.utils.idle                     as idle
44
import fsleyes_widgets                    as fwidgets
45
import fsleyes_widgets.utils.status       as status
46

47
import                       fsleyes
48
import fsleyes.strings    as strings
49
import fsleyes.splash     as fslsplash
50
import fsleyes.cliserver  as cliserver
51
import fsleyes.colourmaps as colourmaps
52

53

54
# wx.ModalDialogHook does not exist in wxPython < 4
55

56
if fwidgets.wxFlavour() in (fwidgets.WX_PYTHON, fwidgets.WX_UNKNOWN):
57
58
59
60
61
62
63
    class ModalDialogHook(object):
        def Register(self):
            pass

    wx.ModalDialogHook = ModalDialogHook


64
log = logging.getLogger(__name__)
65
66


Paul McCarthy's avatar
Paul McCarthy committed
67
class FSLeyesApp(wx.App):
68
69
    """FSLeyes-specific sub-class of ``wx.App``. """

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

    class ModalHook(wx.ModalDialogHook):
        """Keeps track of any modal dialogs/windows that are opened.

        Modal dialogs can interfere with shutdown, as they run their own event
        loop. Therefore we keep a reference is kept to all opened modal
        dialogs, so we can manually shut them down if needed (see the
        :func:`main` function).
        """

        def __init__(self, *args, **kwargs):
            wx.ModalDialogHook.__init__(self, *args, **kwargs)
            self.modals = set()

        def Enter(self, dlg):
            self.modals.add(dlg)
            return wx.ID_NONE

        def Exit(self, dlg):
89
            self.modals.discard(dlg)
90
91


92
    def __init__(self):
93
        """Create a ``FSLeyesApp``. """
94
95
96
97

        self.__overlayList = None
        self.__displayCtx  = None

98
99
100
101
102
103
104
105
        # On macOS, when the user drags a file onto the FSLeyes window,
        # or onto the FSLeyes.app icon, the file path will be passed to
        # MacOpenFiles method. But that method may be called very early
        # on in the startup process, before the DisplayContext and
        # OverlayList have been created. So when this happens, we cache
        # the files here, and then open them when the
        # SetOverlayListAndDisplayContext method gets called.
        self.__filesToOpen = []
Paul McCarthy's avatar
Paul McCarthy committed
106

107
108
109
        self.__modalHook = FSLeyesApp.ModalHook()
        self.__modalHook.Register()

110
        wx.App.__init__(self, clearSigInt=False)
111

112
113
        self.SetAppName('FSLeyes')

114
115
116
        try:
            self.__icon = wx.adv.TaskBarIcon(iconType=wx.adv.TBI_DOCK)
            self.__icon.SetIcon(wx.Icon(
Paul McCarthy's avatar
Paul McCarthy committed
117
                op.join(fsleyes.assetDir, 'icons', 'app_icon.png')))
118
119
        except Exception:
            self.__icon = None
Paul McCarthy's avatar
Paul McCarthy committed
120

121

122
123
124
125
126
127
    @property
    def modals(self):
        """Returns a list of all currently open modal windows. """
        return list(self.__modalHook.modals)


128
    def SetOverlayListAndDisplayContext(self, overlayList, displayCtx):
129
130
131
132
        """References to the :class:`.OverlayList` and master
        :class:`.DisplayContext` must be passed to the ``FSLeyesApp`` via this
        method.
        """
133
134
135
        self.__overlayList = overlayList
        self.__displayCtx  = displayCtx

136
137
138
139
140
141
142
143
        # MacOpenFiles was called before the
        # overlaylist/dc were created, and
        # queued some files that need to be
        # opened
        if len(self.__filesToOpen) > 0:
            wx.CallAfter(self.MacOpenFiles, self.__filesToOpen)
            self.__filesToOpen = None

144

145
    def MacReopenApp(self):
146
147
        """On OSX, make sure that the FSLeyes frame is restored if it is
        minimised, and (e.g.) the dock icon is clicked.
148
149
150
151
152
153
154
        """

        frame = self.GetTopWindow()
        frame.Iconize(False)
        frame.Raise()


155
156
157
158
159
160
161
    def MacOpenFile(self, filename):
        """On OSX, support opening files via context menu, and files dropped
        on the application icon.
        """
        self.MacOpenFiles([filename])


162
    def MacOpenURL(self, url):
163
        """On OSX, support opening files via a ``fsleyes://`` url. """
164
165
166
167

        if self.__overlayList is None:
            return

168
        import fsleyes_widgets.utils.status     as status
169
170
171
172
173
174
175
176
177
178
179
180
        import fsleyes.strings                  as strings
        import fsleyes.parseargs                as parseargs
        import fsleyes.actions.applycommandline as applycommandline

        errTitle = strings.titles[  self, 'openURLError']
        errMsg   = strings.messages[self, 'openURLError']

        with status.reportIfError(errTitle, errMsg):
            applycommandline.applyCommandLineArgs(
                self.__overlayList,
                self.__displayCtx,
                parseargs.fsleyesUrlToArgs(url))
181
182


183
184
185
186
187
    def MacOpenFiles(self, filenames):
        """On OSX, support opening files via context menu, and files dropped
        on the application icon.
        """

188
189
190
        # OverlayList has not yet been created -
        # queue the files to open them later
        # in SetOverlayListAndDisplayContext
191
        if self.__overlayList is None:
192
193
194
195
196
197
198
199

            # On certain systems (observed on Big Sur), when
            # files are passed as command-line arguments, they
            # are passed to MacOpenFiles. We don't want that,
            # because cli arguments are parsed separately. So
            # we remove dupes here.

            filenames = [f for f in filenames if f not in sys.argv]
200
            self.__filesToOpen.extend(filenames)
201
202
203
204
205
            return

        import fsleyes.actions.loadoverlay as loadoverlay
        import fsleyes.autodisplay         as autodisplay

206
        def onLoad(paths, overlays):
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221

            if len(overlays) == 0:
                return

            self.__overlayList.extend(overlays)

            if self.__displayCtx.autoDisplay:
                for overlay in overlays:
                    autodisplay.autoDisplay(overlay,
                                            self.__overlayList,
                                            self.__displayCtx)

        loadoverlay.loadOverlays(
            filenames,
            onLoad=onLoad,
222
            inmem=self.__displayCtx.loadInMemory)
223
224


225
def main(args=None):
226
    """*FSLeyes* entry point. Shows a :class:`.FSLeyesSplash` screen, parses
227
228
    command line arguments, and shows a :class:`.FSLeyesFrame`. Returns
    an exit code.
229
    """
230

231
232
    if args is None:
        args = sys.argv[1:]
233

234
235
236
237
238
239
240
    # Hack to allow render to
    # be called via fsleyes.main
    if len(args) >= 1 and args[0] == 'render':
        import fsleyes.render as render
        render.main(args[1:])
        sys.exit(0)

241
242
243
244
    # the fsleyes.initialise function figures
    # out the path to asset files (e.g. cmaps)
    fsleyes.initialise()

245
    # Hook which allows us to run a jupyter
246
247
    # notebook server from an existing FSLeyes
    # instance
248
    if len(args) >= 1 and args[0] == 'notebook':
249
        from fsleyes.actions.notebook import nbmain
250
        fsleyes.configLogging()
251
        sys.exit(nbmain(args))
252
253
254
255
256
257
258

    # initialise colour maps - this must be
    # done before parsing arguments, as if
    # the user asks for help, available
    # colourmaps/luts will be listed.
    colourmaps.init()

259
260
261
262
263
264
265
266
267
268
269
270
271
    # Function to bootstrap the GUI - keep
    # reading below.
    def initgui():

        # First thing's first. Create a wx.App,
        # and initialise the FSLeyes package.
        app = FSLeyesApp()

        # Create a splash screen frame
        splash = fslsplash.FSLeyesSplash(None)
        return app, splash

    # If it looks like the user is asking for
272
273
274
275
276
277
    # help, or using cliserver to pass arguments
    # to an existing FSLeyes instance, then we
    # parse command line arguments before
    # creating a wx.App and showing the splash
    # screen. This means that FSLeyes help/
    # version information can be retrieved
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
    # without a display, and hopefully fairly
    # quickly.
    #
    # Otherwise we create the app and splash
    # screen first, so the splash screen gets
    # shown as soon as possible. Arguments
    # will get parsed in the init function below.
    #
    # The argparse.Namespace object is kept in a
    # list so it can be shared between the sub-
    # functions below
    #
    # If argument parsing bombs out, we put the
    # exit code here and return it at the bottom.
    namespace = [None]
    exitCode  = [0]
294

295
    # user asking for help - parse args first
296
297
298
    if (len(args) > 0) and (args[0] in ('-V',
                                        '-h',
                                        '-fh',
299
                                        '-cs',
300
301
                                        '--version',
                                        '--help',
302
303
                                        '--fullhelp',
                                        '--cliserver')):
304
305
306
307
308
309
310
        namespace   = [parseArgs(args)]
        app, splash = initgui()

    # otherwise parse arguments on wx.MainLoop
    # below
    else:
        app, splash = initgui()
311
312
313

    # We are going do all processing on the
    # wx.MainLoop, so the GUI can be shown
314
    # as soon as possible, and because it is
315
316
    # difficult to force immediate GUI
    # refreshes when not running on the main
Paul McCarthy's avatar
Paul McCarthy committed
317
    # loop - this is important for FSLeyes,
318
319
320
    # which displays status updates to the
    # user while it is loading overlays and
    # setting up the interface.
321
    #
322
323
324
    # All of the work is defined in a series
    # of functions, which are chained together
    # via ugly callbacks, but which are
325
    # ultimately scheduled and executed on the
326
    # wx main loop.
327
    def init(splash):
328

329
330
331
332
333
334
335
        # See FSLeyesSplash.Show
        # for horribleness.
        splash.Show()

        # Parse command line arguments if necessary.
        # If arguments are invalid, the parseargs
        # module will raise SystemExit.
336
        try:
337
            if namespace[0] is None:
338
339
340
341
342

                errmsg   = strings.messages['main.parseArgs.error']
                errtitle = strings.titles[  'main.parseArgs.error']
                with status.reportIfError(errtitle, errmsg, raiseError=True):
                    namespace[0] = parseArgs(args)
343
344

        # But the wx.App.MainLoop eats SystemExit
345
        # exceptions for unknown reasons, and
346
        # causes the application to exit
347
348
349
350
        # immediately. This makes testing FSLeyes
        # (e.g. code coverage) impossible. So I'm
        # catching SystemExit here, and then
        # telling the wx.App to exit gracefully.
351
        except (SystemExit, Exception) as e:
352
            app.ExitMainLoop()
353
            exitCode[0] = getattr(e, 'code', 1)
354
            return
355

356
357
358
        # Configure logging (this has to be done
        # after cli arguments have been parsed,
        # but before initialise is called).
359
        fsleyes.configLogging(namespace[0].verbose, namespace[0].noisy)
360

361
362
363
364
        # Initialise sub-modules/packages. The
        # buildGui function is passed through
        # as a callback, which gets called when
        # initialisation is complete.
365
366
367
        initialise(splash, namespace[0], buildGui)

    def buildGui():
368
369
370

        # Now the main stuff - create the overlay
        # list and the master display context,
Paul McCarthy's avatar
Paul McCarthy committed
371
        # and then create the FSLeyesFrame.
372
        overlayList, displayCtx = makeDisplayContext(namespace[0], splash)
373
        app.SetOverlayListAndDisplayContext(overlayList, displayCtx)
374
375
376
377
378
        frame = makeFrame(namespace[0],
                          displayCtx,
                          overlayList,
                          splash,
                          [shutdown])
379
380
381
382

        app.SetTopWindow(frame)
        frame.Show()

383
        # Check that $FSLDIR is set, complain
384
        # to the user if it isn't
385
        if not namespace[0].skipfslcheck:
386
387
            wx.CallAfter(fslDirWarning, frame)

388
389
390
        # Check for updates. Ignore point
        # releases, otherwise users might
        # get swamped with update notifications.
391
        if namespace[0].updatecheck:
Paul McCarthy's avatar
Paul McCarthy committed
392
393
            import fsleyes.actions.updatecheck as updatecheck
            wx.CallAfter(updatecheck.UpdateCheckAction(),
394
                         showUpToDateMessage=False,
395
                         showErrorMessage=False,
396
                         ignorePoint=False)
Paul McCarthy's avatar
Paul McCarthy committed
397

398
        # start notebook server
399
400
401
        if namespace[0].notebookFile is not None:
            namespace[0].notebook     = True
            namespace[0].notebookFile = op.abspath(namespace[0].notebookFile)
402
403
        if namespace[0].notebook:
            from fsleyes.actions.notebook import NotebookAction
404
            frame.menuActions[NotebookAction](namespace[0].notebookFile)
405

406
407
408
409
        # start CLI server
        if namespace[0].cliserver:
            cliserver.runserver(overlayList, displayCtx)

410
411
412
413
    # Shut down cleanly on sigint/sigterm.
    # We do this so that any functions
    # registered with atexit will actually
    # get called.
414
415
    nsignals = [0]

416
    def sigHandler(signo, frame):
Paul McCarthy's avatar
Paul McCarthy committed
417
        log.debug('Signal received - FSLeyes is shutting down...')
418
419
420
421
422

        # first signal - try to exit cleanly
        if nsignals[0] == 0:
            nsignals[0] += 1
            exitCode[0]  = signo
423
424
425
426
427
428
429

            # kill any modal windows
            # that are open
            for mdlg in app.modals:
                mdlg.EndModal(wx.ID_CANCEL)

            wx.CallAfter(app.ExitMainLoop)
430
431
432
433

        # subsequent signals - exit immediately
        else:
            sys.exit(signo)
434
435
436
437

    signal.signal(signal.SIGINT,  sigHandler)
    signal.signal(signal.SIGTERM, sigHandler)

438
439
440
441
442
443
    # Note: If no wx.Frame is created, the
    # wx.MainLoop call will exit immediately,
    # even if we have scheduled something via
    # wx.CallAfter. In this case, we have
    # already created the splash screen, so
    # all is well.
444
    wx.CallAfter(init, splash)
445
446
447
448
449
450
451
452
453
454
455
456
457
458

    # under mac, use appnope to make sure
    # we don't get put to sleep. This is
    # primarily for the jupyter notebook
    # integration - if the user is working
    # with a notebook in the web browser,
    # macos might put FSLeyes to sleep,
    # causing the kernel to become
    # unresponsive.
    try:
        import appnope
        appnope.nope()
    except ImportError:
        pass
459
    app.MainLoop()
460
    return exitCode[0]
461
462


463
def embed(mkFrame=True, **kwargs):
464
465
466
    """Initialise FSLeyes and create a :class:`.FSLeyesFrame`, when
    running within another application.

467
468
    .. note:: In most cases, this function must be called from the
              ``wx.MainLoop``.
469

470
471
472
473
    :arg mkFrame: Defaults to ``True``. If ``False``, FSLeyes is
                  initialised, but a :class:`.FSLeyesFrame` is not created.
                  If you set this to ``False``, you must ensure that a
                  ``wx.App`` object exists before calling this function.
474

475
476
477
478
479
480
    :returns:     A tuple containing:

                   - The :class:`.OverlayList`
                   - The master :class:`.DisplayContext`
                   - The :class:`.FSLeyesFrame` (or ``None``, if
                     ``makeFrame is False``).
481
482
483
484
485
486
487
488
489
490

    All other arguments are passed to :meth:`.FSLeyesFrame.__init__`.
    """

    import fsleyes_props          as props
    import fsleyes.gl             as fslgl
    import fsleyes.frame          as fslframe
    import fsleyes.overlay        as fsloverlay
    import fsleyes.displaycontext as fsldc

491
492
493
494
495
    # initialise must be called before
    # a FSLeyesApp gets created, as it
    # tries to access app_icon.png
    fsleyes.initialise()

496
    app    = wx.GetApp()
497
498
    ownapp = app is None

499
500
    if ownapp and (mkFrame is False):
        raise RuntimeError('If mkFrame is False, you '
501
502
                           'must create a wx.App before '
                           'calling fsleyes.main.embed')
503
    if ownapp:
504
        app = FSLeyesApp()
505

506
507
508
    colourmaps.init()
    props.initGUI()

509
510
511
512
513
514
    called = [False]
    ret    = [None]

    def until():
        return called[0]

515
516
517
518
519
    def ready():
        fslgl.bootstrap()

        overlayList = fsloverlay.OverlayList()
        displayCtx  = fsldc.DisplayContext(overlayList)
520

521
        if mkFrame:
522
            frame = fslframe.FSLeyesFrame(
523
                None, overlayList, displayCtx, **kwargs)
524
525
        else:
            frame = None
526

527
        if ownapp:
528
529
            app.SetOverlayListAndDisplayContext(overlayList, displayCtx)
            # Keep a ref to prevent the app from being GC'd
530
531
532
533
            frame._embed_app = app

        called[0] = True
        ret[0]    = (overlayList, displayCtx, frame)
534

535
    fslgl.getGLContext(ready=ready, raiseErrors=True)
536
537
538
539
540
    idle.block(10, until=until)

    if ret[0] is None:
        raise RuntimeError('Failed to start FSLeyes')
    return ret[0]
541
542


543
def initialise(splash, namespace, callback):
Paul McCarthy's avatar
Paul McCarthy committed
544
545
    """Called by :func:`main`. Bootstraps/Initialises various parts of
    *FSLeyes*.
546
547
548
549

    The ``callback`` function is asynchronously called when the initialisation
    is complete.

Paul McCarthy's avatar
Paul McCarthy committed
550
    :arg splash:    The :class:`.FSLeyesSplash` screen.
551
552
553
554
555

    :arg namespace: The ``argparse.Namespace`` object containing parsed
                    command line arguments.

    :arg callback:  Function which is called when initialisation is done.
Paul McCarthy's avatar
Paul McCarthy committed
556
    """
557

558
    import fsl.utils.settings as fslsettings
559
    import fsleyes_props      as props
Paul McCarthy's avatar
Paul McCarthy committed
560
    import fsleyes.gl         as fslgl
561
562
563

    props.initGUI()

564
565
566
567
568
569
    # The save/load directory defaults
    # to the current working directory.
    curDir = op.normpath(os.getcwd())

    fslsettings.write('loadSaveOverlayDir', curDir)

Paul McCarthy's avatar
Paul McCarthy committed
570
571
    # Initialise silly things
    if namespace.bumMode:
Paul McCarthy's avatar
Paul McCarthy committed
572
573
        import fsleyes.icons as icons
        icons.BUM_MODE = True
Paul McCarthy's avatar
Paul McCarthy committed
574

575
576
577
    # Set notebook server port
    fslsettings.write('fsleyes.notebook.port', namespace.notebookPort)

578
579
    # This is called by fsleyes.gl.getGLContext
    # when the GL context is ready to be used.
580
    def realCallback():
581
        fslgl.bootstrap(namespace.glversion)
582
        callback()
583

584
    try:
585
586
        # Force the creation of a wx.glcanvas.GLContext object,
        # and initialise OpenGL version-specific module loads.
Paul McCarthy's avatar
Paul McCarthy committed
587
        fslgl.getGLContext(ready=realCallback)
588

Paul McCarthy's avatar
Paul McCarthy committed
589
    except Exception:
590
591
592
        log.error('Unable to initialise OpenGL!', exc_info=True)
        splash.Destroy()
        sys.exit(1)
593
594


595
def shutdown(ev=None):
596
597
    """Called when FSLeyes exits normally (i.e. the user closes the window).
    Does some final clean-up before exiting.
598
599
600

    This function is used as a wxpython event handler, so it accepts an ``ev``
    arguments, but ignores its value.
601
602
    """

603
604
605
    if ev is not None:
        ev.Skip()

606
    import fsl.utils.settings as fslsettings
607
    import fsleyes.gl         as fslgl
608

609
610
611
612
    # Clear the cached directory for loading/saving
    # files - when FSLeyes starts up, we want it to
    # default to the current directory.
    fslsettings.delete('loadSaveOverlayDir')
613

614
615
616
    # Shut down the GL rendering context
    fslgl.shutdown()

617
618
619

def parseArgs(argv):
    """Parses the given ``fsleyes`` command line arguments. See the
620
    :mod:`.parseargs` module for details on the ``fsleyes`` command
621
    line interface.
622

623
624
625
    :arg argv: command line arguments for ``fsleyes``.
    """

626
627
628
    import fsleyes.parseargs as parseargs
    import fsleyes.layouts   as layouts
    import fsleyes.version   as version
629

630
    parser = parseargs.ArgumentParser(
631
        add_help=False,
Paul McCarthy's avatar
Paul McCarthy committed
632
        formatter_class=parseargs.FSLeyesHelpFormatter)
633

634
635
    serveraction = ft.partial(cliserver.CLIServerAction, allArgs=argv)

636
637
638
    parser.add_argument('-r', '--runscript',
                        metavar='SCRIPTFILE',
                        help='Run custom FSLeyes script')
639
    parser.add_argument('-cs', '--cliserver',
640
                        action=serveraction,
641
642
                        help='Pass all command-line arguments '
                             'to a single FSLeyes instance')
643

644
    # We include the list of available
645
646
647
    # layouts in the help description
    allLayouts  = list(layouts.BUILT_IN_LAYOUTS.keys()) + \
                  list(layouts.getAllLayouts())
648
649
    name        = 'fsleyes'
    prolog      = 'FSLeyes version {}\n'.format(version.__version__)
650
651
    description = textwrap.dedent("""\
        FSLeyes - the FSL image viewer.
652

653
        Use the '--scene' option to load a saved layout ({layouts}).
654

655
656
657
658
        If no '--scene' is specified, a default layout is shown or the
        previous layout is restored. If a script is provided via
        the '--runscript' argument, it is assumed that the script sets
        up the scene.
659
        """.format(layouts=', '.join(allLayouts)))
660
661

    # Options for configuring the scene are
662
    # managed by the parseargs module
663
664
665
    return parseargs.parseArgs(parser,
                               argv,
                               name,
666
667
                               prolog=prolog,
                               desc=description,
668
                               argOpts=['-r', '--runscript'])
669
670


671
672
673
674
def makeDisplayContext(namespace, splash):
    """Creates the top-level *FSLeyes* :class:`.DisplayContext` and
    :class:`.OverlayList` .

675
    This function does the following:
676

677
     1. Creates the :class:`.OverlayList` and the top level
678
679
        :class:`.DisplayContext`.

680
681
     2. Loads and configures all of the overlays which were passed in on the
        command line.
682

683
    :arg namesace: Parsed command line arguments (see :func:`parseArgs`).
684

Paul McCarthy's avatar
Paul McCarthy committed
685
    :arg splash:   The :class:`.FSLeyesSplash` frame, created in :func:`init`.
686
687
688
689
690
691

    :returns: a tuple containing:
                - the :class:`.OverlayList`
                - the master :class:`.DisplayContext`
    """

692
693
694
695
    import fsleyes_widgets.utils.status as status
    import fsleyes.overlay              as fsloverlay
    import fsleyes.parseargs            as parseargs
    import fsleyes.displaycontext       as displaycontext
696

697
698
699
700
    # Splash status update must be
    # performed on the main thread.
    def splashStatus(msg):
        wx.CallAfter(splash.SetStatus, msg)
701

702
703
    # Redirect status updates
    # to the splash frame
704
    status.setTarget(splashStatus)
705
706
707
708
709

    # Create the overlay list (only one of these
    # ever exists) and the master DisplayContext.
    # A new DisplayContext instance will be
    # created for every new view that is opened
Paul McCarthy's avatar
Paul McCarthy committed
710
    # in the FSLeyesFrame, but all child
Paul McCarthy's avatar
Paul McCarthy committed
711
712
    # DisplayContext instances will be linked to
    # this master one.
713
714
715
716
717
    overlayList = fsloverlay.OverlayList()
    displayCtx  = displaycontext.DisplayContext(overlayList)

    log.debug('Created overlay list and master DisplayContext ({})'.format(
        id(displayCtx)))
718
719

    # Load the images - the splash screen status will
720
    # be updated with the currently loading overlay name.
721
    parseargs.applyMainArgs(   namespace, overlayList, displayCtx)
722
    parseargs.applyOverlayArgs(namespace, overlayList, displayCtx)
723

724
    return overlayList, displayCtx
725
726


727
def makeFrame(namespace, displayCtx, overlayList, splash, closeHandlers):
728
    """Creates the *FSLeyes* interface.
729
730
731

    This function does the following:

Paul McCarthy's avatar
Paul McCarthy committed
732
     1. Creates the :class:`.FSLeyesFrame` the top-level frame for ``fsleyes``.
733

734
     2. Configures the frame according to the command line arguments (e.g.
735
736
737
738
739
        ortho or lightbox view).

     3. Destroys the splash screen that was created by the :func:`context`
        function.

740
741
742
743
744
    :arg namespace:     Parsed command line arguments, as returned by
                        :func:`parseArgs`.

    :arg displayCtx:    The  :class:`.DisplayContext`, as created and returned
                        by :func:`makeDisplayContext`.
745

746
747
    :arg overlayList:   The :class:`.OverlayList`, as created and returned by
                        :func:`makeDisplayContext`.
748

749
    :arg splash:        The :class:`.FSLeyesSplash` frame.
750

751
752
    :arg closeHandlers: List of event handlers to be called when the
                        ``FSLeyesFrame`` closes.
753

Paul McCarthy's avatar
Paul McCarthy committed
754
    :returns: the :class:`.FSLeyesFrame` that was created.
755
756
    """

757
758
759
760
761
762
763
764
765
    import fsl.utils.idle                        as idle
    import fsleyes_widgets.utils.status          as status
    import fsleyes.parseargs                     as parseargs
    import fsleyes.frame                         as fsleyesframe
    import fsleyes.displaycontext                as fsldisplay
    import fsleyes.layouts                       as layouts
    import fsleyes.views.canvaspanel             as canvaspanel
    import fsleyes.views.orthopanel              as orthopanel
    import fsleyes.plugins.tools.saveannotations as saveannotations
766

767
    # Set up the frame scene (a.k.a. layout)
768
    # The scene argument can be:
769
    #
770
    #   - The name of a saved (or built-in) layout
771
    #
772
773
774
    #   - None, in which case the default or previous
    #     layout is restored, unless a custom script
    #     has been provided.
775
    script = namespace.runscript
776
    scene  = namespace.scene
777

778
    # If a scene/layout or custom script
779
780
    # has not been specified, the default
    # behaviour is to restore the previous
781
    # frame layout.
782
783
784
    restore = (scene is None) and (script is None)

    status.update('Creating FSLeyes interface...')
785

Paul McCarthy's avatar
Paul McCarthy committed
786
    frame = fsleyesframe.FSLeyesFrame(
787
788
789
790
791
        None,
        overlayList,
        displayCtx,
        restore,
        True,
792
793
        fontSize=namespace.fontSize,
        closeHandlers=closeHandlers)
Paul McCarthy's avatar
Paul McCarthy committed
794
795
796

    # Allow files to be dropped
    # onto FSLeyes to open them
797
798
    dt = fsleyesframe.OverlayDropTarget(overlayList, displayCtx)
    frame.SetDropTarget(dt)
Paul McCarthy's avatar
Paul McCarthy committed
799
800
801

    # Make sure the new frame is shown
    # before destroying the splash screen
802
803
804
805
    frame.Show(True)
    frame.Refresh()
    frame.Update()

806
807
808
809
810
811
812
    # In certain instances under Linux/GTK,
    # closing the splash screen will crash
    # the application. No idea why. So we
    # leave the splash screen hidden, but
    # not closed, and close it when the main
    # frame is closed. This also works under
    # OSX.
813
814
815
    splash.Hide()
    splash.Refresh()
    splash.Update()
816

817
818
    def onFrameDestroy(ev):
        ev.Skip()
819
820
821
822
823
824

        # splash screen may already
        # have been destroyed
        try:              splash.Close()
        except Exception: pass

825
    frame.Bind(wx.EVT_WINDOW_DESTROY, onFrameDestroy)
826
827
828
829
830
831

    status.update('Setting up scene...')

    # Set the default SceneOpts.performance
    # level so that all created SceneOpts
    # instances will default to it
832
    if namespace.performance is not None:
833
        fsldisplay.SceneOpts.performance.setAttribute(
834
            None, 'default', namespace.performance)
835

836
837
    # If a layout has been specified,
    # we load the layout
838
    if namespace.scene is not None:
839
        layouts.loadLayout(frame, namespace.scene)
840
841

    # Apply any view-panel specific arguments
Paul McCarthy's avatar
Paul McCarthy committed
842
    viewPanels = frame.viewPanels
843
844
    for viewPanel in viewPanels:

845
        if not isinstance(viewPanel, canvaspanel.CanvasPanel):
846
847
            continue

Paul McCarthy's avatar
Paul McCarthy committed
848
849
        displayCtx = viewPanel.displayCtx
        viewOpts   = viewPanel.sceneOpts
850

851
852
        parseargs.applySceneArgs(
            namespace, overlayList, displayCtx, viewOpts)
853

854
855
856
857
858
859
860
861
862
863
864
865
    # If an annotations file has eben specified,
    # and an ortho view was opened, load the
    # annotations file, and apply it to the
    # first ortho view
    orthos = [vp for vp in viewPanels if isinstance(vp, orthopanel.OrthoPanel)]
    if namespace.annotations is not None and len(orthos) > 0:
        try:
            saveannotations.loadAnnotations(orthos[0], namespace.annotations)
        except Exception as e:
            log.warning('Error loading annotations from %s: %s',
                        namespace.annotations, e, exc_info=True)

866
867
868
869
    # If a script has been specified, we run
    # the script. This has to be done on the
    # idle loop, because overlays specified
    # on the command line are loaded on the
870
871
872
873
    # idle loop. Therefore, if we schedule the
    # script on idle (which is a queue), the
    # script can assume that all overlays have
    # already been loaded.
874
    from fsleyes.actions.runscript import RunScriptAction
875
    if script is not None:
876
        idle.idle(frame.menuActions[RunScriptAction], script)
877

878
879
880
    return frame


881
def fslDirWarning(parent):
882
883
884
885
    """Checks to see if the ``$FSLDIR`` environment variable is set, or
    if a FSL installation directory has been saved previously. If not,
    displays a warning via a :class:`.FSLDirDialog`.

886
    :arg parent: A ``wx`` parent object.
887
888
    """

889
890
    if fslplatform.fsldir is not None:
        return
891

892
    import fsl.utils.settings as fslsettings
893

894
    # Check settings before
895
896
897
898
    # prompting the user
    fsldir = fslsettings.read('fsldir')

    if fsldir is not None:
899
        fslplatform.fsldir = fsldir
900
901
        return

902
    from fsleyes_widgets.dialog import FSLDirDialog
903

904
    dlg = FSLDirDialog(parent, 'FSLeyes', fslplatform.os == 'Darwin')
905

906
    if dlg.ShowModal() == wx.ID_OK:
907

908
        fsldir = dlg.GetFSLDir()
909

910
911
912
        log.debug('Setting $FSLDIR to {} (specified '
                  'by user)'.format(fsldir))

913
914
        fslplatform.fsldir        = fsldir
        fslsettings.write('fsldir', fsldir)
915
916
917


if __name__ == '__main__':
918
    main()