1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Snuffling infrastructure
8This module provides the base class :py:class:`Snuffling` for user-defined
9snufflings and some utilities for their handling.
10'''
11from __future__ import absolute_import
13import os
14import sys
15import time
16import logging
17import traceback
18import tempfile
20from .qt_compat import qc, qw, getSaveFileName, use_pyqt5
22from pyrocko import pile, config
23from pyrocko.util import quote
25from .util import (ValControl, LinValControl, FigureFrame, WebKitFrame,
26 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker,
27 load_markers, save_markers)
30if sys.version_info >= (3, 0):
31 from importlib import reload
34Marker, load_markers, save_markers # noqa
36logger = logging.getLogger('pyrocko.gui.snuffling')
39def fnpatch(x):
40 if use_pyqt5:
41 return x
42 else:
43 return x, None
46class MyFrame(qw.QFrame):
47 widgetVisibilityChanged = qc.pyqtSignal(bool)
49 def showEvent(self, ev):
50 self.widgetVisibilityChanged.emit(True)
52 def hideEvent(self, ev):
53 self.widgetVisibilityChanged.emit(False)
56class Param(object):
57 '''
58 Definition of an adjustable floating point parameter for the
59 snuffling. The snuffling may display controls for user input for
60 such parameters.
62 :param name: labels the parameter on the snuffling's control panel
63 :param ident: identifier of the parameter
64 :param default: default value
65 :param minimum: minimum value for the parameter
66 :param maximum: maximum value for the parameter
67 :param low_is_none: if ``True``: parameter is set to None at lowest value
68 of parameter range (optional)
69 :param high_is_none: if ``True``: parameter is set to None at highest value
70 of parameter range (optional)
71 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
72 value of parameter range (optional)
73 '''
75 def __init__(
76 self, name, ident, default, minimum, maximum,
77 low_is_none=None,
78 high_is_none=None,
79 low_is_zero=False):
81 if low_is_none and default == minimum:
82 default = None
83 if high_is_none and default == maximum:
84 default = None
86 self.name = name
87 self.ident = ident
88 self.default = default
89 self.minimum = minimum
90 self.maximum = maximum
91 self.low_is_none = low_is_none
92 self.high_is_none = high_is_none
93 self.low_is_zero = low_is_zero
94 self._control = None
97class Switch(object):
98 '''
99 Definition of a boolean switch for the snuffling. The snuffling
100 may display a checkbox for such a switch.
102 :param name: labels the switch on the snuffling's control panel
103 :param ident: identifier of the parameter
104 :param default: default value
105 '''
107 def __init__(self, name, ident, default):
108 self.name = name
109 self.ident = ident
110 self.default = default
113class Choice(object):
114 '''
115 Definition of a string choice for the snuffling. The snuffling
116 may display a menu for such a choice.
118 :param name: labels the menu on the snuffling's control panel
119 :param ident: identifier of the parameter
120 :param default: default value
121 :param choices: tuple of other options
122 '''
124 def __init__(self, name, ident, default, choices):
125 self.name = name
126 self.ident = ident
127 self.default = default
128 self.choices = choices
131class Snuffling(object):
132 '''
133 Base class for user snufflings.
135 Snufflings are plugins for snuffler (and other applications using the
136 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in
137 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
138 should provide a simple way of extending the functionality of snuffler.
140 A snuffling has access to all data available in a pile viewer, can process
141 this data and can create and add new traces and markers to the viewer.
142 '''
144 def __init__(self):
145 self._path = None
147 self._name = 'Untitled Snuffling'
148 self._viewer = None
149 self._tickets = []
150 self._markers = []
152 self._delete_panel = None
153 self._delete_menuitem = None
155 self._panel_parent = None
156 self._menu_parent = None
158 self._panel = None
159 self._menuitem = None
160 self._helpmenuitem = None
161 self._parameters = []
162 self._param_controls = {}
164 self._triggers = []
166 self._live_update = True
167 self._previous_output_filename = None
168 self._previous_input_filename = None
169 self._previous_input_directory = None
171 self._tempdir = None
172 self._iplot = 0
174 self._have_pre_process_hook = False
175 self._have_post_process_hook = False
176 self._pre_process_hook_enabled = False
177 self._post_process_hook_enabled = False
179 self._no_viewer_pile = None
180 self._cli_params = {}
181 self._filename = None
182 self._force_panel = False
183 self._call_in_progress = False
185 def setup(self):
186 '''
187 Setup the snuffling.
189 This method should be implemented in subclass and contain e.g. calls to
190 :py:meth:`set_name` and :py:meth:`add_parameter`.
191 '''
193 pass
195 def module_dir(self):
196 '''
197 Returns the path of the directory where snufflings are stored.
199 The default path is ``$HOME/.snufflings``.
200 '''
202 return self._path
204 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
205 '''
206 Set parent viewer and hooks to add panel and menu entry.
208 This method is called from the
209 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls
210 :py:meth:`setup_gui`.
211 '''
213 self._viewer = viewer
214 self._panel_parent = panel_parent
215 self._menu_parent = menu_parent
217 self.setup_gui(reloaded=reloaded)
219 def setup_gui(self, reloaded=False):
220 '''
221 Create and add gui elements to the viewer.
223 This method is initially called from :py:meth:`init_gui`. It is also
224 called, e.g. when new parameters have been added or if the name of the
225 snuffling has been changed.
226 '''
228 if self._panel_parent is not None:
229 self._panel = self.make_panel(self._panel_parent)
230 if self._panel:
231 self._panel_parent.add_panel(
232 self.get_name(), self._panel, reloaded)
234 if self._menu_parent is not None:
235 self._menuitem = self.make_menuitem(self._menu_parent)
236 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
237 if self._menuitem:
238 self._menu_parent.add_snuffling_menuitem(self._menuitem)
240 if self._helpmenuitem:
241 self._menu_parent.add_snuffling_help_menuitem(
242 self._helpmenuitem)
244 def set_force_panel(self, bool=True):
245 '''
246 Force to create a panel.
248 :param bool: if ``True`` will create a panel with Help, Clear and Run
249 button.
250 '''
252 self._force_panel = bool
254 def make_cli_parser1(self):
255 import optparse
257 class MyOptionParser(optparse.OptionParser):
258 def error(self, msg):
259 logger.error(msg)
260 self.exit(1)
262 parser = MyOptionParser()
264 parser.add_option(
265 '--format',
266 dest='format',
267 default='from_extension',
268 choices=(
269 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
270 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
271 'from_extension', 'detect'),
272 help='assume files are of given FORMAT [default: \'%default\']')
274 parser.add_option(
275 '--pattern',
276 dest='regex',
277 metavar='REGEX',
278 help='only include files whose paths match REGEX')
280 self.add_params_to_cli_parser(parser)
281 self.configure_cli_parser(parser)
282 return parser
284 def configure_cli_parser(self, parser):
285 pass
287 def cli_usage(self):
288 return None
290 def add_params_to_cli_parser(self, parser):
292 for param in self._parameters:
293 if isinstance(param, Param):
294 parser.add_option(
295 '--' + param.ident,
296 dest=param.ident,
297 default=param.default,
298 type='float',
299 help=param.name)
301 def setup_cli(self):
302 self.setup()
303 parser = self.make_cli_parser1()
304 (options, args) = parser.parse_args()
306 for param in self._parameters:
307 if isinstance(param, Param):
308 setattr(self, param.ident, getattr(options, param.ident))
310 self._cli_params['regex'] = options.regex
311 self._cli_params['format'] = options.format
312 self._cli_params['sources'] = args
314 return options, args, parser
316 def delete_gui(self):
317 '''
318 Remove the gui elements of the snuffling.
320 This removes the panel and menu entry of the widget from the viewer and
321 also removes all traces and markers added with the
322 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
323 '''
325 self.cleanup()
327 if self._panel is not None:
328 self._panel_parent.remove_panel(self._panel)
329 self._panel = None
331 if self._menuitem is not None:
332 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
333 self._menuitem = None
335 if self._helpmenuitem is not None:
336 self._menu_parent.remove_snuffling_help_menuitem(
337 self._helpmenuitem)
339 def set_name(self, name):
340 '''
341 Set the snuffling's name.
343 The snuffling's name is shown as a menu entry and in the panel header.
344 '''
346 self._name = name
347 self.reset_gui()
349 def get_name(self):
350 '''
351 Get the snuffling's name.
352 '''
354 return self._name
356 def set_have_pre_process_hook(self, bool):
357 self._have_pre_process_hook = bool
358 self._live_update = False
359 self._pre_process_hook_enabled = False
360 self.reset_gui()
362 def set_have_post_process_hook(self, bool):
363 self._have_post_process_hook = bool
364 self._live_update = False
365 self._post_process_hook_enabled = False
366 self.reset_gui()
368 def set_have_pile_changed_hook(self, bool):
369 self._pile_ = False
371 def enable_pile_changed_notifications(self):
372 '''
373 Get informed when pile changed.
375 When activated, the :py:meth:`pile_changed` method is called on every
376 update in the viewer's pile.
377 '''
379 viewer = self.get_viewer()
380 viewer.pile_has_changed_signal.connect(
381 self.pile_changed)
383 def disable_pile_changed_notifications(self):
384 '''
385 Stop getting informed about changes in viewer's pile.
386 '''
388 viewer = self.get_viewer()
389 viewer.pile_has_changed_signal.disconnect(
390 self.pile_changed)
392 def pile_changed(self):
393 '''
394 Called when the connected viewer's pile has changed.
396 Must be activated with a call to
397 :py:meth:`enable_pile_changed_notifications`.
398 '''
400 pass
402 def reset_gui(self, reloaded=False):
403 '''
404 Delete and recreate the snuffling's panel.
405 '''
407 if self._panel or self._menuitem:
408 sett = self.get_settings()
409 self.delete_gui()
410 self.setup_gui(reloaded=reloaded)
411 self.set_settings(sett)
413 def show_message(self, kind, message):
414 '''
415 Display a message box.
417 :param kind: string defining kind of message
418 :param message: the message to be displayed
419 '''
421 try:
422 box = qw.QMessageBox(self.get_viewer())
423 box.setText('%s: %s' % (kind.capitalize(), message))
424 box.exec_()
425 except NoViewerSet:
426 pass
428 def error(self, message):
429 '''
430 Show an error message box.
432 :param message: specifying the error
433 '''
435 logger.error('%s: %s' % (self._name, message))
436 self.show_message('error', message)
438 def warn(self, message):
439 '''
440 Display a warning message.
442 :param message: specifying the warning
443 '''
445 logger.warning('%s: %s' % (self._name, message))
446 self.show_message('warning', message)
448 def fail(self, message):
449 '''
450 Show an error message box and raise :py:exc:`SnufflingCallFailed`
451 exception.
453 :param message: specifying the error
454 '''
456 self.error(message)
457 raise SnufflingCallFailed(message)
459 def pylab(self, name=None, get='axes'):
460 '''
461 Create a :py:class:`FigureFrame` and return either the frame,
462 a :py:class:`matplotlib.figure.Figure` instance or a
463 :py:class:`matplotlib.axes.Axes` instance.
465 :param name: labels the figure frame's tab
466 :param get: 'axes'|'figure'|'frame' (optional)
467 '''
469 if name is None:
470 self._iplot += 1
471 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
473 fframe = FigureFrame()
474 self._panel_parent.add_tab(name, fframe)
475 if get == 'axes':
476 return fframe.gca()
477 elif get == 'figure':
478 return fframe.gcf()
479 elif get == 'figure_frame':
480 return fframe
482 def figure(self, name=None):
483 '''
484 Returns a :py:class:`matplotlib.figure.Figure` instance
485 which can be displayed within snuffler by calling
486 :py:meth:`canvas.draw`.
488 :param name: labels the tab of the figure
489 '''
491 return self.pylab(name=name, get='figure')
493 def axes(self, name=None):
494 '''
495 Returns a :py:class:`matplotlib.axes.Axes` instance.
497 :param name: labels the tab of axes
498 '''
500 return self.pylab(name=name, get='axes')
502 def figure_frame(self, name=None):
503 '''
504 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
506 :param name: labels the tab figure frame
507 '''
509 return self.pylab(name=name, get='figure_frame')
511 def pixmap_frame(self, filename=None, name=None):
512 '''
513 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
515 :param name: labels the tab
516 :param filename: name of file to be displayed
517 '''
519 f = PixmapFrame(filename)
521 scroll_area = qw.QScrollArea()
522 scroll_area.setWidget(f)
523 scroll_area.setWidgetResizable(True)
525 self._panel_parent.add_tab(name or "Pixmap", scroll_area)
526 return f
528 def web_frame(self, url=None, name=None):
529 '''
530 Creates a :py:class:`WebKitFrame` which can be used as a browser
531 within snuffler.
533 :param url: url to open
534 :param name: labels the tab
535 '''
537 if name is None:
538 self._iplot += 1
539 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
541 f = WebKitFrame(url)
542 self._panel_parent.add_tab(name, f)
543 return f
545 def vtk_frame(self, name=None, actors=None):
546 '''
547 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
548 graphics.
550 :param actors: list of VTKActors
551 :param name: labels the tab
553 Initialize the interactive rendering by calling the frames'
554 :py:meth`initialize` method after having added all actors to the frames
555 renderer.
557 Requires installation of vtk including python wrapper.
558 '''
559 if name is None:
560 self._iplot += 1
561 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
563 try:
564 f = VTKFrame(actors=actors)
565 except ImportError as e:
566 self.fail(e)
568 self._panel_parent.add_tab(name, f)
569 return f
571 def tempdir(self):
572 '''
573 Create a temporary directory and return its absolute path.
575 The directory and all its contents are removed when the Snuffling
576 instance is deleted.
577 '''
579 if self._tempdir is None:
580 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
582 return self._tempdir
584 def set_live_update(self, live_update):
585 '''
586 Enable/disable live updating.
588 When live updates are enabled, the :py:meth:`call` method is called
589 whenever the user changes a parameter. If it is disabled, the user has
590 to initiate such a call manually by triggering the snuffling's menu
591 item or pressing the call button.
592 '''
594 self._live_update = live_update
595 if self._have_pre_process_hook:
596 self._pre_process_hook_enabled = live_update
597 if self._have_post_process_hook:
598 self._post_process_hook_enabled = live_update
600 try:
601 self.get_viewer().clean_update()
602 except NoViewerSet:
603 pass
605 def add_parameter(self, param):
606 '''
607 Add an adjustable parameter to the snuffling.
609 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
610 :py:class:`Choice`.
612 For each parameter added, controls are added to the snuffling's panel,
613 so that the parameter can be adjusted from the gui.
614 '''
616 self._parameters.append(param)
617 self._set_parameter_value(param.ident, param.default)
619 if self._panel is not None:
620 self.delete_gui()
621 self.setup_gui()
623 def add_trigger(self, name, method):
624 '''
625 Add a button to the snuffling's panel.
627 :param name: string that labels the button
628 :param method: method associated with the button
629 '''
631 self._triggers.append((name, method))
633 if self._panel is not None:
634 self.delete_gui()
635 self.setup_gui()
637 def get_parameters(self):
638 '''
639 Get the snuffling's adjustable parameter definitions.
641 Returns a list of objects of type :py:class:`Param`.
642 '''
644 return self._parameters
646 def get_parameter(self, ident):
647 '''
648 Get one of the snuffling's adjustable parameter definitions.
650 :param ident: identifier of the parameter
652 Returns an object of type :py:class:`Param` or ``None``.
653 '''
655 for param in self._parameters:
656 if param.ident == ident:
657 return param
658 return None
660 def set_parameter(self, ident, value):
661 '''
662 Set one of the snuffling's adjustable parameters.
664 :param ident: identifier of the parameter
665 :param value: new value of the parameter
667 Adjusts the control of a parameter without calling :py:meth:`call`.
668 '''
670 self._set_parameter_value(ident, value)
672 control = self._param_controls.get(ident, None)
673 if control:
674 control.set_value(value)
676 def set_parameter_range(self, ident, vmin, vmax):
677 '''
678 Set the range of one of the snuffling's adjustable parameters.
680 :param ident: identifier of the parameter
681 :param vmin,vmax: new minimum and maximum value for the parameter
683 Adjusts the control of a parameter without calling :py:meth:`call`.
684 '''
686 control = self._param_controls.get(ident, None)
687 if control:
688 control.set_range(vmin, vmax)
690 def set_parameter_choices(self, ident, choices):
691 '''
692 Update the choices of a Choice parameter.
694 :param ident: identifier of the parameter
695 :param choices: list of strings
696 '''
698 control = self._param_controls.get(ident, None)
699 if control:
700 selected_choice = control.set_choices(choices)
701 self._set_parameter_value(ident, selected_choice)
703 def _set_parameter_value(self, ident, value):
704 setattr(self, ident, value)
706 def get_parameter_value(self, ident):
707 '''
708 Get the current value of a parameter.
710 :param ident: identifier of the parameter
711 '''
712 return getattr(self, ident)
714 def get_settings(self):
715 '''
716 Returns a dictionary with identifiers of all parameters as keys and
717 their values as the dictionaries values.
718 '''
720 params = self.get_parameters()
721 settings = {}
722 for param in params:
723 settings[param.ident] = self.get_parameter_value(param.ident)
725 return settings
727 def set_settings(self, settings):
728 params = self.get_parameters()
729 dparams = dict([(param.ident, param) for param in params])
730 for k, v in settings.items():
731 if k in dparams:
732 self._set_parameter_value(k, v)
733 if k in self._param_controls:
734 control = self._param_controls[k]
735 control.set_value(v)
737 def get_viewer(self):
738 '''
739 Get the parent viewer.
741 Returns a reference to an object of type :py:class:`PileOverview`,
742 which is the main viewer widget.
744 If no gui has been initialized for the snuffling, a
745 :py:exc:`NoViewerSet` exception is raised.
746 '''
748 if self._viewer is None:
749 raise NoViewerSet()
750 return self._viewer
752 def get_pile(self):
753 '''
754 Get the pile.
756 If a gui has been initialized, a reference to the viewer's internal
757 pile is returned. If not, the :py:meth:`make_pile` method (which may be
758 overloaded in subclass) is called to create a pile. This can be
759 utilized to make hybrid snufflings, which may work also in a standalone
760 mode.
761 '''
763 try:
764 p = self.get_viewer().get_pile()
765 except NoViewerSet:
766 if self._no_viewer_pile is None:
767 self._no_viewer_pile = self.make_pile()
769 p = self._no_viewer_pile
771 return p
773 def get_active_event_and_stations(
774 self, trange=(-3600., 3600.), missing='warn'):
776 '''
777 Get event and stations with available data for active event.
779 :param trange: (begin, end), time range around event origin time to
780 query for available data
781 :param missing: string, what to do in case of missing station
782 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
784 :returns: ``(event, stations)``
785 '''
787 p = self.get_pile()
788 v = self.get_viewer()
790 event = v.get_active_event()
791 if event is None:
792 self.fail(
793 'No active event set. Select an event and press "e" to make '
794 'it the "active event"')
796 stations = {}
797 for traces in p.chopper(
798 event.time+trange[0],
799 event.time+trange[1],
800 load_data=False,
801 degap=False):
803 for tr in traces:
804 try:
805 for skey in v.station_keys(tr):
806 if skey in stations:
807 continue
809 station = v.get_station(skey)
810 stations[skey] = station
812 except KeyError:
813 s = 'No station information for station key "%s".' \
814 % '.'.join(skey)
816 if missing == 'warn':
817 logger.warning(s)
818 elif missing == 'raise':
819 raise MissingStationInformation(s)
820 elif missing == 'ignore':
821 pass
822 else:
823 assert False, 'invalid argument to "missing"'
825 stations[skey] = None
827 return event, list(set(
828 st for st in stations.values() if st is not None))
830 def get_stations(self):
831 '''
832 Get all stations known to the viewer.
833 '''
835 v = self.get_viewer()
836 stations = list(v.stations.values())
837 return stations
839 def get_markers(self):
840 '''
841 Get all markers from the viewer.
842 '''
844 return self.get_viewer().get_markers()
846 def get_event_markers(self):
847 '''
848 Get all event markers from the viewer.
849 '''
851 return [m for m in self.get_viewer().get_markers()
852 if isinstance(m, EventMarker)]
854 def get_selected_markers(self):
855 '''
856 Get all selected markers from the viewer.
857 '''
859 return self.get_viewer().selected_markers()
861 def get_selected_event_markers(self):
862 '''
863 Get all selected event markers from the viewer.
864 '''
866 return [m for m in self.get_viewer().selected_markers()
867 if isinstance(m, EventMarker)]
869 def get_active_event_and_phase_markers(self):
870 '''
871 Get the marker of the active event and any associated phase markers
872 '''
874 viewer = self.get_viewer()
875 markers = viewer.get_markers()
876 event_marker = viewer.get_active_event_marker()
877 if event_marker is None:
878 self.fail(
879 'No active event set. '
880 'Select an event and press "e" to make it the "active event"')
882 event = event_marker.get_event()
884 selection = []
885 for m in markers:
886 if isinstance(m, PhaseMarker):
887 if m.get_event() is event:
888 selection.append(m)
890 return (
891 event_marker,
892 [m for m in markers if isinstance(m, PhaseMarker) and
893 m.get_event() == event])
895 def get_viewer_trace_selector(self, mode='inview'):
896 '''
897 Get currently active trace selector from viewer.
899 :param mode: set to ``'inview'`` (default) to only include selections
900 currently shown in the viewer, ``'visible' to include all traces
901 not currenly hidden by hide or quick-select commands, or ``'all'``
902 to disable any restrictions.
903 '''
905 viewer = self.get_viewer()
907 def rtrue(tr):
908 return True
910 if mode == 'inview':
911 return viewer.trace_selector or rtrue
912 elif mode == 'visible':
913 return viewer.trace_filter or rtrue
914 elif mode == 'all':
915 return rtrue
916 else:
917 raise Exception('invalid mode argument')
919 def chopper_selected_traces(self, fallback=False, marker_selector=None,
920 mode='inview', main_bandpass=False,
921 progress=None, responsive=False,
922 *args, **kwargs):
923 '''
924 Iterate over selected traces.
926 Shortcut to get all trace data contained in the selected markers in the
927 running snuffler. For each selected marker,
928 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
929 *tmin*, *tmax*, and *trace_selector* set to values according to the
930 marker. Additional arguments to the chopper are handed over from
931 *\\*args* and *\\*\\*kwargs*.
933 :param fallback:
934 If ``True``, if no selection has been marked, use the content
935 currently visible in the viewer.
937 :param marker_selector:
938 If not ``None`` a callback to filter markers.
940 :param mode:
941 Set to ``'inview'`` (default) to only include selections currently
942 shown in the viewer (excluding traces accessible through vertical
943 scrolling), ``'visible'`` to include all traces not currently
944 hidden by hide or quick-select commands (including traces
945 accessible through vertical scrolling), or ``'all'`` to disable any
946 restrictions.
948 :param main_bandpass:
949 If ``True``, apply main control high- and lowpass filters to
950 traces. Note: use with caution. Processing is fixed to use 4th
951 order Butterworth highpass and lowpass and the signal is always
952 demeaned before filtering. FFT filtering, rotation, demean and
953 bandpass settings from the graphical interface are not respected
954 here. Padding is not automatically adjusted so results may include
955 artifacts.
957 :param progress:
958 If given a string a progress bar is shown to the user. The string
959 is used as the label for the progress bar.
961 :param responsive:
962 If set to ``True``, occasionally allow UI events to be processed.
963 If used in combination with ``progress``, this allows the iterator
964 to be aborted by the user.
965 '''
967 try:
968 viewer = self.get_viewer()
969 markers = [
970 m for m in viewer.selected_markers()
971 if not isinstance(m, EventMarker)]
973 if marker_selector is not None:
974 markers = [
975 marker for marker in markers if marker_selector(marker)]
977 pile = self.get_pile()
979 def rtrue(tr):
980 return True
982 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
983 trace_selector_viewer = self.get_viewer_trace_selector(mode)
985 style_arg = kwargs.pop('style', None)
987 if main_bandpass:
988 def apply_filters(traces):
989 for tr in traces:
990 if viewer.highpass is not None:
991 tr.highpass(4, viewer.highpass)
992 if viewer.lowpass is not None:
993 tr.lowpass(4, viewer.lowpass)
994 return traces
995 else:
996 def apply_filters(traces):
997 return traces
999 pb = viewer.parent().get_progressbars()
1001 time_last = [time.time()]
1003 def update_progress(label, batch):
1004 time_now = time.time()
1005 if responsive:
1006 # start processing events with one second delay, so that
1007 # e.g. cleanup actions at startup do not cause track number
1008 # changes etc.
1009 if time_last[0] + 1. < time_now:
1010 qw.qApp.processEvents()
1011 else:
1012 # redraw about once a second
1013 if time_last[0] + 1. < time_now:
1014 viewer.repaint()
1016 time_last[0] = time.time() # use time after drawing
1018 abort = pb.set_status(
1019 label, batch.i*100./batch.n, responsive)
1020 abort |= viewer.window().is_closing()
1022 return abort
1024 if markers:
1025 for imarker, marker in enumerate(markers):
1026 try:
1027 if progress:
1028 label = '%s: %i/%i' % (
1029 progress, imarker+1, len(markers))
1031 pb.set_status(label, 0, responsive)
1033 if not marker.nslc_ids:
1034 trace_selector_marker = rtrue
1035 else:
1036 def trace_selector_marker(tr):
1037 return marker.match_nslc(tr.nslc_id)
1039 def trace_selector(tr):
1040 return trace_selector_arg(tr) \
1041 and trace_selector_viewer(tr) \
1042 and trace_selector_marker(tr)
1044 for batch in pile.chopper(
1045 tmin=marker.tmin,
1046 tmax=marker.tmax,
1047 trace_selector=trace_selector,
1048 style='batch',
1049 *args,
1050 **kwargs):
1052 if progress:
1053 abort = update_progress(label, batch)
1054 if abort:
1055 return
1057 batch.traces = apply_filters(batch.traces)
1058 if style_arg == 'batch':
1059 yield batch
1060 else:
1061 yield batch.traces
1063 finally:
1064 if progress:
1065 pb.set_status(label, 100., responsive)
1067 elif fallback:
1068 def trace_selector(tr):
1069 return trace_selector_arg(tr) \
1070 and trace_selector_viewer(tr)
1072 tmin, tmax = viewer.get_time_range()
1074 if not pile.is_empty():
1075 ptmin = pile.get_tmin()
1076 tpad = kwargs.get('tpad', 0.0)
1077 if ptmin > tmin:
1078 tmin = ptmin + tpad
1079 ptmax = pile.get_tmax()
1080 if ptmax < tmax:
1081 tmax = ptmax - tpad
1083 try:
1084 if progress:
1085 label = progress
1086 pb.set_status(label, 0, responsive)
1088 for batch in pile.chopper(
1089 tmin=tmin,
1090 tmax=tmax,
1091 trace_selector=trace_selector,
1092 style='batch',
1093 *args,
1094 **kwargs):
1096 if progress:
1097 abort = update_progress(label, batch)
1099 if abort:
1100 return
1102 batch.traces = apply_filters(batch.traces)
1104 if style_arg == 'batch':
1105 yield batch
1106 else:
1107 yield batch.traces
1109 finally:
1110 if progress:
1111 pb.set_status(label, 100., responsive)
1113 else:
1114 raise NoTracesSelected()
1116 except NoViewerSet:
1117 pile = self.get_pile()
1118 return pile.chopper(*args, **kwargs)
1120 def get_selected_time_range(self, fallback=False):
1121 '''
1122 Get the time range spanning all selected markers.
1124 :param fallback: if ``True`` and no marker is selected return begin and
1125 end of visible time range
1126 '''
1128 viewer = self.get_viewer()
1129 markers = viewer.selected_markers()
1130 mins = [marker.tmin for marker in markers]
1131 maxs = [marker.tmax for marker in markers]
1133 if mins and maxs:
1134 tmin = min(mins)
1135 tmax = max(maxs)
1137 elif fallback:
1138 tmin, tmax = viewer.get_time_range()
1140 else:
1141 raise NoTracesSelected()
1143 return tmin, tmax
1145 def panel_visibility_changed(self, bool):
1146 '''
1147 Called when the snuffling's panel becomes visible or is hidden.
1149 Can be overloaded in subclass, e.g. to perform additional setup actions
1150 when the panel is activated the first time.
1151 '''
1153 pass
1155 def make_pile(self):
1156 '''
1157 Create a pile.
1159 To be overloaded in subclass. The default implementation just calls
1160 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1161 arguments.
1162 '''
1164 cachedirname = config.config().cache_dir
1165 sources = self._cli_params.get('sources', sys.argv[1:])
1166 return pile.make_pile(
1167 sources,
1168 cachedirname=cachedirname,
1169 regex=self._cli_params['regex'],
1170 fileformat=self._cli_params['format'])
1172 def make_panel(self, parent):
1173 '''
1174 Create a widget for the snuffling's control panel.
1176 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1177 if no panel is needed (e.g. if the snuffling has no adjustable
1178 parameters).
1179 '''
1181 params = self.get_parameters()
1182 self._param_controls = {}
1183 if params or self._force_panel:
1184 sarea = MyScrollArea(parent.get_panel_parent_widget())
1185 sarea.setFrameStyle(qw.QFrame.NoFrame)
1186 sarea.setSizePolicy(qw.QSizePolicy(
1187 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1188 frame = MyFrame(sarea)
1189 frame.widgetVisibilityChanged.connect(
1190 self.panel_visibility_changed)
1192 frame.setSizePolicy(qw.QSizePolicy(
1193 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1194 frame.setFrameStyle(qw.QFrame.NoFrame)
1195 sarea.setWidget(frame)
1196 sarea.setWidgetResizable(True)
1197 layout = qw.QGridLayout()
1198 layout.setContentsMargins(0, 0, 0, 0)
1199 layout.setSpacing(0)
1200 frame.setLayout(layout)
1202 parlayout = qw.QGridLayout()
1204 irow = 0
1205 ipar = 0
1206 have_switches = False
1207 have_params = False
1208 for iparam, param in enumerate(params):
1209 if isinstance(param, Param):
1210 if param.minimum <= 0.0:
1211 param_control = LinValControl(
1212 high_is_none=param.high_is_none,
1213 low_is_none=param.low_is_none)
1214 else:
1215 param_control = ValControl(
1216 high_is_none=param.high_is_none,
1217 low_is_none=param.low_is_none,
1218 low_is_zero=param.low_is_zero)
1220 param_control.setup(
1221 param.name,
1222 param.minimum,
1223 param.maximum,
1224 param.default,
1225 iparam)
1227 param_control.valchange.connect(
1228 self.modified_snuffling_panel)
1230 self._param_controls[param.ident] = param_control
1231 for iw, w in enumerate(param_control.widgets()):
1232 parlayout.addWidget(w, ipar, iw)
1234 ipar += 1
1235 have_params = True
1237 elif isinstance(param, Choice):
1238 param_widget = ChoiceControl(
1239 param.ident, param.default, param.choices, param.name)
1240 param_widget.choosen.connect(
1241 self.choose_on_snuffling_panel)
1243 self._param_controls[param.ident] = param_widget
1244 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1245 ipar += 1
1246 have_params = True
1248 elif isinstance(param, Switch):
1249 have_switches = True
1251 if have_params:
1252 parframe = qw.QFrame(sarea)
1253 parframe.setSizePolicy(qw.QSizePolicy(
1254 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1255 parframe.setLayout(parlayout)
1256 layout.addWidget(parframe, irow, 0)
1257 irow += 1
1259 if have_switches:
1260 swlayout = qw.QGridLayout()
1261 isw = 0
1262 for iparam, param in enumerate(params):
1263 if isinstance(param, Switch):
1264 param_widget = SwitchControl(
1265 param.ident, param.default, param.name)
1266 param_widget.sw_toggled.connect(
1267 self.switch_on_snuffling_panel)
1269 self._param_controls[param.ident] = param_widget
1270 swlayout.addWidget(param_widget, isw/10, isw % 10)
1271 isw += 1
1273 swframe = qw.QFrame(sarea)
1274 swframe.setSizePolicy(qw.QSizePolicy(
1275 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1276 swframe.setLayout(swlayout)
1277 layout.addWidget(swframe, irow, 0)
1278 irow += 1
1280 butframe = qw.QFrame(sarea)
1281 butframe.setSizePolicy(qw.QSizePolicy(
1282 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1283 butlayout = qw.QHBoxLayout()
1284 butframe.setLayout(butlayout)
1286 live_update_checkbox = qw.QCheckBox('Auto-Run')
1287 if self._live_update:
1288 live_update_checkbox.setCheckState(qc.Qt.Checked)
1290 butlayout.addWidget(live_update_checkbox)
1291 live_update_checkbox.toggled.connect(
1292 self.live_update_toggled)
1294 help_button = qw.QPushButton('Help')
1295 butlayout.addWidget(help_button)
1296 help_button.clicked.connect(
1297 self.help_button_triggered)
1299 clear_button = qw.QPushButton('Clear')
1300 butlayout.addWidget(clear_button)
1301 clear_button.clicked.connect(
1302 self.clear_button_triggered)
1304 call_button = qw.QPushButton('Run')
1305 butlayout.addWidget(call_button)
1306 call_button.clicked.connect(
1307 self.call_button_triggered)
1309 for name, method in self._triggers:
1310 but = qw.QPushButton(name)
1312 def call_and_update(method):
1313 def f():
1314 try:
1315 method()
1316 except SnufflingError as e:
1317 if not isinstance(e, SnufflingCallFailed):
1318 # those have logged within error()
1319 logger.error('%s: %s' % (self._name, e))
1320 logger.error(
1321 '%s: Snuffling action failed' % self._name)
1323 self.get_viewer().update()
1324 return f
1326 but.clicked.connect(
1327 call_and_update(method))
1329 butlayout.addWidget(but)
1331 layout.addWidget(butframe, irow, 0)
1333 irow += 1
1334 spacer = qw.QSpacerItem(
1335 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1337 layout.addItem(spacer, irow, 0)
1339 return sarea
1341 else:
1342 return None
1344 def make_helpmenuitem(self, parent):
1345 '''
1346 Create the help menu item for the snuffling.
1347 '''
1349 item = qw.QAction(self.get_name(), None)
1351 item.triggered.connect(
1352 self.help_button_triggered)
1354 return item
1356 def make_menuitem(self, parent):
1357 '''
1358 Create the menu item for the snuffling.
1360 This method may be overloaded in subclass and return ``None``, if no
1361 menu entry is wanted.
1362 '''
1364 item = qw.QAction(self.get_name(), None)
1365 item.setCheckable(
1366 self._have_pre_process_hook or self._have_post_process_hook)
1368 item.triggered.connect(
1369 self.menuitem_triggered)
1371 return item
1373 def output_filename(
1374 self,
1375 caption='Save File',
1376 dir='',
1377 filter='',
1378 selected_filter=None):
1380 '''
1381 Query user for an output filename.
1383 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1384 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1385 dialog.
1386 '''
1388 if not dir and self._previous_output_filename:
1389 dir = self._previous_output_filename
1391 fn = getSaveFileName(
1392 self.get_viewer(), caption, dir, filter, selected_filter)
1393 if not fn:
1394 raise UserCancelled()
1396 self._previous_output_filename = fn
1397 return str(fn)
1399 def input_directory(self, caption='Open Directory', dir=''):
1400 '''
1401 Query user for an input directory.
1403 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1404 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1405 dialog.
1406 '''
1408 if not dir and self._previous_input_directory:
1409 dir = self._previous_input_directory
1411 dn = qw.QFileDialog.getExistingDirectory(
1412 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1414 if not dn:
1415 raise UserCancelled()
1417 self._previous_input_directory = dn
1418 return str(dn)
1420 def input_filename(self, caption='Open File', dir='', filter='',
1421 selected_filter=None):
1422 '''
1423 Query user for an input filename.
1425 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1426 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1427 dialog.
1428 '''
1430 if not dir and self._previous_input_filename:
1431 dir = self._previous_input_filename
1433 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1434 self.get_viewer(),
1435 caption,
1436 dir,
1437 filter)) # selected_filter)
1439 if not fn:
1440 raise UserCancelled()
1442 self._previous_input_filename = fn
1443 return str(fn)
1445 def input_dialog(self, caption='', request='', directory=False):
1446 '''
1447 Query user for a text input.
1449 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1450 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1451 dialog.
1452 '''
1454 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1456 if not ok:
1457 raise UserCancelled()
1459 return inp
1461 def modified_snuffling_panel(self, value, iparam):
1462 '''
1463 Called when the user has played with an adjustable parameter.
1465 The default implementation sets the parameter, calls the snuffling's
1466 :py:meth:`call` method and finally triggers an update on the viewer
1467 widget.
1468 '''
1470 param = self.get_parameters()[iparam]
1471 self._set_parameter_value(param.ident, value)
1472 if self._live_update:
1473 self.check_call()
1474 self.get_viewer().update()
1476 def switch_on_snuffling_panel(self, ident, state):
1477 '''
1478 Called when the user has toggled a switchable parameter.
1479 '''
1481 self._set_parameter_value(ident, state)
1482 if self._live_update:
1483 self.check_call()
1484 self.get_viewer().update()
1486 def choose_on_snuffling_panel(self, ident, state):
1487 '''
1488 Called when the user has made a choice about a choosable parameter.
1489 '''
1491 self._set_parameter_value(ident, state)
1492 if self._live_update:
1493 self.check_call()
1494 self.get_viewer().update()
1496 def menuitem_triggered(self, arg):
1497 '''
1498 Called when the user has triggered the snuffling's menu.
1500 The default implementation calls the snuffling's :py:meth:`call` method
1501 and triggers an update on the viewer widget.
1502 '''
1504 self.check_call()
1506 if self._have_pre_process_hook:
1507 self._pre_process_hook_enabled = arg
1509 if self._have_post_process_hook:
1510 self._post_process_hook_enabled = arg
1512 if self._have_pre_process_hook or self._have_post_process_hook:
1513 self.get_viewer().clean_update()
1514 else:
1515 self.get_viewer().update()
1517 def call_button_triggered(self):
1518 '''
1519 Called when the user has clicked the snuffling's call button.
1521 The default implementation calls the snuffling's :py:meth:`call` method
1522 and triggers an update on the viewer widget.
1523 '''
1525 self.check_call()
1526 self.get_viewer().update()
1528 def clear_button_triggered(self):
1529 '''
1530 Called when the user has clicked the snuffling's clear button.
1532 This calls the :py:meth:`cleanup` method and triggers an update on the
1533 viewer widget.
1534 '''
1536 self.cleanup()
1537 self.get_viewer().update()
1539 def help_button_triggered(self):
1540 '''
1541 Creates a :py:class:`QLabel` which contains the documentation as
1542 given in the snufflings' __doc__ string.
1543 '''
1545 if self.__doc__:
1546 if self.__doc__.strip().startswith('<html>'):
1547 doc = qw.QLabel(self.__doc__)
1548 else:
1549 try:
1550 import markdown
1551 doc = qw.QLabel(markdown.markdown(self.__doc__))
1553 except ImportError:
1554 logger.error(
1555 'Install Python module "markdown" for pretty help '
1556 'formatting.')
1558 doc = qw.QLabel(self.__doc__)
1559 else:
1560 doc = qw.QLabel('This snuffling does not provide any online help.')
1562 labels = [doc]
1564 if self._filename:
1565 from html import escape
1567 code = open(self._filename, 'r').read()
1569 doc_src = qw.QLabel(
1570 '''<html><body>
1571<hr />
1572<center><em>May the source be with you, young Skywalker!</em><br /><br />
1573<a href="file://%s"><code>%s</code></a></center>
1574<br />
1575<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1576<pre style="white-space: pre-wrap"><code>%s
1577</code></pre></p></body></html>'''
1578 % (
1579 quote(self._filename),
1580 escape(self._filename),
1581 escape(code)))
1583 labels.append(doc_src)
1585 for h in labels:
1586 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1587 h.setWordWrap(True)
1589 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1591 def live_update_toggled(self, on):
1592 '''
1593 Called when the checkbox for live-updates has been toggled.
1594 '''
1596 self.set_live_update(on)
1598 def add_traces(self, traces):
1599 '''
1600 Add traces to the viewer.
1602 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1604 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1605 added to the viewer's internal pile for display. Note, that unlike with
1606 the traces from the files given on the command line, these traces are
1607 kept in memory and so may quickly occupy a lot of ram if a lot of
1608 traces are added.
1610 This method should be preferred over modifying the viewer's internal
1611 pile directly, because this way, the snuffling has a chance to
1612 automatically remove its private traces again (see :py:meth:`cleanup`
1613 method).
1614 '''
1616 ticket = self.get_viewer().add_traces(traces)
1617 self._tickets.append(ticket)
1618 return ticket
1620 def add_trace(self, tr):
1621 '''
1622 Add a trace to the viewer.
1624 See :py:meth:`add_traces`.
1625 '''
1627 self.add_traces([tr])
1629 def add_markers(self, markers):
1630 '''
1631 Add some markers to the display.
1633 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1634 adds these to the viewer.
1635 '''
1637 self.get_viewer().add_markers(markers)
1638 self._markers.extend(markers)
1640 def add_marker(self, marker):
1641 '''
1642 Add a marker to the display.
1644 See :py:meth:`add_markers`.
1645 '''
1647 self.add_markers([marker])
1649 def cleanup(self):
1650 '''
1651 Remove all traces and markers which have been added so far by the
1652 snuffling.
1653 '''
1655 try:
1656 viewer = self.get_viewer()
1657 viewer.release_data(self._tickets)
1658 viewer.remove_markers(self._markers)
1660 except NoViewerSet:
1661 pass
1663 self._tickets = []
1664 self._markers = []
1666 def check_call(self):
1668 if self._call_in_progress:
1669 self.show_message('error', 'Previous action still in progress.')
1670 return
1672 try:
1673 self._call_in_progress = True
1674 self.call()
1675 return 0
1677 except SnufflingError as e:
1678 if not isinstance(e, SnufflingCallFailed):
1679 # those have logged within error()
1680 logger.error('%s: %s' % (self._name, e))
1681 logger.error('%s: Snuffling action failed' % self._name)
1682 return 1
1684 except Exception as e:
1685 message = '%s: Snuffling action raised an exception: %s' % (
1686 self._name, str(e))
1688 logger.exception(message)
1689 self.show_message('error', message)
1691 finally:
1692 self._call_in_progress = False
1694 def call(self):
1695 '''
1696 Main work routine of the snuffling.
1698 This method is called when the snuffling's menu item has been triggered
1699 or when the user has played with the panel controls. To be overloaded
1700 in subclass. The default implementation does nothing useful.
1701 '''
1703 pass
1705 def pre_process_hook(self, traces):
1706 return traces
1708 def post_process_hook(self, traces):
1709 return traces
1711 def get_tpad(self):
1712 '''
1713 Return current amount of extra padding needed by live processing hooks.
1714 '''
1716 return 0.0
1718 def pre_destroy(self):
1719 '''
1720 Called when the snuffling instance is about to be deleted.
1722 Can be overloaded to do user-defined cleanup actions. The
1723 default implementation calls :py:meth:`cleanup` and deletes
1724 the snuffling`s tempory directory, if needed.
1725 '''
1727 self.cleanup()
1728 if self._tempdir is not None:
1729 import shutil
1730 shutil.rmtree(self._tempdir)
1733class SnufflingError(Exception):
1734 pass
1737class NoViewerSet(SnufflingError):
1738 '''
1739 This exception is raised, when no viewer has been set on a Snuffling.
1740 '''
1742 def __str__(self):
1743 return 'No GUI available. ' \
1744 'Maybe this Snuffling cannot be run in command line mode?'
1747class MissingStationInformation(SnufflingError):
1748 '''
1749 Raised when station information is missing.
1750 '''
1753class NoTracesSelected(SnufflingError):
1754 '''
1755 This exception is raised, when no traces have been selected in the viewer
1756 and we cannot fallback to using the current view.
1757 '''
1759 def __str__(self):
1760 return 'No traces have been selected / are available.'
1763class UserCancelled(SnufflingError):
1764 '''
1765 This exception is raised, when the user has cancelled a snuffling dialog.
1766 '''
1768 def __str__(self):
1769 return 'The user has cancelled a dialog.'
1772class SnufflingCallFailed(SnufflingError):
1773 '''
1774 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1775 :py:meth:`Snuffling.call`.
1776 '''
1779class InvalidSnufflingFilename(Exception):
1780 pass
1783class SnufflingModule(object):
1784 '''
1785 Utility class to load/reload snufflings from a file.
1787 The snufflings are created by user modules which have the special function
1788 :py:func:`__snufflings__` which return the snuffling instances to be
1789 exported. The snuffling module is attached to a handler class, which makes
1790 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1791 from ``pile_viewer.py``). The handler class must implement the methods
1792 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1793 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1794 :py:meth:`remove_snufflings` which may be called from the handler class,
1795 when needed.
1796 '''
1798 mtimes = {}
1800 def __init__(self, path, name, handler):
1801 self._path = path
1802 self._name = name
1803 self._mtime = None
1804 self._module = None
1805 self._snufflings = []
1806 self._handler = handler
1808 def load_if_needed(self):
1809 filename = os.path.join(self._path, self._name+'.py')
1811 try:
1812 mtime = os.stat(filename)[8]
1813 except OSError as e:
1814 if e.errno == 2:
1815 logger.error(e)
1816 raise BrokenSnufflingModule(filename)
1818 if self._module is None:
1819 sys.path[0:0] = [self._path]
1820 try:
1821 logger.debug('Loading snuffling module %s' % filename)
1822 if self._name in sys.modules:
1823 raise InvalidSnufflingFilename(self._name)
1825 self._module = __import__(self._name)
1826 del sys.modules[self._name]
1828 for snuffling in self._module.__snufflings__():
1829 snuffling._filename = filename
1830 self.add_snuffling(snuffling)
1832 except Exception:
1833 logger.error(traceback.format_exc())
1834 raise BrokenSnufflingModule(filename)
1836 finally:
1837 sys.path[0:1] = []
1839 elif self._mtime != mtime:
1840 logger.warning('Reloading snuffling module %s' % filename)
1841 settings = self.remove_snufflings()
1842 sys.path[0:0] = [self._path]
1843 try:
1845 sys.modules[self._name] = self._module
1847 reload(self._module)
1848 del sys.modules[self._name]
1850 for snuffling in self._module.__snufflings__():
1851 snuffling._filename = filename
1852 self.add_snuffling(snuffling, reloaded=True)
1854 if len(self._snufflings) == len(settings):
1855 for sett, snuf in zip(settings, self._snufflings):
1856 snuf.set_settings(sett)
1858 except Exception:
1859 logger.error(traceback.format_exc())
1860 raise BrokenSnufflingModule(filename)
1862 finally:
1863 sys.path[0:1] = []
1865 self._mtime = mtime
1867 def add_snuffling(self, snuffling, reloaded=False):
1868 snuffling._path = self._path
1869 snuffling.setup()
1870 self._snufflings.append(snuffling)
1871 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1873 def remove_snufflings(self):
1874 settings = []
1875 for snuffling in self._snufflings:
1876 settings.append(snuffling.get_settings())
1877 self._handler.remove_snuffling(snuffling)
1879 self._snufflings = []
1880 return settings
1883class BrokenSnufflingModule(Exception):
1884 pass
1887class MyScrollArea(qw.QScrollArea):
1889 def sizeHint(self):
1890 s = qc.QSize()
1891 s.setWidth(self.widget().sizeHint().width())
1892 s.setHeight(self.widget().sizeHint().height())
1893 return s
1896class SwitchControl(qw.QCheckBox):
1897 sw_toggled = qc.pyqtSignal(object, bool)
1899 def __init__(self, ident, default, *args):
1900 qw.QCheckBox.__init__(self, *args)
1901 self.ident = ident
1902 self.setChecked(default)
1903 self.toggled.connect(self._sw_toggled)
1905 def _sw_toggled(self, state):
1906 self.sw_toggled.emit(self.ident, state)
1908 def set_value(self, state):
1909 self.blockSignals(True)
1910 self.setChecked(state)
1911 self.blockSignals(False)
1914class ChoiceControl(qw.QFrame):
1915 choosen = qc.pyqtSignal(object, object)
1917 def __init__(self, ident, default, choices, name, *args):
1918 qw.QFrame.__init__(self, *args)
1919 self.label = qw.QLabel(name, self)
1920 self.label.setMinimumWidth(120)
1921 self.cbox = qw.QComboBox(self)
1922 self.layout = qw.QHBoxLayout(self)
1923 self.layout.addWidget(self.label)
1924 self.layout.addWidget(self.cbox)
1925 self.layout.setContentsMargins(0, 0, 0, 0)
1926 self.layout.setSpacing(0)
1927 self.ident = ident
1928 self.choices = choices
1929 for ichoice, choice in enumerate(choices):
1930 self.cbox.addItem(choice)
1932 self.set_value(default)
1933 self.cbox.activated.connect(self.emit_choosen)
1935 def set_choices(self, choices):
1936 icur = self.cbox.currentIndex()
1937 if icur != -1:
1938 selected_choice = choices[icur]
1939 else:
1940 selected_choice = None
1942 self.choices = choices
1943 self.cbox.clear()
1944 for ichoice, choice in enumerate(choices):
1945 self.cbox.addItem(qc.QString(choice))
1947 if selected_choice is not None and selected_choice in choices:
1948 self.set_value(selected_choice)
1949 return selected_choice
1950 else:
1951 self.set_value(choices[0])
1952 return choices[0]
1954 def emit_choosen(self, i):
1955 self.choosen.emit(
1956 self.ident,
1957 self.choices[i])
1959 def set_value(self, v):
1960 self.cbox.blockSignals(True)
1961 for i, choice in enumerate(self.choices):
1962 if choice == v:
1963 self.cbox.setCurrentIndex(i)
1964 self.cbox.blockSignals(False)