1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Snuffling infrastructure
9This module provides the base class :py:class:`Snuffling` for user-defined
10snufflings and some utilities for their handling.
11'''
13import os
14import sys
15import time
16import logging
17import traceback
18import tempfile
20from ..qt_compat import qc, qw, getSaveFileName
22from pyrocko import pile, config
23from pyrocko.util import quote
25from ..util import (
26 ValControl, LinValControl, FigureFrame, SmartplotFrame, WebKitFrame,
27 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker, load_markers,
28 save_markers)
30from importlib import reload
32Marker, load_markers, save_markers # noqa
34logger = logging.getLogger('pyrocko.gui.snuffler.snuffling')
37class MyFrame(qw.QFrame):
38 widgetVisibilityChanged = qc.pyqtSignal(bool)
40 def showEvent(self, ev):
41 self.widgetVisibilityChanged.emit(True)
43 def hideEvent(self, ev):
44 self.widgetVisibilityChanged.emit(False)
47class Param(object):
48 '''
49 Definition of an adjustable floating point parameter for the
50 snuffling. The snuffling may display controls for user input for
51 such parameters.
53 :param name: labels the parameter on the snuffling's control panel
54 :param ident: identifier of the parameter
55 :param default: default value
56 :param minimum: minimum value for the parameter
57 :param maximum: maximum value for the parameter
58 :param low_is_none: if ``True``: parameter is set to None at lowest value
59 of parameter range (optional)
60 :param high_is_none: if ``True``: parameter is set to None at highest value
61 of parameter range (optional)
62 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
63 value of parameter range (optional)
64 '''
66 def __init__(
67 self, name, ident, default, minimum, maximum,
68 low_is_none=None,
69 high_is_none=None,
70 low_is_zero=False,
71 tracking=True,
72 type=float):
74 if low_is_none and default == minimum:
75 default = None
76 if high_is_none and default == maximum:
77 default = None
79 self.name = name
80 self.ident = ident
81 self.default = default
82 self.minimum = minimum
83 self.maximum = maximum
84 self.low_is_none = low_is_none
85 self.high_is_none = high_is_none
86 self.low_is_zero = low_is_zero
87 self.tracking = tracking
88 self.type = type
90 self._control = None
93class Switch(object):
94 '''
95 Definition of a boolean switch for the snuffling. The snuffling
96 may display a checkbox for such a switch.
98 :param name: labels the switch on the snuffling's control panel
99 :param ident: identifier of the parameter
100 :param default: default value
101 '''
103 def __init__(self, name, ident, default):
104 self.name = name
105 self.ident = ident
106 self.default = default
109class Choice(object):
110 '''
111 Definition of a string choice for the snuffling. The snuffling
112 may display a menu for such a choice.
114 :param name: labels the menu on the snuffling's control panel
115 :param ident: identifier of the parameter
116 :param default: default value
117 :param choices: tuple of other options
118 '''
120 def __init__(self, name, ident, default, choices):
121 self.name = name
122 self.ident = ident
123 self.default = default
124 self.choices = choices
127class Snuffling(object):
128 '''
129 Base class for user snufflings.
131 Snufflings are plugins for snuffler (and other applications using the
132 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in
133 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
134 should provide a simple way of extending the functionality of snuffler.
136 A snuffling has access to all data available in a pile viewer, can process
137 this data and can create and add new traces and markers to the viewer.
138 '''
140 def __init__(self):
141 self._path = None
143 self._name = 'Untitled Snuffling'
144 self._viewer = None
145 self._tickets = []
146 self._markers = []
148 self._delete_panel = None
149 self._delete_menuitem = None
151 self._panel_parent = None
152 self._menu_parent = None
154 self._panel = None
155 self._menuitem = None
156 self._helpmenuitem = None
157 self._parameters = []
158 self._param_controls = {}
160 self._triggers = []
162 self._live_update = True
163 self._previous_output_filename = None
164 self._previous_input_filename = None
165 self._previous_input_directory = None
167 self._tempdir = None
168 self._iplot = 0
170 self._have_pre_process_hook = False
171 self._have_post_process_hook = False
172 self._pre_process_hook_enabled = False
173 self._post_process_hook_enabled = False
175 self._no_viewer_pile = None
176 self._cli_params = {}
177 self._filename = None
178 self._force_panel = False
179 self._call_in_progress = {}
181 def setup(self):
182 '''
183 Setup the snuffling.
185 This method should be implemented in subclass and contain e.g. calls to
186 :py:meth:`set_name` and :py:meth:`add_parameter`.
187 '''
189 pass
191 def module_dir(self):
192 '''
193 Returns the path of the directory where snufflings are stored.
195 The default path is ``$HOME/.snufflings``.
196 '''
198 return self._path
200 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
201 '''
202 Set parent viewer and hooks to add panel and menu entry.
204 This method is called from the
205 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls
206 :py:meth:`setup_gui`.
207 '''
209 self._viewer = viewer
210 self._panel_parent = panel_parent
211 self._menu_parent = menu_parent
213 self.setup_gui(reloaded=reloaded)
215 def setup_gui(self, reloaded=False):
216 '''
217 Create and add gui elements to the viewer.
219 This method is initially called from :py:meth:`init_gui`. It is also
220 called, e.g. when new parameters have been added or if the name of the
221 snuffling has been changed.
222 '''
224 if self._panel_parent is not None:
225 self._panel = self.make_panel(self._panel_parent)
226 if self._panel:
227 self._panel_parent.add_panel(
228 self.get_name(), self._panel, reloaded)
230 if self._menu_parent is not None:
231 self._menuitem = self.make_menuitem(self._menu_parent)
232 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
233 if self._menuitem:
234 self._menu_parent.add_snuffling_menuitem(self._menuitem)
236 if self._helpmenuitem:
237 self._menu_parent.add_snuffling_help_menuitem(
238 self._helpmenuitem)
240 def set_force_panel(self, bool=True):
241 '''
242 Force to create a panel.
244 :param bool: if ``True`` will create a panel with Help, Clear and Run
245 button.
246 '''
248 self._force_panel = bool
250 def make_cli_parser1(self):
251 import optparse
253 class MyOptionParser(optparse.OptionParser):
254 def error(self, msg):
255 logger.error(msg)
256 self.exit(1)
258 parser = MyOptionParser()
260 parser.add_option(
261 '--format',
262 dest='format',
263 default='from_extension',
264 choices=(
265 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
266 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
267 'from_extension', 'detect'),
268 help="assume files are of given FORMAT [default: '%default']")
270 parser.add_option(
271 '--pattern',
272 dest='regex',
273 metavar='REGEX',
274 help='only include files whose paths match REGEX')
276 self.add_params_to_cli_parser(parser)
277 self.configure_cli_parser(parser)
278 return parser
280 def configure_cli_parser(self, parser):
281 pass
283 def cli_usage(self):
284 return None
286 def add_params_to_cli_parser(self, parser):
288 for param in self._parameters:
289 if isinstance(param, Param):
290 parser.add_option(
291 '--' + param.ident,
292 dest=param.ident,
293 default=param.default,
294 type={float: 'float', int: 'int'}[param.type],
295 help=param.name)
297 def setup_cli(self):
298 self.setup()
299 parser = self.make_cli_parser1()
300 (options, args) = parser.parse_args()
302 for param in self._parameters:
303 if isinstance(param, Param):
304 setattr(self, param.ident, getattr(options, param.ident))
306 self._cli_params['regex'] = options.regex
307 self._cli_params['format'] = options.format
308 self._cli_params['sources'] = args
310 return options, args, parser
312 def delete_gui(self):
313 '''
314 Remove the gui elements of the snuffling.
316 This removes the panel and menu entry of the widget from the viewer and
317 also removes all traces and markers added with the
318 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
319 '''
321 self.cleanup()
323 if self._panel is not None:
324 self._panel_parent.remove_panel(self._panel)
325 self._panel = None
327 if self._menuitem is not None:
328 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
329 self._menuitem = None
331 if self._helpmenuitem is not None:
332 self._menu_parent.remove_snuffling_help_menuitem(
333 self._helpmenuitem)
335 def set_name(self, name):
336 '''
337 Set the snuffling's name.
339 The snuffling's name is shown as a menu entry and in the panel header.
340 '''
342 self._name = name
343 self.reset_gui()
345 def get_name(self):
346 '''
347 Get the snuffling's name.
348 '''
350 return self._name
352 def set_have_pre_process_hook(self, bool):
353 self._have_pre_process_hook = bool
354 self._live_update = False
355 self._pre_process_hook_enabled = False
356 self.reset_gui()
358 def set_have_post_process_hook(self, bool):
359 self._have_post_process_hook = bool
360 self._live_update = False
361 self._post_process_hook_enabled = False
362 self.reset_gui()
364 def set_have_pile_changed_hook(self, bool):
365 self._pile_ = False
367 def enable_pile_changed_notifications(self):
368 '''
369 Get informed when pile changed.
371 When activated, the :py:meth:`pile_changed` method is called on every
372 update in the viewer's pile.
373 '''
375 viewer = self.get_viewer()
376 viewer.pile_has_changed_signal.connect(
377 self.pile_changed)
379 def disable_pile_changed_notifications(self):
380 '''
381 Stop getting informed about changes in viewer's pile.
382 '''
384 viewer = self.get_viewer()
385 viewer.pile_has_changed_signal.disconnect(
386 self.pile_changed)
388 def pile_changed(self):
389 '''
390 Called when the connected viewer's pile has changed.
392 Must be activated with a call to
393 :py:meth:`enable_pile_changed_notifications`.
394 '''
396 pass
398 def reset_gui(self, reloaded=False):
399 '''
400 Delete and recreate the snuffling's panel.
401 '''
403 if self._panel or self._menuitem:
404 sett = self.get_settings()
405 self.delete_gui()
406 self.setup_gui(reloaded=reloaded)
407 self.set_settings(sett)
409 def show_message(self, kind, message):
410 '''
411 Display a message box.
413 :param kind: string defining kind of message
414 :param message: the message to be displayed
415 '''
417 try:
418 box = qw.QMessageBox(self.get_viewer())
419 box.setText('%s: %s' % (kind.capitalize(), message))
420 box.exec_()
421 except NoViewerSet:
422 pass
424 def error(self, message):
425 '''
426 Show an error message box.
428 :param message: specifying the error
429 '''
431 logger.error('%s: %s' % (self._name, message))
432 self.show_message('error', message)
434 def warn(self, message):
435 '''
436 Display a warning message.
438 :param message: specifying the warning
439 '''
441 logger.warning('%s: %s' % (self._name, message))
442 self.show_message('warning', message)
444 def fail(self, message):
445 '''
446 Show an error message box and raise :py:exc:`SnufflingCallFailed`
447 exception.
449 :param message: specifying the error
450 '''
452 self.error(message)
453 raise SnufflingCallFailed(message)
455 def pylab(self, name=None, get='axes', figure_cls=None):
456 '''
457 Create a :py:class:`FigureFrame` and return either the frame,
458 a :py:class:`matplotlib.figure.Figure` instance or a
459 :py:class:`matplotlib.axes.Axes` instance.
461 :param name: labels the figure frame's tab
462 :param get: 'axes'|'figure'|'frame' (optional)
463 '''
465 if name is None:
466 self._iplot += 1
467 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
469 fframe = FigureFrame(figure_cls=figure_cls)
470 self._panel_parent.add_tab(name, fframe)
471 if get == 'axes':
472 return fframe.gca()
473 elif get == 'figure':
474 return fframe.gcf()
475 elif get == 'figure_frame':
476 return fframe
478 def figure(self, name=None):
479 '''
480 Returns a :py:class:`matplotlib.figure.Figure` instance
481 which can be displayed within snuffler by calling
482 :py:meth:`canvas.draw`.
484 :param name: labels the tab of the figure
485 '''
487 return self.pylab(name=name, get='figure')
489 def axes(self, name=None):
490 '''
491 Returns a :py:class:`matplotlib.axes.Axes` instance.
493 :param name: labels the tab of axes
494 '''
496 return self.pylab(name=name, get='axes')
498 def figure_frame(self, name=None, figure_cls=None):
499 '''
500 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
502 :param name: labels the tab figure frame
503 '''
505 return self.pylab(name=name, get='figure_frame', figure_cls=figure_cls)
507 def smartplot_frame(self, name, *args, plot_cls=None, **kwargs):
508 '''
509 Create a :py:class:`pyrocko.gui.util.SmartplotFrame`.
511 :param name: labels the tab
512 :param *args, **kwargs:
513 passed to :py:class:`pyrocko.plot.smartplot.Plot`
514 :param plot_cls:
515 if given, subclass to be used instead of
516 :py:class:`pyrocko.plot.smartplot.Plot`
517 '''
518 frame = SmartplotFrame(
519 plot_args=args,
520 plot_cls=plot_cls,
521 plot_kwargs=kwargs)
523 self._panel_parent.add_tab(name, frame)
524 return frame
526 def pixmap_frame(self, filename=None, name=None):
527 '''
528 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
530 :param name: labels the tab
531 :param filename: name of file to be displayed
532 '''
534 f = PixmapFrame(filename)
536 scroll_area = qw.QScrollArea()
537 scroll_area.setWidget(f)
538 scroll_area.setWidgetResizable(True)
540 self._panel_parent.add_tab(name or 'Pixmap', scroll_area)
541 return f
543 def web_frame(self, url=None, name=None):
544 '''
545 Creates a :py:class:`WebKitFrame` which can be used as a browser
546 within snuffler.
548 :param url: url to open
549 :param name: labels the tab
550 '''
552 if name is None:
553 self._iplot += 1
554 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
556 f = WebKitFrame(url)
557 self._panel_parent.add_tab(name, f)
558 return f
560 def vtk_frame(self, name=None, actors=None):
561 '''
562 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
563 graphics.
565 :param actors: list of VTKActors
566 :param name: labels the tab
568 Initialize the interactive rendering by calling the frames'
569 :py:meth`initialize` method after having added all actors to the frames
570 renderer.
572 Requires installation of vtk including python wrapper.
573 '''
574 if name is None:
575 self._iplot += 1
576 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
578 try:
579 f = VTKFrame(actors=actors)
580 except ImportError as e:
581 self.fail(e)
583 self._panel_parent.add_tab(name, f)
584 return f
586 def tempdir(self):
587 '''
588 Create a temporary directory and return its absolute path.
590 The directory and all its contents are removed when the Snuffling
591 instance is deleted.
592 '''
594 if self._tempdir is None:
595 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
597 return self._tempdir
599 def set_live_update(self, live_update):
600 '''
601 Enable/disable live updating.
603 When live updates are enabled, the :py:meth:`call` method is called
604 whenever the user changes a parameter. If it is disabled, the user has
605 to initiate such a call manually by triggering the snuffling's menu
606 item or pressing the call button.
607 '''
609 self._live_update = live_update
610 if self._have_pre_process_hook:
611 self._pre_process_hook_enabled = live_update
612 if self._have_post_process_hook:
613 self._post_process_hook_enabled = live_update
615 try:
616 self.get_viewer().clean_update()
617 except NoViewerSet:
618 pass
620 def add_parameter(self, param):
621 '''
622 Add an adjustable parameter to the snuffling.
624 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
625 :py:class:`Choice`.
627 For each parameter added, controls are added to the snuffling's panel,
628 so that the parameter can be adjusted from the gui.
629 '''
631 self._parameters.append(param)
632 self._set_parameter_value(param.ident, param.default)
634 if self._panel is not None:
635 self.delete_gui()
636 self.setup_gui()
638 def add_trigger(self, name, method):
639 '''
640 Add a button to the snuffling's panel.
642 :param name: string that labels the button
643 :param method: method associated with the button
644 '''
646 self._triggers.append((name, method))
648 if self._panel is not None:
649 self.delete_gui()
650 self.setup_gui()
652 def get_parameters(self):
653 '''
654 Get the snuffling's adjustable parameter definitions.
656 Returns a list of objects of type :py:class:`Param`.
657 '''
659 return self._parameters
661 def get_parameter(self, ident):
662 '''
663 Get one of the snuffling's adjustable parameter definitions.
665 :param ident: identifier of the parameter
667 Returns an object of type :py:class:`Param` or ``None``.
668 '''
670 for param in self._parameters:
671 if param.ident == ident:
672 return param
673 return None
675 def set_parameter(self, ident, value):
676 '''
677 Set one of the snuffling's adjustable parameters.
679 :param ident: identifier of the parameter
680 :param value: new value of the parameter
682 Adjusts the control of a parameter without calling :py:meth:`call`.
683 '''
685 self._set_parameter_value(ident, value)
687 control = self._param_controls.get(ident, None)
688 if control:
689 control.set_value(value)
691 def set_parameter_range(self, ident, vmin, vmax):
692 '''
693 Set the range of one of the snuffling's adjustable parameters.
695 :param ident: identifier of the parameter
696 :param vmin,vmax: new minimum and maximum value for the parameter
698 Adjusts the control of a parameter without calling :py:meth:`call`.
699 '''
701 control = self._param_controls.get(ident, None)
702 if control:
703 control.set_range(vmin, vmax)
705 def set_parameter_choices(self, ident, choices):
706 '''
707 Update the choices of a Choice parameter.
709 :param ident: identifier of the parameter
710 :param choices: list of strings
711 '''
713 control = self._param_controls.get(ident, None)
714 if control:
715 selected_choice = control.set_choices(choices)
716 self._set_parameter_value(ident, selected_choice)
718 def _set_parameter_value(self, ident, value):
719 setattr(self, ident, value)
721 def get_parameter_value(self, ident):
722 '''
723 Get the current value of a parameter.
725 :param ident: identifier of the parameter
726 '''
727 return getattr(self, ident)
729 def get_settings(self):
730 '''
731 Returns a dictionary with identifiers of all parameters as keys and
732 their values as the dictionaries values.
733 '''
735 params = self.get_parameters()
736 settings = {}
737 for param in params:
738 settings[param.ident] = self.get_parameter_value(param.ident)
740 return settings
742 def set_settings(self, settings):
743 params = self.get_parameters()
744 dparams = dict([(param.ident, param) for param in params])
745 for k, v in settings.items():
746 if k in dparams:
747 self._set_parameter_value(k, v)
748 if k in self._param_controls:
749 control = self._param_controls[k]
750 control.set_value(v)
752 def get_viewer(self):
753 '''
754 Get the parent viewer.
756 Returns a reference to an object of type :py:class:`PileOverview`,
757 which is the main viewer widget.
759 If no gui has been initialized for the snuffling, a
760 :py:exc:`NoViewerSet` exception is raised.
761 '''
763 if self._viewer is None:
764 raise NoViewerSet()
765 return self._viewer
767 def get_pile(self):
768 '''
769 Get the pile.
771 If a gui has been initialized, a reference to the viewer's internal
772 pile is returned. If not, the :py:meth:`make_pile` method (which may be
773 overloaded in subclass) is called to create a pile. This can be
774 utilized to make hybrid snufflings, which may work also in a standalone
775 mode.
776 '''
778 try:
779 p = self.get_viewer().get_pile()
780 except NoViewerSet:
781 if self._no_viewer_pile is None:
782 self._no_viewer_pile = self.make_pile()
784 p = self._no_viewer_pile
786 return p
788 def get_active_event_and_stations(
789 self, trange=(-3600., 3600.), missing='warn'):
791 '''
792 Get event and stations with available data for active event.
794 :param trange: (begin, end), time range around event origin time to
795 query for available data
796 :param missing: string, what to do in case of missing station
797 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
799 :returns: ``(event, stations)``
800 '''
802 p = self.get_pile()
803 v = self.get_viewer()
805 event = v.get_active_event()
806 if event is None:
807 self.fail(
808 'No active event set. Select an event and press "e" to make '
809 'it the "active event"')
811 stations = {}
812 for traces in p.chopper(
813 event.time+trange[0],
814 event.time+trange[1],
815 load_data=False,
816 degap=False):
818 for tr in traces:
819 try:
820 for skey in v.station_keys(tr):
821 if skey in stations:
822 continue
824 station = v.get_station(skey)
825 stations[skey] = station
827 except KeyError:
828 s = 'No station information for station key "%s".' \
829 % '.'.join(skey)
831 if missing == 'warn':
832 logger.warning(s)
833 elif missing == 'raise':
834 raise MissingStationInformation(s)
835 elif missing == 'ignore':
836 pass
837 else:
838 assert False, 'invalid argument to "missing"'
840 stations[skey] = None
842 return event, list(set(
843 st for st in stations.values() if st is not None))
845 def get_stations(self):
846 '''
847 Get all stations known to the viewer.
848 '''
850 v = self.get_viewer()
851 stations = list(v.stations.values())
852 return stations
854 def get_markers(self):
855 '''
856 Get all markers from the viewer.
857 '''
859 return self.get_viewer().get_markers()
861 def get_event_markers(self):
862 '''
863 Get all event markers from the viewer.
864 '''
866 return [m for m in self.get_viewer().get_markers()
867 if isinstance(m, EventMarker)]
869 def get_selected_markers(self):
870 '''
871 Get all selected markers from the viewer.
872 '''
874 return self.get_viewer().selected_markers()
876 def get_selected_event_markers(self):
877 '''
878 Get all selected event markers from the viewer.
879 '''
881 return [m for m in self.get_viewer().selected_markers()
882 if isinstance(m, EventMarker)]
884 def get_active_event_and_phase_markers(self):
885 '''
886 Get the marker of the active event and any associated phase markers
887 '''
889 viewer = self.get_viewer()
890 markers = viewer.get_markers()
891 event_marker = viewer.get_active_event_marker()
892 if event_marker is None:
893 self.fail(
894 'No active event set. '
895 'Select an event and press "e" to make it the "active event"')
897 event = event_marker.get_event()
899 selection = []
900 for m in markers:
901 if isinstance(m, PhaseMarker):
902 if m.get_event() is event:
903 selection.append(m)
905 return (
906 event_marker,
907 [m for m in markers if isinstance(m, PhaseMarker) and
908 m.get_event() == event])
910 def get_viewer_trace_selector(self, mode='inview'):
911 '''
912 Get currently active trace selector from viewer.
914 :param mode: set to ``'inview'`` (default) to only include selections
915 currently shown in the viewer, ``'visible' to include all traces
916 not currenly hidden by hide or quick-select commands, or ``'all'``
917 to disable any restrictions.
918 '''
920 viewer = self.get_viewer()
922 def rtrue(tr):
923 return True
925 if mode == 'inview':
926 return viewer.trace_selector or rtrue
927 elif mode == 'visible':
928 return viewer.trace_filter or rtrue
929 elif mode == 'all':
930 return rtrue
931 else:
932 raise Exception('invalid mode argument')
934 def chopper_selected_traces(self, fallback=False, marker_selector=None,
935 mode='inview', main_bandpass=False,
936 progress=None, responsive=False,
937 *args, **kwargs):
938 '''
939 Iterate over selected traces.
941 Shortcut to get all trace data contained in the selected markers in the
942 running snuffler. For each selected marker,
943 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
944 *tmin*, *tmax*, and *trace_selector* set to values according to the
945 marker. Additional arguments to the chopper are handed over from
946 *\\*args* and *\\*\\*kwargs*.
948 :param fallback:
949 If ``True``, if no selection has been marked, use the content
950 currently visible in the viewer.
952 :param marker_selector:
953 If not ``None`` a callback to filter markers.
955 :param mode:
956 Set to ``'inview'`` (default) to only include selections currently
957 shown in the viewer (excluding traces accessible through vertical
958 scrolling), ``'visible'`` to include all traces not currently
959 hidden by hide or quick-select commands (including traces
960 accessible through vertical scrolling), or ``'all'`` to disable any
961 restrictions.
963 :param main_bandpass:
964 If ``True``, apply main control high- and lowpass filters to
965 traces. Note: use with caution. Processing is fixed to use 4th
966 order Butterworth highpass and lowpass and the signal is always
967 demeaned before filtering. FFT filtering, rotation, demean and
968 bandpass settings from the graphical interface are not respected
969 here. Padding is not automatically adjusted so results may include
970 artifacts.
972 :param progress:
973 If given a string a progress bar is shown to the user. The string
974 is used as the label for the progress bar.
976 :param responsive:
977 If set to ``True``, occasionally allow UI events to be processed.
978 If used in combination with ``progress``, this allows the iterator
979 to be aborted by the user.
980 '''
982 try:
983 viewer = self.get_viewer()
984 markers = [
985 m for m in viewer.selected_markers()
986 if not isinstance(m, EventMarker)]
988 if marker_selector is not None:
989 markers = [
990 marker for marker in markers if marker_selector(marker)]
992 pile = self.get_pile()
994 def rtrue(tr):
995 return True
997 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
998 trace_selector_viewer = self.get_viewer_trace_selector(mode)
1000 style_arg = kwargs.pop('style', None)
1002 if main_bandpass:
1003 def apply_filters(traces):
1004 for tr in traces:
1005 if viewer.highpass is not None:
1006 tr.highpass(4, viewer.highpass)
1007 if viewer.lowpass is not None:
1008 tr.lowpass(4, viewer.lowpass)
1009 return traces
1010 else:
1011 def apply_filters(traces):
1012 return traces
1014 pb = viewer.parent().get_progressbars()
1016 time_last = [time.time()]
1018 def update_progress(label, batch):
1019 time_now = time.time()
1020 if responsive:
1021 # start processing events with one second delay, so that
1022 # e.g. cleanup actions at startup do not cause track number
1023 # changes etc.
1024 if time_last[0] + 1. < time_now:
1025 qw.qApp.processEvents()
1026 else:
1027 # redraw about once a second
1028 if time_last[0] + 1. < time_now:
1029 viewer.repaint()
1031 time_last[0] = time.time() # use time after drawing
1033 abort = pb.set_status(
1034 label, batch.i*100./batch.n, responsive)
1035 abort |= viewer.window().is_closing()
1037 return abort
1039 if markers:
1040 for imarker, marker in enumerate(markers):
1041 try:
1042 if progress:
1043 label = '%s: %i/%i' % (
1044 progress, imarker+1, len(markers))
1046 pb.set_status(label, 0, responsive)
1048 if not marker.nslc_ids:
1049 trace_selector_marker = rtrue
1050 else:
1051 def trace_selector_marker(tr):
1052 return marker.match_nslc(tr.nslc_id)
1054 def trace_selector(tr):
1055 return trace_selector_arg(tr) \
1056 and trace_selector_viewer(tr) \
1057 and trace_selector_marker(tr)
1059 for batch in pile.chopper(
1060 tmin=marker.tmin,
1061 tmax=marker.tmax,
1062 trace_selector=trace_selector,
1063 style='batch',
1064 *args,
1065 **kwargs):
1067 if progress:
1068 abort = update_progress(label, batch)
1069 if abort:
1070 return
1072 batch.traces = apply_filters(batch.traces)
1073 if style_arg == 'batch':
1074 yield batch
1075 else:
1076 yield batch.traces
1078 finally:
1079 if progress:
1080 pb.set_status(label, 100., responsive)
1082 elif fallback:
1083 def trace_selector(tr):
1084 return trace_selector_arg(tr) \
1085 and trace_selector_viewer(tr)
1087 tmin, tmax = viewer.get_time_range()
1089 if not pile.is_empty():
1090 ptmin = pile.get_tmin()
1091 tpad = kwargs.get('tpad', 0.0)
1092 if ptmin > tmin:
1093 tmin = ptmin + tpad
1094 ptmax = pile.get_tmax()
1095 if ptmax < tmax:
1096 tmax = ptmax - tpad
1098 try:
1099 if progress:
1100 label = progress
1101 pb.set_status(label, 0, responsive)
1103 for batch in pile.chopper(
1104 tmin=tmin,
1105 tmax=tmax,
1106 trace_selector=trace_selector,
1107 style='batch',
1108 *args,
1109 **kwargs):
1111 if progress:
1112 abort = update_progress(label, batch)
1114 if abort:
1115 return
1117 batch.traces = apply_filters(batch.traces)
1119 if style_arg == 'batch':
1120 yield batch
1121 else:
1122 yield batch.traces
1124 finally:
1125 if progress:
1126 pb.set_status(label, 100., responsive)
1128 else:
1129 raise NoTracesSelected()
1131 except NoViewerSet:
1132 pile = self.get_pile()
1133 return pile.chopper(*args, **kwargs)
1135 def get_selected_time_range(self, fallback=False):
1136 '''
1137 Get the time range spanning all selected markers.
1139 :param fallback: if ``True`` and no marker is selected return begin and
1140 end of visible time range
1141 '''
1143 viewer = self.get_viewer()
1144 markers = viewer.selected_markers()
1145 mins = [marker.tmin for marker in markers]
1146 maxs = [marker.tmax for marker in markers]
1148 if mins and maxs:
1149 tmin = min(mins)
1150 tmax = max(maxs)
1152 elif fallback:
1153 tmin, tmax = viewer.get_time_range()
1155 else:
1156 raise NoTracesSelected()
1158 return tmin, tmax
1160 def panel_visibility_changed(self, bool):
1161 '''
1162 Called when the snuffling's panel becomes visible or is hidden.
1164 Can be overloaded in subclass, e.g. to perform additional setup actions
1165 when the panel is activated the first time.
1166 '''
1168 pass
1170 def make_pile(self):
1171 '''
1172 Create a pile.
1174 To be overloaded in subclass. The default implementation just calls
1175 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1176 arguments.
1177 '''
1179 cachedirname = config.config().cache_dir
1180 sources = self._cli_params.get('sources', sys.argv[1:])
1181 return pile.make_pile(
1182 sources,
1183 cachedirname=cachedirname,
1184 regex=self._cli_params['regex'],
1185 fileformat=self._cli_params['format'])
1187 def make_panel(self, parent):
1188 '''
1189 Create a widget for the snuffling's control panel.
1191 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1192 if no panel is needed (e.g. if the snuffling has no adjustable
1193 parameters).
1194 '''
1196 params = self.get_parameters()
1197 self._param_controls = {}
1198 if params or self._force_panel:
1199 sarea = MyScrollArea(parent.get_panel_parent_widget())
1200 sarea.setFrameStyle(qw.QFrame.NoFrame)
1201 sarea.setSizePolicy(qw.QSizePolicy(
1202 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1203 frame = MyFrame(sarea)
1204 frame.widgetVisibilityChanged.connect(
1205 self.panel_visibility_changed)
1207 frame.setSizePolicy(qw.QSizePolicy(
1208 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1209 frame.setFrameStyle(qw.QFrame.NoFrame)
1210 sarea.setWidget(frame)
1211 sarea.setWidgetResizable(True)
1212 layout = qw.QGridLayout()
1213 layout.setContentsMargins(0, 0, 0, 0)
1214 layout.setSpacing(0)
1215 frame.setLayout(layout)
1217 parlayout = qw.QGridLayout()
1219 irow = 0
1220 ipar = 0
1221 have_switches = False
1222 have_params = False
1223 for iparam, param in enumerate(params):
1224 if isinstance(param, Param):
1225 if param.minimum <= 0.0:
1226 param_control = LinValControl(
1227 high_is_none=param.high_is_none,
1228 low_is_none=param.low_is_none,
1229 type=param.type)
1230 else:
1231 param_control = ValControl(
1232 high_is_none=param.high_is_none,
1233 low_is_none=param.low_is_none,
1234 low_is_zero=param.low_is_zero,
1235 type=param.type)
1237 param_control.setup(
1238 param.name,
1239 param.minimum,
1240 param.maximum,
1241 param.default,
1242 iparam)
1244 param_control.set_tracking(param.tracking)
1245 param_control.valchange.connect(
1246 self.modified_snuffling_panel)
1248 self._param_controls[param.ident] = param_control
1249 for iw, w in enumerate(param_control.widgets()):
1250 parlayout.addWidget(w, ipar, iw)
1252 ipar += 1
1253 have_params = True
1255 elif isinstance(param, Choice):
1256 param_widget = ChoiceControl(
1257 param.ident, param.default, param.choices, param.name)
1258 param_widget.choosen.connect(
1259 self.choose_on_snuffling_panel)
1261 self._param_controls[param.ident] = param_widget
1262 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1263 ipar += 1
1264 have_params = True
1266 elif isinstance(param, Switch):
1267 have_switches = True
1269 if have_params:
1270 parframe = qw.QFrame(sarea)
1271 parframe.setSizePolicy(qw.QSizePolicy(
1272 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1273 parframe.setLayout(parlayout)
1274 layout.addWidget(parframe, irow, 0)
1275 irow += 1
1277 if have_switches:
1278 swlayout = qw.QGridLayout()
1279 isw = 0
1280 for iparam, param in enumerate(params):
1281 if isinstance(param, Switch):
1282 param_widget = SwitchControl(
1283 param.ident, param.default, param.name)
1284 param_widget.sw_toggled.connect(
1285 self.switch_on_snuffling_panel)
1287 self._param_controls[param.ident] = param_widget
1288 swlayout.addWidget(param_widget, isw//10, isw % 10)
1289 isw += 1
1291 swframe = qw.QFrame(sarea)
1292 swframe.setSizePolicy(qw.QSizePolicy(
1293 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1294 swframe.setLayout(swlayout)
1295 layout.addWidget(swframe, irow, 0)
1296 irow += 1
1298 butframe = qw.QFrame(sarea)
1299 butframe.setSizePolicy(qw.QSizePolicy(
1300 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1301 butlayout = qw.QHBoxLayout()
1302 butframe.setLayout(butlayout)
1304 live_update_checkbox = qw.QCheckBox('Auto-Run')
1305 if self._live_update:
1306 live_update_checkbox.setCheckState(qc.Qt.Checked)
1308 butlayout.addWidget(live_update_checkbox)
1309 live_update_checkbox.toggled.connect(
1310 self.live_update_toggled)
1312 help_button = qw.QPushButton('Help')
1313 butlayout.addWidget(help_button)
1314 help_button.clicked.connect(
1315 self.help_button_triggered)
1317 clear_button = qw.QPushButton('Clear')
1318 butlayout.addWidget(clear_button)
1319 clear_button.clicked.connect(
1320 self.clear_button_triggered)
1322 call_button = qw.QPushButton('Run')
1323 butlayout.addWidget(call_button)
1324 call_button.clicked.connect(
1325 self.call_button_triggered)
1327 for name, method in self._triggers:
1328 but = qw.QPushButton(name)
1330 def call_and_update(method):
1331 def f():
1332 self.check_call(method)
1333 self.get_viewer().update()
1334 return f
1336 but.clicked.connect(
1337 call_and_update(method))
1339 butlayout.addWidget(but)
1341 layout.addWidget(butframe, irow, 0)
1343 irow += 1
1344 spacer = qw.QSpacerItem(
1345 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1347 layout.addItem(spacer, irow, 0)
1349 return sarea
1351 else:
1352 return None
1354 def make_helpmenuitem(self, parent):
1355 '''
1356 Create the help menu item for the snuffling.
1357 '''
1359 item = qw.QAction(self.get_name(), None)
1361 item.triggered.connect(
1362 self.help_button_triggered)
1364 return item
1366 def make_menuitem(self, parent):
1367 '''
1368 Create the menu item for the snuffling.
1370 This method may be overloaded in subclass and return ``None``, if no
1371 menu entry is wanted.
1372 '''
1374 item = qw.QAction(self.get_name(), None)
1375 item.setCheckable(
1376 self._have_pre_process_hook or self._have_post_process_hook)
1378 item.triggered.connect(
1379 self.menuitem_triggered)
1381 return item
1383 def output_filename(
1384 self,
1385 caption='Save File',
1386 dir='',
1387 filter='',
1388 selected_filter=None):
1390 '''
1391 Query user for an output filename.
1393 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1394 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1395 dialog.
1396 '''
1398 if not dir and self._previous_output_filename:
1399 dir = self._previous_output_filename
1401 fn = getSaveFileName(
1402 self.get_viewer(), caption, dir, filter, selected_filter)
1403 if not fn:
1404 raise UserCancelled()
1406 self._previous_output_filename = fn
1407 return str(fn)
1409 def input_directory(self, caption='Open Directory', dir=''):
1410 '''
1411 Query user for an input directory.
1413 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1414 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1415 dialog.
1416 '''
1418 if not dir and self._previous_input_directory:
1419 dir = self._previous_input_directory
1421 dn = qw.QFileDialog.getExistingDirectory(
1422 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1424 if not dn:
1425 raise UserCancelled()
1427 self._previous_input_directory = dn
1428 return str(dn)
1430 def input_filename(self, caption='Open File', dir='', filter='',
1431 selected_filter=None):
1432 '''
1433 Query user for an input filename.
1435 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1436 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1437 dialog.
1438 '''
1440 if not dir and self._previous_input_filename:
1441 dir = self._previous_input_filename
1443 fn, _ = qw.QFileDialog.getOpenFileName(
1444 self.get_viewer(),
1445 caption,
1446 dir,
1447 filter)
1449 if not fn:
1450 raise UserCancelled()
1452 self._previous_input_filename = fn
1453 return str(fn)
1455 def input_dialog(self, caption='', request='', directory=False):
1456 '''
1457 Query user for a text input.
1459 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1460 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1461 dialog.
1462 '''
1464 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1466 if not ok:
1467 raise UserCancelled()
1469 return inp
1471 def modified_snuffling_panel(self, value, iparam):
1472 '''
1473 Called when the user has played with an adjustable parameter.
1475 The default implementation sets the parameter, calls the snuffling's
1476 :py:meth:`call` method and finally triggers an update on the viewer
1477 widget.
1478 '''
1480 param = self.get_parameters()[iparam]
1481 self._set_parameter_value(param.ident, value)
1482 if self._live_update:
1483 self.check_call(self.call)
1484 self.get_viewer().update()
1486 def switch_on_snuffling_panel(self, ident, state):
1487 '''
1488 Called when the user has toggled a switchable parameter.
1489 '''
1491 self._set_parameter_value(ident, state)
1492 if self._live_update:
1493 self.check_call(self.call)
1494 self.get_viewer().update()
1496 def choose_on_snuffling_panel(self, ident, state):
1497 '''
1498 Called when the user has made a choice about a choosable parameter.
1499 '''
1501 self._set_parameter_value(ident, state)
1502 if self._live_update:
1503 self.check_call(self.call)
1504 self.get_viewer().update()
1506 def menuitem_triggered(self, arg):
1507 '''
1508 Called when the user has triggered the snuffling's menu.
1510 The default implementation calls the snuffling's :py:meth:`call` method
1511 and triggers an update on the viewer widget.
1512 '''
1514 self.check_call(self.call)
1516 if self._have_pre_process_hook:
1517 self._pre_process_hook_enabled = arg
1519 if self._have_post_process_hook:
1520 self._post_process_hook_enabled = arg
1522 if self._have_pre_process_hook or self._have_post_process_hook:
1523 self.get_viewer().clean_update()
1524 else:
1525 self.get_viewer().update()
1527 def call_button_triggered(self):
1528 '''
1529 Called when the user has clicked the snuffling's call button.
1531 The default implementation calls the snuffling's :py:meth:`call` method
1532 and triggers an update on the viewer widget.
1533 '''
1535 self.check_call(self.call)
1536 self.get_viewer().update()
1538 def clear_button_triggered(self):
1539 '''
1540 Called when the user has clicked the snuffling's clear button.
1542 This calls the :py:meth:`cleanup` method and triggers an update on the
1543 viewer widget.
1544 '''
1546 self.cleanup()
1547 self.get_viewer().update()
1549 def help_button_triggered(self):
1550 '''
1551 Creates a :py:class:`QLabel` which contains the documentation as
1552 given in the snufflings' __doc__ string.
1553 '''
1555 if self.__doc__:
1556 if self.__doc__.strip().startswith('<html>'):
1557 doc = qw.QLabel(self.__doc__)
1558 else:
1559 try:
1560 import markdown
1561 doc = qw.QLabel(markdown.markdown(self.__doc__))
1563 except ImportError:
1564 logger.error(
1565 'Install Python module "markdown" for pretty help '
1566 'formatting.')
1568 doc = qw.QLabel(self.__doc__)
1569 else:
1570 doc = qw.QLabel('This snuffling does not provide any online help.')
1572 labels = [doc]
1574 if self._filename:
1575 from html import escape
1577 code = open(self._filename, 'r').read()
1579 doc_src = qw.QLabel(
1580 '''<html><body>
1581<hr />
1582<center><em>May the source be with you, young Skywalker!</em><br /><br />
1583<a href="file://%s"><code>%s</code></a></center>
1584<br />
1585<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1586<pre style="white-space: pre-wrap"><code>%s
1587</code></pre></p></body></html>'''
1588 % (
1589 quote(self._filename),
1590 escape(self._filename),
1591 escape(code)))
1593 labels.append(doc_src)
1595 for h in labels:
1596 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1597 h.setWordWrap(True)
1599 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1601 def live_update_toggled(self, on):
1602 '''
1603 Called when the checkbox for live-updates has been toggled.
1604 '''
1606 self.set_live_update(on)
1608 def add_traces(self, traces):
1609 '''
1610 Add traces to the viewer.
1612 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1614 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1615 added to the viewer's internal pile for display. Note, that unlike with
1616 the traces from the files given on the command line, these traces are
1617 kept in memory and so may quickly occupy a lot of ram if a lot of
1618 traces are added.
1620 This method should be preferred over modifying the viewer's internal
1621 pile directly, because this way, the snuffling has a chance to
1622 automatically remove its private traces again (see :py:meth:`cleanup`
1623 method).
1624 '''
1626 ticket = self.get_viewer().add_traces(traces)
1627 self._tickets.append(ticket)
1628 return ticket
1630 def add_trace(self, tr):
1631 '''
1632 Add a trace to the viewer.
1634 See :py:meth:`add_traces`.
1635 '''
1637 self.add_traces([tr])
1639 def add_markers(self, markers):
1640 '''
1641 Add some markers to the display.
1643 Takes a list of objects of type
1644 :py:class:`pyrocko.gui.snuffler.marker.Marker` and adds these to the
1645 viewer.
1646 '''
1648 self.get_viewer().add_markers(markers)
1649 self._markers.extend(markers)
1651 def add_marker(self, marker):
1652 '''
1653 Add a marker to the display.
1655 See :py:meth:`add_markers`.
1656 '''
1658 self.add_markers([marker])
1660 def cleanup(self):
1661 '''
1662 Remove all traces and markers which have been added so far by the
1663 snuffling.
1664 '''
1666 try:
1667 viewer = self.get_viewer()
1668 viewer.release_data(self._tickets)
1669 viewer.remove_markers(self._markers)
1671 except NoViewerSet:
1672 pass
1674 self._tickets = []
1675 self._markers = []
1677 def check_call(self, method):
1679 if method in self._call_in_progress:
1680 self.show_message('error', 'Previous action still in progress.')
1681 return
1683 try:
1684 self._call_in_progress[method] = True
1685 method()
1686 return 0
1688 except SnufflingError as e:
1689 if not isinstance(e, SnufflingCallFailed):
1690 # those have logged within error()
1691 logger.error('%s: %s' % (self._name, e))
1692 logger.error('%s: Snuffling action failed' % self._name)
1693 return 1
1695 except Exception as e:
1696 message = '%s: Snuffling action raised an exception: %s' % (
1697 self._name, str(e))
1699 logger.exception(message)
1700 self.show_message('error', message)
1702 finally:
1703 del self._call_in_progress[method]
1705 def call(self):
1706 '''
1707 Main work routine of the snuffling.
1709 This method is called when the snuffling's menu item has been triggered
1710 or when the user has played with the panel controls. To be overloaded
1711 in subclass. The default implementation does nothing useful.
1712 '''
1714 pass
1716 def pre_process_hook(self, traces):
1717 return traces
1719 def post_process_hook(self, traces):
1720 return traces
1722 def get_tpad(self):
1723 '''
1724 Return current amount of extra padding needed by live processing hooks.
1725 '''
1727 return 0.0
1729 def pre_destroy(self):
1730 '''
1731 Called when the snuffling instance is about to be deleted.
1733 Can be overloaded to do user-defined cleanup actions. The
1734 default implementation calls :py:meth:`cleanup` and deletes
1735 the snuffling`s tempory directory, if needed.
1736 '''
1738 self.cleanup()
1739 if self._tempdir is not None:
1740 import shutil
1741 shutil.rmtree(self._tempdir)
1744class SnufflingError(Exception):
1745 pass
1748class NoViewerSet(SnufflingError):
1749 '''
1750 This exception is raised, when no viewer has been set on a Snuffling.
1751 '''
1753 def __str__(self):
1754 return 'No GUI available. ' \
1755 'Maybe this Snuffling cannot be run in command line mode?'
1758class MissingStationInformation(SnufflingError):
1759 '''
1760 Raised when station information is missing.
1761 '''
1764class NoTracesSelected(SnufflingError):
1765 '''
1766 This exception is raised, when no traces have been selected in the viewer
1767 and we cannot fallback to using the current view.
1768 '''
1770 def __str__(self):
1771 return 'No traces have been selected / are available.'
1774class UserCancelled(SnufflingError):
1775 '''
1776 This exception is raised, when the user has cancelled a snuffling dialog.
1777 '''
1779 def __str__(self):
1780 return 'The user has cancelled a dialog.'
1783class SnufflingCallFailed(SnufflingError):
1784 '''
1785 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1786 :py:meth:`Snuffling.call`.
1787 '''
1790class InvalidSnufflingFilename(Exception):
1791 pass
1794class SnufflingModule(object):
1795 '''
1796 Utility class to load/reload snufflings from a file.
1798 The snufflings are created by user modules which have the special function
1799 :py:func:`__snufflings__` which return the snuffling instances to be
1800 exported. The snuffling module is attached to a handler class, which makes
1801 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1802 from ``pile_viewer.py``). The handler class must implement the methods
1803 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1804 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1805 :py:meth:`remove_snufflings` which may be called from the handler class,
1806 when needed.
1807 '''
1809 mtimes = {}
1811 def __init__(self, path, name, handler):
1812 self._path = path
1813 self._name = name
1814 self._mtime = None
1815 self._module = None
1816 self._snufflings = []
1817 self._handler = handler
1819 def load_if_needed(self):
1820 filename = os.path.join(self._path, self._name+'.py')
1822 try:
1823 mtime = os.stat(filename)[8]
1824 except OSError as e:
1825 if e.errno == 2:
1826 logger.error(e)
1827 raise BrokenSnufflingModule(filename)
1829 if self._module is None:
1830 sys.path[0:0] = [self._path]
1831 try:
1832 logger.debug('Loading snuffling module %s' % filename)
1833 if self._name in sys.modules:
1834 raise InvalidSnufflingFilename(self._name)
1836 self._module = __import__(self._name)
1837 del sys.modules[self._name]
1839 for snuffling in self._module.__snufflings__():
1840 snuffling._filename = filename
1841 self.add_snuffling(snuffling)
1843 except Exception:
1844 logger.error(traceback.format_exc())
1845 raise BrokenSnufflingModule(filename)
1847 finally:
1848 sys.path[0:1] = []
1850 elif self._mtime != mtime:
1851 logger.warning('Reloading snuffling module %s' % filename)
1852 settings = self.remove_snufflings()
1853 sys.path[0:0] = [self._path]
1854 try:
1856 sys.modules[self._name] = self._module
1858 reload(self._module)
1859 del sys.modules[self._name]
1861 for snuffling in self._module.__snufflings__():
1862 snuffling._filename = filename
1863 self.add_snuffling(snuffling, reloaded=True)
1865 if len(self._snufflings) == len(settings):
1866 for sett, snuf in zip(settings, self._snufflings):
1867 snuf.set_settings(sett)
1869 except Exception:
1870 logger.error(traceback.format_exc())
1871 raise BrokenSnufflingModule(filename)
1873 finally:
1874 sys.path[0:1] = []
1876 self._mtime = mtime
1878 def add_snuffling(self, snuffling, reloaded=False):
1879 snuffling._path = self._path
1880 snuffling.setup()
1881 self._snufflings.append(snuffling)
1882 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1884 def remove_snufflings(self):
1885 settings = []
1886 for snuffling in self._snufflings:
1887 settings.append(snuffling.get_settings())
1888 self._handler.remove_snuffling(snuffling)
1890 self._snufflings = []
1891 return settings
1894class BrokenSnufflingModule(Exception):
1895 pass
1898class MyScrollArea(qw.QScrollArea):
1900 def sizeHint(self):
1901 s = qc.QSize()
1902 s.setWidth(self.widget().sizeHint().width())
1903 s.setHeight(self.widget().sizeHint().height())
1904 return s
1907class SwitchControl(qw.QCheckBox):
1908 sw_toggled = qc.pyqtSignal(object, bool)
1910 def __init__(self, ident, default, *args):
1911 qw.QCheckBox.__init__(self, *args)
1912 self.ident = ident
1913 self.setChecked(default)
1914 self.toggled.connect(self._sw_toggled)
1916 def _sw_toggled(self, state):
1917 self.sw_toggled.emit(self.ident, state)
1919 def set_value(self, state):
1920 self.blockSignals(True)
1921 self.setChecked(state)
1922 self.blockSignals(False)
1925class ChoiceControl(qw.QFrame):
1926 choosen = qc.pyqtSignal(object, object)
1928 def __init__(self, ident, default, choices, name, *args):
1929 qw.QFrame.__init__(self, *args)
1930 self.label = qw.QLabel(name, self)
1931 self.label.setMinimumWidth(120)
1932 self.cbox = qw.QComboBox(self)
1933 self.layout = qw.QHBoxLayout(self)
1934 self.layout.addWidget(self.label)
1935 self.layout.addWidget(self.cbox)
1936 self.layout.setContentsMargins(0, 0, 0, 0)
1937 self.layout.setSpacing(0)
1938 self.ident = ident
1939 self.choices = choices
1940 for ichoice, choice in enumerate(choices):
1941 self.cbox.addItem(choice)
1943 self.set_value(default)
1944 self.cbox.activated.connect(self.emit_choosen)
1946 def set_choices(self, choices):
1947 icur = self.cbox.currentIndex()
1948 if icur != -1:
1949 selected_choice = choices[icur]
1950 else:
1951 selected_choice = None
1953 self.choices = choices
1954 self.cbox.clear()
1955 for ichoice, choice in enumerate(choices):
1956 self.cbox.addItem(qc.QString(choice))
1958 if selected_choice is not None and selected_choice in choices:
1959 self.set_value(selected_choice)
1960 return selected_choice
1961 else:
1962 self.set_value(choices[0])
1963 return choices[0]
1965 def emit_choosen(self, i):
1966 self.choosen.emit(
1967 self.ident,
1968 self.choices[i])
1970 def set_value(self, v):
1971 self.cbox.blockSignals(True)
1972 for i, choice in enumerate(self.choices):
1973 if choice == v:
1974 self.cbox.setCurrentIndex(i)
1975 self.cbox.blockSignals(False)