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
22from pyrocko import pile, config
23from pyrocko.util import quote
25from .util import (ValControl, LinValControl, FigureFrame, SmartplotFrame,
26 WebKitFrame, VTKFrame, PixmapFrame, Marker, EventMarker,
27 PhaseMarker, load_markers, save_markers)
30from importlib import reload
32Marker, load_markers, save_markers # noqa
34logger = logging.getLogger('pyrocko.gui.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 :py:class:`pyrocko.gui.util.Marker` and
1644 adds these to the viewer.
1645 '''
1647 self.get_viewer().add_markers(markers)
1648 self._markers.extend(markers)
1650 def add_marker(self, marker):
1651 '''
1652 Add a marker to the display.
1654 See :py:meth:`add_markers`.
1655 '''
1657 self.add_markers([marker])
1659 def cleanup(self):
1660 '''
1661 Remove all traces and markers which have been added so far by the
1662 snuffling.
1663 '''
1665 try:
1666 viewer = self.get_viewer()
1667 viewer.release_data(self._tickets)
1668 viewer.remove_markers(self._markers)
1670 except NoViewerSet:
1671 pass
1673 self._tickets = []
1674 self._markers = []
1676 def check_call(self, method):
1678 if method in self._call_in_progress:
1679 self.show_message('error', 'Previous action still in progress.')
1680 return
1682 try:
1683 self._call_in_progress[method] = True
1684 method()
1685 return 0
1687 except SnufflingError as e:
1688 if not isinstance(e, SnufflingCallFailed):
1689 # those have logged within error()
1690 logger.error('%s: %s' % (self._name, e))
1691 logger.error('%s: Snuffling action failed' % self._name)
1692 return 1
1694 except Exception as e:
1695 message = '%s: Snuffling action raised an exception: %s' % (
1696 self._name, str(e))
1698 logger.exception(message)
1699 self.show_message('error', message)
1701 finally:
1702 del self._call_in_progress[method]
1704 def call(self):
1705 '''
1706 Main work routine of the snuffling.
1708 This method is called when the snuffling's menu item has been triggered
1709 or when the user has played with the panel controls. To be overloaded
1710 in subclass. The default implementation does nothing useful.
1711 '''
1713 pass
1715 def pre_process_hook(self, traces):
1716 return traces
1718 def post_process_hook(self, traces):
1719 return traces
1721 def get_tpad(self):
1722 '''
1723 Return current amount of extra padding needed by live processing hooks.
1724 '''
1726 return 0.0
1728 def pre_destroy(self):
1729 '''
1730 Called when the snuffling instance is about to be deleted.
1732 Can be overloaded to do user-defined cleanup actions. The
1733 default implementation calls :py:meth:`cleanup` and deletes
1734 the snuffling`s tempory directory, if needed.
1735 '''
1737 self.cleanup()
1738 if self._tempdir is not None:
1739 import shutil
1740 shutil.rmtree(self._tempdir)
1743class SnufflingError(Exception):
1744 pass
1747class NoViewerSet(SnufflingError):
1748 '''
1749 This exception is raised, when no viewer has been set on a Snuffling.
1750 '''
1752 def __str__(self):
1753 return 'No GUI available. ' \
1754 'Maybe this Snuffling cannot be run in command line mode?'
1757class MissingStationInformation(SnufflingError):
1758 '''
1759 Raised when station information is missing.
1760 '''
1763class NoTracesSelected(SnufflingError):
1764 '''
1765 This exception is raised, when no traces have been selected in the viewer
1766 and we cannot fallback to using the current view.
1767 '''
1769 def __str__(self):
1770 return 'No traces have been selected / are available.'
1773class UserCancelled(SnufflingError):
1774 '''
1775 This exception is raised, when the user has cancelled a snuffling dialog.
1776 '''
1778 def __str__(self):
1779 return 'The user has cancelled a dialog.'
1782class SnufflingCallFailed(SnufflingError):
1783 '''
1784 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1785 :py:meth:`Snuffling.call`.
1786 '''
1789class InvalidSnufflingFilename(Exception):
1790 pass
1793class SnufflingModule(object):
1794 '''
1795 Utility class to load/reload snufflings from a file.
1797 The snufflings are created by user modules which have the special function
1798 :py:func:`__snufflings__` which return the snuffling instances to be
1799 exported. The snuffling module is attached to a handler class, which makes
1800 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1801 from ``pile_viewer.py``). The handler class must implement the methods
1802 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1803 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1804 :py:meth:`remove_snufflings` which may be called from the handler class,
1805 when needed.
1806 '''
1808 mtimes = {}
1810 def __init__(self, path, name, handler):
1811 self._path = path
1812 self._name = name
1813 self._mtime = None
1814 self._module = None
1815 self._snufflings = []
1816 self._handler = handler
1818 def load_if_needed(self):
1819 filename = os.path.join(self._path, self._name+'.py')
1821 try:
1822 mtime = os.stat(filename)[8]
1823 except OSError as e:
1824 if e.errno == 2:
1825 logger.error(e)
1826 raise BrokenSnufflingModule(filename)
1828 if self._module is None:
1829 sys.path[0:0] = [self._path]
1830 try:
1831 logger.debug('Loading snuffling module %s' % filename)
1832 if self._name in sys.modules:
1833 raise InvalidSnufflingFilename(self._name)
1835 self._module = __import__(self._name)
1836 del sys.modules[self._name]
1838 for snuffling in self._module.__snufflings__():
1839 snuffling._filename = filename
1840 self.add_snuffling(snuffling)
1842 except Exception:
1843 logger.error(traceback.format_exc())
1844 raise BrokenSnufflingModule(filename)
1846 finally:
1847 sys.path[0:1] = []
1849 elif self._mtime != mtime:
1850 logger.warning('Reloading snuffling module %s' % filename)
1851 settings = self.remove_snufflings()
1852 sys.path[0:0] = [self._path]
1853 try:
1855 sys.modules[self._name] = self._module
1857 reload(self._module)
1858 del sys.modules[self._name]
1860 for snuffling in self._module.__snufflings__():
1861 snuffling._filename = filename
1862 self.add_snuffling(snuffling, reloaded=True)
1864 if len(self._snufflings) == len(settings):
1865 for sett, snuf in zip(settings, self._snufflings):
1866 snuf.set_settings(sett)
1868 except Exception:
1869 logger.error(traceback.format_exc())
1870 raise BrokenSnufflingModule(filename)
1872 finally:
1873 sys.path[0:1] = []
1875 self._mtime = mtime
1877 def add_snuffling(self, snuffling, reloaded=False):
1878 snuffling._path = self._path
1879 snuffling.setup()
1880 self._snufflings.append(snuffling)
1881 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1883 def remove_snufflings(self):
1884 settings = []
1885 for snuffling in self._snufflings:
1886 settings.append(snuffling.get_settings())
1887 self._handler.remove_snuffling(snuffling)
1889 self._snufflings = []
1890 return settings
1893class BrokenSnufflingModule(Exception):
1894 pass
1897class MyScrollArea(qw.QScrollArea):
1899 def sizeHint(self):
1900 s = qc.QSize()
1901 s.setWidth(self.widget().sizeHint().width())
1902 s.setHeight(self.widget().sizeHint().height())
1903 return s
1906class SwitchControl(qw.QCheckBox):
1907 sw_toggled = qc.pyqtSignal(object, bool)
1909 def __init__(self, ident, default, *args):
1910 qw.QCheckBox.__init__(self, *args)
1911 self.ident = ident
1912 self.setChecked(default)
1913 self.toggled.connect(self._sw_toggled)
1915 def _sw_toggled(self, state):
1916 self.sw_toggled.emit(self.ident, state)
1918 def set_value(self, state):
1919 self.blockSignals(True)
1920 self.setChecked(state)
1921 self.blockSignals(False)
1924class ChoiceControl(qw.QFrame):
1925 choosen = qc.pyqtSignal(object, object)
1927 def __init__(self, ident, default, choices, name, *args):
1928 qw.QFrame.__init__(self, *args)
1929 self.label = qw.QLabel(name, self)
1930 self.label.setMinimumWidth(120)
1931 self.cbox = qw.QComboBox(self)
1932 self.layout = qw.QHBoxLayout(self)
1933 self.layout.addWidget(self.label)
1934 self.layout.addWidget(self.cbox)
1935 self.layout.setContentsMargins(0, 0, 0, 0)
1936 self.layout.setSpacing(0)
1937 self.ident = ident
1938 self.choices = choices
1939 for ichoice, choice in enumerate(choices):
1940 self.cbox.addItem(choice)
1942 self.set_value(default)
1943 self.cbox.activated.connect(self.emit_choosen)
1945 def set_choices(self, choices):
1946 icur = self.cbox.currentIndex()
1947 if icur != -1:
1948 selected_choice = choices[icur]
1949 else:
1950 selected_choice = None
1952 self.choices = choices
1953 self.cbox.clear()
1954 for ichoice, choice in enumerate(choices):
1955 self.cbox.addItem(qc.QString(choice))
1957 if selected_choice is not None and selected_choice in choices:
1958 self.set_value(selected_choice)
1959 return selected_choice
1960 else:
1961 self.set_value(choices[0])
1962 return choices[0]
1964 def emit_choosen(self, i):
1965 self.choosen.emit(
1966 self.ident,
1967 self.choices[i])
1969 def set_value(self, v):
1970 self.cbox.blockSignals(True)
1971 for i, choice in enumerate(self.choices):
1972 if choice == v:
1973 self.cbox.setCurrentIndex(i)
1974 self.cbox.blockSignals(False)