/
Control.py
2960 lines (2408 loc) · 99.9 KB
/
Control.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Control.py -- Controller for the Ginga reference viewer.
#
# This is open-source software licensed under a BSD license.
# Please see the file LICENSE.txt for details.
#
# stdlib imports
import sys
import os
import traceback
import time
import tempfile
import threading
import logging
import platform
import atexit
import shutil
import inspect
from collections import deque, OrderedDict
import _thread as thread # noqa
import queue as Queue # noqa
# Local application imports
from ginga import cmap, imap
from ginga.misc import Bunch, Timer, Future
from ginga.util import catalog, iohelper, loader, toolbox
from ginga.util import viewer as gviewer
from ginga.canvas.CanvasObject import drawCatalog
from ginga.canvas.types.layer import DrawingCanvas
# GUI imports
from ginga.gw import GwHelp, GwMain, PluginManager
from ginga.gw import Widgets, Viewers, Desktop
from ginga import toolkit
from ginga.fonts import font_asst
# Version
from ginga import __version__
# Reference viewer
from ginga.rv.Channel import Channel
have_docutils = False
try:
from docutils.core import publish_string
have_docutils = True
except ImportError:
pass
#pluginconfpfx = 'plugins'
pluginconfpfx = None
package_home = os.path.split(sys.modules['ginga.version'].__file__)[0]
# pick up plugins specific to our chosen toolkit
tkname = toolkit.get_family()
if tkname is not None:
# TODO: this relies on a naming convention for widget directories!
# TODO: I think this can be removed, since the widget specific
# plugin directories have been deleted
child_dir = os.path.join(package_home, tkname + 'w', 'plugins')
sys.path.insert(0, child_dir)
icon_path = os.path.abspath(os.path.join(package_home, 'icons'))
class ControlError(Exception):
pass
class GingaViewError(Exception):
pass
class GingaShell(GwMain.GwMain, Widgets.Application):
"""
Main Ginga shell for housing plugins and running the reference
viewer.
"""
def __init__(self, logger, thread_pool, module_manager, preferences,
ev_quit=None):
GwMain.GwMain.__init__(self, logger=logger, ev_quit=ev_quit,
app=self, thread_pool=thread_pool)
# Create general preferences
self.prefs = preferences
settings = self.prefs.create_category('general')
settings.add_defaults(fixedFont=None,
serifFont=None,
sansFont=None,
channel_follows_focus=False,
scrollbars='off',
numImages=10,
# Offset to add to numpy-based coords
pixel_coords_offset=1.0,
# inherit from primary header
inherit_primary_header=False,
cursor_interval=0.050,
download_folder=None,
save_layout=False,
channel_prefix="Image")
settings.load(onError='silent')
# this will set self.logger and self.settings
Widgets.Application.__init__(self, logger=logger, settings=settings)
self.mm = module_manager
# event for controlling termination of threads executing in this
# object
if not ev_quit:
self.ev_quit = threading.Event()
else:
self.ev_quit = ev_quit
self.tmpdir = tempfile.mkdtemp()
# remove temporary directory on exit
atexit.register(_rmtmpdir, self.tmpdir)
# For callbacks
for name in ('add-image', 'channel-change', 'remove-image',
'add-channel', 'delete-channel', 'field-info',
'add-image-info', 'remove-image-info'):
self.enable_callback(name)
# Initialize the timer factory
self.timer_factory = Timer.TimerFactory(ev_quit=self.ev_quit,
logger=self.logger)
self.timer_factory.wind()
self.lock = threading.RLock()
self.channel = {}
self.channel_names = []
self.cur_channel = None
self.wscount = 0
self.statustask = None
self.preload_lock = threading.RLock()
self.preload_list = deque([], 4)
# Load bindings preferences
bindprefs = self.prefs.create_category('bindings')
bindprefs.load(onError='silent')
self.plugins = []
self._plugin_sort_method = self.get_plugin_menuname
# some default colormap info
self.cm = cmap.get_cmap("gray")
self.im = imap.get_imap("ramp")
# This plugin manager handles "global" (aka standard) plug ins
# (unique instances, not per channel)
self.gpmon = self.get_plugin_manager(self.logger, self,
None, self.mm)
# Initialize catalog and image server bank
self.imgsrv = catalog.ServerBank(self.logger)
# state for implementing field-info callback
self._cursor_task = self.get_backend_timer()
self._cursor_task.set_callback('expired', self._cursor_timer_cb)
self._cursor_last_update = time.time()
self.cursor_interval = self.settings.get('cursor_interval', 0.050)
# Try to load some bundled truetype fonts we might like to use
for font_name in font_asst.get_loadable_fonts():
self.logger.info("trying to load bundled font '%s'" % (font_name))
font_info = font_asst.get_font_info(font_name)
try:
GwHelp.load_font(font_name, font_info.font_path)
except Exception as e:
# quietly ignore font-loading problems for now--
# other fonts will be substituted
self.logger.warning("Error loading font '%s': %s" % (
font_name, str(e)))
font_asst.remove_font(font_name)
# add user preferred fonts for aliases, if present
fixed_font = self.settings.get('fixedFont', None)
if fixed_font is not None:
font_asst.add_alias('fixed', fixed_font)
serif_font = self.settings.get('serifFont', None)
if serif_font is not None:
font_asst.add_alias('serif', serif_font)
sans_font = self.settings.get('sansFont', None)
if sans_font is not None:
font_asst.add_alias('sans', sans_font)
# GUI initialization
self.w = Bunch.Bunch()
self.iconpath = icon_path
self.main_wsname = None
self._lastwsname = None
self.ds = None
self.layout = None
self.layout_file = None
self._lsize = None
self._rsize = None
self.filesel = None
self.menubar = None
self.gui_dialog_lock = threading.RLock()
self.gui_dialog_list = []
gviewer.register_viewer(Viewers.CanvasView)
gviewer.register_viewer(Viewers.TableViewGw)
gviewer.register_viewer(Viewers.PlotViewGw)
def get_server_bank(self):
return self.imgsrv
def get_preferences(self):
return self.prefs
def get_timer(self):
return self.timer_factory.timer()
def get_backend_timer(self):
return GwHelp.Timer()
def stop(self):
self.logger.info("shutting down Ginga...")
self.timer_factory.quit()
self.ev_quit.set()
self.logger.debug("should be exiting now")
def reset_viewer(self):
channel = self.get_current_channel()
opmon = channel.opmon
opmon.deactivate_focused()
self.normalsize()
def get_draw_class(self, drawtype):
drawtype = drawtype.lower()
return drawCatalog[drawtype]
def get_draw_classes(self):
return drawCatalog
def make_async_gui_callback(self, name, *args, **kwargs):
# NOTE: asynchronous!
self.gui_do(self.make_callback, name, *args, **kwargs)
def make_gui_callback(self, name, *args, **kwargs):
if self.is_gui_thread():
return self.make_callback(name, *args, **kwargs)
else:
# note: this cannot be "gui_call"--locks viewer.
# so call becomes async when a non-gui thread invokes it
self.gui_do(self.make_callback, name, *args, **kwargs)
# PLUGIN MANAGEMENT
def start_operation(self, opname):
return self.start_local_plugin(None, opname, None)
def stop_operation_channel(self, chname, opname):
self.logger.warning(
"Do not use this method name--it will be deprecated!")
return self.stop_local_plugin(chname, opname)
def start_local_plugin(self, chname, opname, future):
channel = self.get_channel(chname)
opmon = channel.opmon
opmon.start_plugin_future(channel.name, opname, future)
if hasattr(channel.viewer, 'onscreen_message'):
channel.viewer.onscreen_message(opname, delay=1.0)
def stop_local_plugin(self, chname, opname):
channel = self.get_channel(chname)
opmon = channel.opmon
opmon.deactivate(opname)
def call_local_plugin_method(self, chname, plugin_name, method_name,
args, kwargs):
"""
Parameters
----------
chname : str
The name of the channel containing the plugin.
plugin_name : str
The name of the local plugin containing the method to call.
method_name : str
The name of the method to call.
args : list or tuple
The positional arguments to the method
kwargs : dict
The keyword arguments to the method
Returns
-------
result : return value from calling the method
"""
channel = self.get_channel(chname)
opmon = channel.opmon
p_obj = opmon.get_plugin(plugin_name)
method = getattr(p_obj, method_name)
return self.gui_call(method, *args, **kwargs)
def start_global_plugin(self, plugin_name, raise_tab=False):
self.gpmon.start_plugin_future(None, plugin_name, None)
if raise_tab:
pInfo = self.gpmon.get_plugin_info(plugin_name)
self.ds.raise_tab(pInfo.tabname)
def stop_global_plugin(self, plugin_name):
self.gpmon.deactivate(plugin_name)
def call_global_plugin_method(self, plugin_name, method_name,
args, kwargs):
"""
Parameters
----------
plugin_name : str
The name of the global plugin containing the method to call.
method_name : str
The name of the method to call.
args : list or tuple
The positional arguments to the method
kwargs : dict
The keyword arguments to the method
Returns
-------
result : return value from calling the method
"""
p_obj = self.gpmon.get_plugin(plugin_name)
method = getattr(p_obj, method_name)
return self.gui_call(method, *args, **kwargs)
def start_plugin(self, plugin_name, spec):
ptype = spec.get('ptype', 'local')
if ptype == 'local':
self.start_operation(plugin_name)
else:
self.start_global_plugin(plugin_name, raise_tab=True)
def add_local_plugin(self, spec):
try:
spec.setdefault('ptype', 'local')
name = spec.setdefault('name', spec.get('klass', spec.module))
pfx = spec.get('pfx', pluginconfpfx)
path = spec.get('path', None)
self.mm.load_module(spec.module, pfx=pfx, path=path)
self.plugins.append(spec)
except Exception as e:
self.logger.error("Unable to load local plugin '%s': %s" % (
name, str(e)))
def add_global_plugin(self, spec):
try:
spec.setdefault('ptype', 'global')
name = spec.setdefault('name', spec.get('klass', spec.module))
pfx = spec.get('pfx', pluginconfpfx)
path = spec.get('path', None)
self.mm.load_module(spec.module, pfx=pfx, path=path)
self.plugins.append(spec)
self.gpmon.load_plugin(name, spec)
except Exception as e:
self.logger.error("Unable to load global plugin '%s': %s" % (
name, str(e)))
def add_plugin(self, spec):
if not spec.get('enabled', True):
return
ptype = spec.get('ptype', 'local')
if ptype == 'global':
self.add_global_plugin(spec)
else:
self.add_local_plugin(spec)
def set_plugins(self, plugins):
self.plugins = []
for spec in plugins:
self.add_plugin(spec)
def get_plugins(self):
return self.plugins
def get_plugin_spec(self, name):
"""Get the specification attributes for plugin with name `name`."""
l_name = name.lower()
for spec in self.plugins:
name = spec.get('name', spec.get('klass', spec.module))
if name.lower() == l_name:
return spec
raise KeyError(name)
def get_plugin_menuname(self, spec):
category = spec.get('category', None)
name = spec.setdefault('name', spec.get('klass', spec.module))
menu = spec.get('menu', spec.get('tab', name))
if category is None:
return menu
return category + '.' + menu
def set_plugin_sortmethod(self, fn):
self._plugin_sort_method = fn
def boot_plugins(self):
# Sort plugins according to desired order
self.plugins.sort(key=self._plugin_sort_method)
for spec in self.plugins:
name = spec.setdefault('name', spec.get('klass', spec.module))
hidden = spec.get('hidden', False)
if not hidden:
self.add_plugin_menu(name, spec)
start = spec.get('start', True)
# for now only start global plugins that have start==True
# channels are not yet created by this time
if start and spec.get('ptype', 'local') == 'global':
self.error_wrap(self.start_plugin, name, spec)
def show_error(self, errmsg, raisetab=True):
if self.gpmon.has_plugin('Errors'):
obj = self.gpmon.get_plugin('Errors')
obj.add_error(errmsg)
if raisetab:
self.ds.raise_tab('Errors')
def error_wrap(self, method, *args, **kwargs):
try:
return method(*args, **kwargs)
except Exception as e:
errmsg = "\n".join([e.__class__.__name__, str(e)])
try:
(type, value, tb) = sys.exc_info()
tb_str = "\n".join(traceback.format_tb(tb))
except Exception as e:
tb_str = "Traceback information unavailable."
errmsg += tb_str
self.logger.error(errmsg)
self.gui_do(self.show_error, errmsg, raisetab=True)
def help_text(self, name, text, text_kind='plain', trim_pfx=0):
"""
Provide help text for the user.
This method will convert the text as necessary with docutils and
display it in the WBrowser plugin, if available. If the plugin is
not available and the text is type 'rst' then the text will be
displayed in a plain text widget.
Parameters
----------
name : str
Category of help to show.
text : str
The text to show. Should be plain, HTML or RST text
text_kind : str (optional)
One of 'plain', 'html', 'rst'. Default is 'plain'.
trim_pfx : int (optional)
Number of spaces to trim off the beginning of each line of text.
"""
if trim_pfx > 0:
# caller wants to trim some space off the front
# of each line
text = toolbox.trim_prefix(text, trim_pfx)
if text_kind == 'rst':
# try to convert RST to HTML using docutils
try:
overrides = {'input_encoding': 'ascii',
'output_encoding': 'utf-8'}
text_html = publish_string(text, writer_name='html',
settings_overrides=overrides)
# docutils produces 'bytes' output, but webkit needs
# a utf-8 string
text = text_html.decode('utf-8')
text_kind = 'html'
except Exception as e:
self.logger.error("Error converting help text to HTML: %s" % (
str(e)))
# revert to showing RST as plain text
else:
raise ValueError(
"I don't know how to display text of kind '%s'" % (text_kind))
if text_kind == 'html':
self.help(text=text, text_kind='html')
else:
self.show_help_text(name, text)
def help(self, text=None, text_kind='url'):
if not self.gpmon.has_plugin('WBrowser'):
return self.show_error("help() requires 'WBrowser' plugin")
self.start_global_plugin('WBrowser')
# need to let GUI finish processing, it seems
self.update_pending()
obj = self.gpmon.get_plugin('WBrowser')
if text is not None:
if text_kind == 'url':
obj.browse(text)
else:
obj.browse(text, url_is_content=True)
else:
obj.show_help()
def show_help_text(self, name, help_txt, wsname='right'):
"""
Show help text in a closeable tab window. The title of the
window is set from ``name`` prefixed with 'HELP:'
"""
tabname = 'HELP: {}'.format(name)
group = 1
tabnames = self.ds.get_tabnames(group)
if tabname in tabnames:
# tab is already up somewhere
return
vbox = Widgets.VBox()
vbox.set_margins(4, 4, 4, 4)
vbox.set_spacing(2)
msg_font = self.get_font('fixed', 12)
tw = Widgets.TextArea(wrap=False, editable=False)
tw.set_font(msg_font)
tw.set_text(help_txt)
vbox.add_widget(tw, stretch=1)
btns = Widgets.HBox()
btns.set_border_width(4)
btns.set_spacing(3)
def _close_cb(w):
self.ds.remove_tab(tabname)
btn = Widgets.Button("Close")
btn.add_callback('activated', _close_cb)
btns.add_widget(btn, stretch=0)
btns.add_widget(Widgets.Label(''), stretch=1)
vbox.add_widget(btns, stretch=0)
self.ds.add_tab(wsname, vbox, group, tabname)
self.ds.raise_tab(tabname)
# BASIC IMAGE OPERATIONS
def load_image(self, filespec, idx=None, show_error=True):
"""
A wrapper around ginga.util.loader.load_data()
Parameters
----------
filespec : str
The path of the file to load (must reference a single file).
idx : str, int or tuple; optional, defaults to None
The index of the image to open within the file.
show_error : bool, optional, defaults to True
If `True`, then display an error in the GUI if the file
loading process fails.
Returns
-------
data_obj : data object named by filespec
"""
inherit_prihdr = self.settings.get('inherit_primary_header',
False)
try:
data_obj = loader.load_data(filespec, logger=self.logger,
idx=idx,
inherit_primary_header=inherit_prihdr)
except Exception as e:
errmsg = "Failed to load file '%s': %s" % (
filespec, str(e))
self.logger.error(errmsg)
try:
(type, value, tb) = sys.exc_info()
tb_str = "\n".join(traceback.format_tb(tb))
except Exception as e:
tb_str = "Traceback information unavailable."
if show_error:
self.gui_do(self.show_error, errmsg + '\n' + tb_str)
raise ControlError(errmsg)
self.logger.debug("Successfully loaded file into object.")
return data_obj
def load_file(self, filepath, chname=None, wait=True,
create_channel=True, display_image=True,
image_loader=None):
"""Load a file and display it.
Parameters
----------
filepath : str
The path of the file to load (must reference a local file).
chname : str, optional
The name of the channel in which to display the image.
wait : bool, optional
If `True`, then wait for the file to be displayed before returning
(synchronous behavior).
create_channel : bool, optional
Create channel.
display_image : bool, optional
If not `False`, then will load the image.
image_loader : func, optional
A special image loader, if provided.
Returns
-------
image
The image object that was loaded.
"""
if not chname:
channel = self.get_current_channel()
else:
if not self.has_channel(chname) and create_channel:
self.gui_call(self.add_channel, chname)
channel = self.get_channel(chname)
chname = channel.name
if image_loader is None:
image_loader = self.load_image
cache_dir = self.settings.get('download_folder', self.tmpdir)
info = iohelper.get_fileinfo(filepath, cache_dir=cache_dir)
# check that file is locally accessible
if not info.ondisk:
errmsg = "File must be locally loadable: %s" % (filepath)
self.gui_do(self.show_error, errmsg)
return
filepath = info.filepath
kwargs = {}
idx = None
if info.numhdu is not None:
kwargs['idx'] = info.numhdu
try:
image = image_loader(filepath, **kwargs)
except Exception as e:
errmsg = "Failed to load '%s': %s" % (filepath, str(e))
self.gui_do(self.show_error, errmsg)
return
future = Future.Future()
future.freeze(image_loader, filepath, **kwargs)
# Save a future for this image to reload it later if we
# have to remove it from memory
image.set(loader=image_loader, image_future=future)
if image.get('path', None) is None:
image.set(path=filepath)
# Assign a name to the image if the loader did not.
name = image.get('name', None)
if name is None:
name = iohelper.name_image_from_path(filepath, idx=idx)
image.set(name=name)
if display_image:
# Display image. If the wait parameter is False then don't wait
# for the image to load into the viewer
if wait:
self.gui_call(self.add_image, name, image, chname=chname)
else:
self.gui_do(self.add_image, name, image, chname=chname)
else:
self.gui_do(self.bulk_add_image, name, image, chname)
# Return the image
return image
def add_download(self, info, future):
"""
Hand off a download to the Downloads plugin, if it is present.
Parameters
----------
info : `~ginga.misc.Bunch.Bunch`
A bunch of information about the URI as returned by
`ginga.util.iohelper.get_fileinfo()`
future : `~ginga.misc.Future.Future`
A future that represents the future computation to be performed
after downloading the file. Resolving the future will trigger
the computation.
"""
if self.gpmon.has_plugin('Downloads'):
obj = self.gpmon.get_plugin('Downloads')
self.gui_do(obj.add_download, info, future)
else:
self.show_error("Please activate the 'Downloads' plugin to"
" enable download functionality")
def open_uri_cont(self, filespec, loader_cont_fn):
"""Download a URI (if necessary) and do some action on it.
If the file is already present (e.g. a file:// URI) then this
merely confirms that and invokes the continuation.
Parameters
----------
filespec : str
The path of the file to load (can be a non-local URI)
loader_cont_fn : func (str) -> None
A continuation consisting of a function of one argument
that does something with the file once it is downloaded
The parameter is the local filepath after download, plus
any "index" understood by the loader.
"""
info = iohelper.get_fileinfo(filespec)
# download file if necessary
if ((not info.ondisk) and (info.url is not None) and
(not info.url.startswith('file:'))):
# create up a future to do the download and set up a
# callback to handle it when finished
def _download_cb(future):
filepath = future.get_value(block=False)
self.logger.debug("downloaded: %s" % (filepath))
self.gui_do(loader_cont_fn, filepath + info.idx)
future = Future.Future()
future.add_callback('resolved', _download_cb)
self.add_download(info, future)
return
# invoke the continuation
loader_cont_fn(info.filepath + info.idx)
def open_file_cont(self, pathspec, loader_cont_fn):
"""Open a file and do some action on it.
Parameters
----------
pathspec : str
The path of the file to load (can be a URI, but must reference
a local file).
loader_cont_fn : func (data_obj) -> None
A continuation consisting of a function of one argument
that does something with the data_obj created by the loader
"""
self.assert_nongui_thread()
info = iohelper.get_fileinfo(pathspec)
filepath = info.filepath
if not os.path.exists(filepath):
errmsg = "File does not appear to exist: '%s'" % (filepath)
self.gui_do(self.show_error, errmsg)
return
warnmsg = ""
try:
typ, subtyp = iohelper.guess_filetype(filepath)
except Exception as e:
warnmsg = "Couldn't determine file type of '{0:}': " \
"{1:}".format(filepath, str(e))
self.logger.warning(warnmsg)
typ = None
def _open_file(opener_class):
# kwd args to pass to opener
kwargs = dict()
inherit_prihdr = self.settings.get('inherit_primary_header',
False)
kwargs['inherit_primary_header'] = inherit_prihdr
# open the file and load the items named by the index
opener = opener_class(self.logger)
try:
with opener.open_file(filepath) as io_f:
io_f.load_idx_cont(info.idx, loader_cont_fn, **kwargs)
except Exception as e:
errmsg = "Error opening '%s': %s" % (filepath, str(e))
try:
(_type, value, tb) = sys.exc_info()
tb_str = "\n".join(traceback.format_tb(tb))
except Exception as e:
tb_str = "Traceback information unavailable."
self.gui_do(self.show_error, errmsg + '\n' + tb_str)
def _check_open(errmsg):
if typ is None:
errmsg = ("Error determining file type: {0:}\n"
"\nPlease choose an opener or cancel, for file:\n"
"{1:}".format(errmsg, filepath))
openers = loader.get_all_openers()
self.gui_do(self.gui_choose_file_opener, errmsg, openers,
_open_file, None, filepath)
else:
mimetype = "{}/{}".format(typ, subtyp)
openers = loader.get_openers(mimetype)
num_openers = len(openers)
if num_openers == 1:
opener_class = openers[0].opener
self.nongui_do(_open_file, opener_class)
self.__next_dialog()
elif num_openers == 0:
errmsg = ("No registered opener for: '{0:}'\n"
"\nPlease choose an opener or cancel, for file:\n"
"{1:}".format(mimetype, filepath))
openers = loader.get_all_openers()
self.gui_do(self.gui_choose_file_opener, errmsg, openers,
_open_file, mimetype, filepath)
else:
errmsg = ("Multiple registered openers for: '{0:}'\n"
"\nPlease choose an opener or cancel, for file:\n"
"{1:}".format(mimetype, filepath))
self.gui_do(self.gui_choose_file_opener, errmsg, openers,
_open_file, '*', filepath)
future = Future.Future()
future.freeze(_check_open, warnmsg)
with self.gui_dialog_lock:
self.gui_dialog_list.append(future)
if len(self.gui_dialog_list) == 1:
self.nongui_do_future(future)
def open_uris(self, uris, chname=None, bulk_add=False):
"""Open a set of URIs.
Parameters
----------
uris : list of str
The URIs of the files to load
chname: str, optional (defaults to channel with focus)
The name of the channel in which to load the items
bulk_add : bool, optional (defaults to False)
If True, then all the data items are loaded into the
channel without disturbing the current item there.
If False, then the first item loaded will be displayed
and the rest of the items will be loaded as bulk.
"""
if len(uris) == 0:
return
if chname is None:
channel = self.get_channel_info()
if channel is None:
# No active channel to load these into
return
chname = channel.name
channel = self.get_channel_on_demand(chname)
def show_dataobj_bulk(data_obj):
self.gui_do(channel.add_image, data_obj, bulk_add=True)
def load_file_bulk(filepath):
self.nongui_do(self.open_file_cont, filepath, show_dataobj_bulk)
def show_dataobj(data_obj):
self.gui_do(channel.add_image, data_obj, bulk_add=False)
def load_file(filepath):
self.nongui_do(self.open_file_cont, filepath, show_dataobj)
# determine whether first file is loaded as a bulk load
if bulk_add:
self.open_uri_cont(uris[0], load_file_bulk)
else:
self.open_uri_cont(uris[0], load_file)
self.update_pending()
for uri in uris[1:]:
# rest of files are all loaded using bulk load
self.open_uri_cont(uri, load_file_bulk)
self.update_pending()
def add_preload(self, chname, image_info):
bnch = Bunch.Bunch(chname=chname, info=image_info)
with self.preload_lock:
self.preload_list.append(bnch)
self.nongui_do(self.preload_scan)
def preload_scan(self):
# preload any pending files
# TODO: do we need any throttling of loading here?
with self.preload_lock:
while len(self.preload_list) > 0:
bnch = self.preload_list.pop()
self.nongui_do(self.preload_file, bnch.chname,
bnch.info.name, bnch.info.path,
image_future=bnch.info.image_future)
def preload_file(self, chname, imname, path, image_future=None):
# sanity check to see if the file is already in memory
self.logger.debug("preload: checking %s in %s" % (imname, chname))
channel = self.get_channel(chname)
if imname not in channel.datasrc:
# not there--load image in a non-gui thread, then have the
# gui add it to the channel silently
self.logger.info("preloading image %s" % (path))
if image_future is None:
# TODO: need index info?
image = self.load_image(path)
else:
image = image_future.thaw()
self.gui_do(self.add_image, imname, image,
chname=chname, silent=True)
self.logger.debug("end preload")
def zoom_in(self):
"""Zoom the view in one zoom step.
"""
viewer = self.getfocus_viewer()
if hasattr(viewer, 'zoom_in'):
viewer.zoom_in()
return True
def zoom_out(self):
"""Zoom the view out one zoom step.
"""
viewer = self.getfocus_viewer()
if hasattr(viewer, 'zoom_out'):
viewer.zoom_out()
return True
def zoom_1_to_1(self):
"""Zoom the view to a 1 to 1 pixel ratio (100 %%).
"""
viewer = self.getfocus_viewer()
if hasattr(viewer, 'scale_to'):
viewer.scale_to(1.0, 1.0)
return True
def zoom_fit(self):
"""Zoom the view to fit the image entirely in the window.
"""
viewer = self.getfocus_viewer()
if hasattr(viewer, 'zoom_fit'):
viewer.zoom_fit()
return True
def auto_levels(self):
"""Perform an auto cut levels on the image.
"""
viewer = self.getfocus_viewer()
if hasattr(viewer, 'auto_levels'):
viewer.auto_levels()
def prev_img_ws(self, ws, loop=True):
"""Go to the previous image in the focused channel in the workspace.
"""
channel = self.get_active_channel_ws(ws)