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, WebKitFrame,
26 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker,
27 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=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=Figure)
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=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=Figure)
507 def pixmap_frame(self, filename=None, name=None):
508 '''
509 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
511 :param name: labels the tab
512 :param filename: name of file to be displayed
513 '''
515 f = PixmapFrame(filename)
517 scroll_area = qw.QScrollArea()
518 scroll_area.setWidget(f)
519 scroll_area.setWidgetResizable(True)
521 self._panel_parent.add_tab(name or "Pixmap", scroll_area)
522 return f
524 def web_frame(self, url=None, name=None):
525 '''
526 Creates a :py:class:`WebKitFrame` which can be used as a browser
527 within snuffler.
529 :param url: url to open
530 :param name: labels the tab
531 '''
533 if name is None:
534 self._iplot += 1
535 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
537 f = WebKitFrame(url)
538 self._panel_parent.add_tab(name, f)
539 return f
541 def vtk_frame(self, name=None, actors=None):
542 '''
543 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
544 graphics.
546 :param actors: list of VTKActors
547 :param name: labels the tab
549 Initialize the interactive rendering by calling the frames'
550 :py:meth`initialize` method after having added all actors to the frames
551 renderer.
553 Requires installation of vtk including python wrapper.
554 '''
555 if name is None:
556 self._iplot += 1
557 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
559 try:
560 f = VTKFrame(actors=actors)
561 except ImportError as e:
562 self.fail(e)
564 self._panel_parent.add_tab(name, f)
565 return f
567 def tempdir(self):
568 '''
569 Create a temporary directory and return its absolute path.
571 The directory and all its contents are removed when the Snuffling
572 instance is deleted.
573 '''
575 if self._tempdir is None:
576 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
578 return self._tempdir
580 def set_live_update(self, live_update):
581 '''
582 Enable/disable live updating.
584 When live updates are enabled, the :py:meth:`call` method is called
585 whenever the user changes a parameter. If it is disabled, the user has
586 to initiate such a call manually by triggering the snuffling's menu
587 item or pressing the call button.
588 '''
590 self._live_update = live_update
591 if self._have_pre_process_hook:
592 self._pre_process_hook_enabled = live_update
593 if self._have_post_process_hook:
594 self._post_process_hook_enabled = live_update
596 try:
597 self.get_viewer().clean_update()
598 except NoViewerSet:
599 pass
601 def add_parameter(self, param):
602 '''
603 Add an adjustable parameter to the snuffling.
605 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
606 :py:class:`Choice`.
608 For each parameter added, controls are added to the snuffling's panel,
609 so that the parameter can be adjusted from the gui.
610 '''
612 self._parameters.append(param)
613 self._set_parameter_value(param.ident, param.default)
615 if self._panel is not None:
616 self.delete_gui()
617 self.setup_gui()
619 def add_trigger(self, name, method):
620 '''
621 Add a button to the snuffling's panel.
623 :param name: string that labels the button
624 :param method: method associated with the button
625 '''
627 self._triggers.append((name, method))
629 if self._panel is not None:
630 self.delete_gui()
631 self.setup_gui()
633 def get_parameters(self):
634 '''
635 Get the snuffling's adjustable parameter definitions.
637 Returns a list of objects of type :py:class:`Param`.
638 '''
640 return self._parameters
642 def get_parameter(self, ident):
643 '''
644 Get one of the snuffling's adjustable parameter definitions.
646 :param ident: identifier of the parameter
648 Returns an object of type :py:class:`Param` or ``None``.
649 '''
651 for param in self._parameters:
652 if param.ident == ident:
653 return param
654 return None
656 def set_parameter(self, ident, value):
657 '''
658 Set one of the snuffling's adjustable parameters.
660 :param ident: identifier of the parameter
661 :param value: new value of the parameter
663 Adjusts the control of a parameter without calling :py:meth:`call`.
664 '''
666 self._set_parameter_value(ident, value)
668 control = self._param_controls.get(ident, None)
669 if control:
670 control.set_value(value)
672 def set_parameter_range(self, ident, vmin, vmax):
673 '''
674 Set the range of one of the snuffling's adjustable parameters.
676 :param ident: identifier of the parameter
677 :param vmin,vmax: new minimum and maximum value for the parameter
679 Adjusts the control of a parameter without calling :py:meth:`call`.
680 '''
682 control = self._param_controls.get(ident, None)
683 if control:
684 control.set_range(vmin, vmax)
686 def set_parameter_choices(self, ident, choices):
687 '''
688 Update the choices of a Choice parameter.
690 :param ident: identifier of the parameter
691 :param choices: list of strings
692 '''
694 control = self._param_controls.get(ident, None)
695 if control:
696 selected_choice = control.set_choices(choices)
697 self._set_parameter_value(ident, selected_choice)
699 def _set_parameter_value(self, ident, value):
700 setattr(self, ident, value)
702 def get_parameter_value(self, ident):
703 '''
704 Get the current value of a parameter.
706 :param ident: identifier of the parameter
707 '''
708 return getattr(self, ident)
710 def get_settings(self):
711 '''
712 Returns a dictionary with identifiers of all parameters as keys and
713 their values as the dictionaries values.
714 '''
716 params = self.get_parameters()
717 settings = {}
718 for param in params:
719 settings[param.ident] = self.get_parameter_value(param.ident)
721 return settings
723 def set_settings(self, settings):
724 params = self.get_parameters()
725 dparams = dict([(param.ident, param) for param in params])
726 for k, v in settings.items():
727 if k in dparams:
728 self._set_parameter_value(k, v)
729 if k in self._param_controls:
730 control = self._param_controls[k]
731 control.set_value(v)
733 def get_viewer(self):
734 '''
735 Get the parent viewer.
737 Returns a reference to an object of type :py:class:`PileOverview`,
738 which is the main viewer widget.
740 If no gui has been initialized for the snuffling, a
741 :py:exc:`NoViewerSet` exception is raised.
742 '''
744 if self._viewer is None:
745 raise NoViewerSet()
746 return self._viewer
748 def get_pile(self):
749 '''
750 Get the pile.
752 If a gui has been initialized, a reference to the viewer's internal
753 pile is returned. If not, the :py:meth:`make_pile` method (which may be
754 overloaded in subclass) is called to create a pile. This can be
755 utilized to make hybrid snufflings, which may work also in a standalone
756 mode.
757 '''
759 try:
760 p = self.get_viewer().get_pile()
761 except NoViewerSet:
762 if self._no_viewer_pile is None:
763 self._no_viewer_pile = self.make_pile()
765 p = self._no_viewer_pile
767 return p
769 def get_active_event_and_stations(
770 self, trange=(-3600., 3600.), missing='warn'):
772 '''
773 Get event and stations with available data for active event.
775 :param trange: (begin, end), time range around event origin time to
776 query for available data
777 :param missing: string, what to do in case of missing station
778 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
780 :returns: ``(event, stations)``
781 '''
783 p = self.get_pile()
784 v = self.get_viewer()
786 event = v.get_active_event()
787 if event is None:
788 self.fail(
789 'No active event set. Select an event and press "e" to make '
790 'it the "active event"')
792 stations = {}
793 for traces in p.chopper(
794 event.time+trange[0],
795 event.time+trange[1],
796 load_data=False,
797 degap=False):
799 for tr in traces:
800 try:
801 for skey in v.station_keys(tr):
802 if skey in stations:
803 continue
805 station = v.get_station(skey)
806 stations[skey] = station
808 except KeyError:
809 s = 'No station information for station key "%s".' \
810 % '.'.join(skey)
812 if missing == 'warn':
813 logger.warning(s)
814 elif missing == 'raise':
815 raise MissingStationInformation(s)
816 elif missing == 'ignore':
817 pass
818 else:
819 assert False, 'invalid argument to "missing"'
821 stations[skey] = None
823 return event, list(set(
824 st for st in stations.values() if st is not None))
826 def get_stations(self):
827 '''
828 Get all stations known to the viewer.
829 '''
831 v = self.get_viewer()
832 stations = list(v.stations.values())
833 return stations
835 def get_markers(self):
836 '''
837 Get all markers from the viewer.
838 '''
840 return self.get_viewer().get_markers()
842 def get_event_markers(self):
843 '''
844 Get all event markers from the viewer.
845 '''
847 return [m for m in self.get_viewer().get_markers()
848 if isinstance(m, EventMarker)]
850 def get_selected_markers(self):
851 '''
852 Get all selected markers from the viewer.
853 '''
855 return self.get_viewer().selected_markers()
857 def get_selected_event_markers(self):
858 '''
859 Get all selected event markers from the viewer.
860 '''
862 return [m for m in self.get_viewer().selected_markers()
863 if isinstance(m, EventMarker)]
865 def get_active_event_and_phase_markers(self):
866 '''
867 Get the marker of the active event and any associated phase markers
868 '''
870 viewer = self.get_viewer()
871 markers = viewer.get_markers()
872 event_marker = viewer.get_active_event_marker()
873 if event_marker is None:
874 self.fail(
875 'No active event set. '
876 'Select an event and press "e" to make it the "active event"')
878 event = event_marker.get_event()
880 selection = []
881 for m in markers:
882 if isinstance(m, PhaseMarker):
883 if m.get_event() is event:
884 selection.append(m)
886 return (
887 event_marker,
888 [m for m in markers if isinstance(m, PhaseMarker) and
889 m.get_event() == event])
891 def get_viewer_trace_selector(self, mode='inview'):
892 '''
893 Get currently active trace selector from viewer.
895 :param mode: set to ``'inview'`` (default) to only include selections
896 currently shown in the viewer, ``'visible' to include all traces
897 not currenly hidden by hide or quick-select commands, or ``'all'``
898 to disable any restrictions.
899 '''
901 viewer = self.get_viewer()
903 def rtrue(tr):
904 return True
906 if mode == 'inview':
907 return viewer.trace_selector or rtrue
908 elif mode == 'visible':
909 return viewer.trace_filter or rtrue
910 elif mode == 'all':
911 return rtrue
912 else:
913 raise Exception('invalid mode argument')
915 def chopper_selected_traces(self, fallback=False, marker_selector=None,
916 mode='inview', main_bandpass=False,
917 progress=None, responsive=False,
918 *args, **kwargs):
919 '''
920 Iterate over selected traces.
922 Shortcut to get all trace data contained in the selected markers in the
923 running snuffler. For each selected marker,
924 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
925 *tmin*, *tmax*, and *trace_selector* set to values according to the
926 marker. Additional arguments to the chopper are handed over from
927 *\\*args* and *\\*\\*kwargs*.
929 :param fallback:
930 If ``True``, if no selection has been marked, use the content
931 currently visible in the viewer.
933 :param marker_selector:
934 If not ``None`` a callback to filter markers.
936 :param mode:
937 Set to ``'inview'`` (default) to only include selections currently
938 shown in the viewer (excluding traces accessible through vertical
939 scrolling), ``'visible'`` to include all traces not currently
940 hidden by hide or quick-select commands (including traces
941 accessible through vertical scrolling), or ``'all'`` to disable any
942 restrictions.
944 :param main_bandpass:
945 If ``True``, apply main control high- and lowpass filters to
946 traces. Note: use with caution. Processing is fixed to use 4th
947 order Butterworth highpass and lowpass and the signal is always
948 demeaned before filtering. FFT filtering, rotation, demean and
949 bandpass settings from the graphical interface are not respected
950 here. Padding is not automatically adjusted so results may include
951 artifacts.
953 :param progress:
954 If given a string a progress bar is shown to the user. The string
955 is used as the label for the progress bar.
957 :param responsive:
958 If set to ``True``, occasionally allow UI events to be processed.
959 If used in combination with ``progress``, this allows the iterator
960 to be aborted by the user.
961 '''
963 try:
964 viewer = self.get_viewer()
965 markers = [
966 m for m in viewer.selected_markers()
967 if not isinstance(m, EventMarker)]
969 if marker_selector is not None:
970 markers = [
971 marker for marker in markers if marker_selector(marker)]
973 pile = self.get_pile()
975 def rtrue(tr):
976 return True
978 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
979 trace_selector_viewer = self.get_viewer_trace_selector(mode)
981 style_arg = kwargs.pop('style', None)
983 if main_bandpass:
984 def apply_filters(traces):
985 for tr in traces:
986 if viewer.highpass is not None:
987 tr.highpass(4, viewer.highpass)
988 if viewer.lowpass is not None:
989 tr.lowpass(4, viewer.lowpass)
990 return traces
991 else:
992 def apply_filters(traces):
993 return traces
995 pb = viewer.parent().get_progressbars()
997 time_last = [time.time()]
999 def update_progress(label, batch):
1000 time_now = time.time()
1001 if responsive:
1002 # start processing events with one second delay, so that
1003 # e.g. cleanup actions at startup do not cause track number
1004 # changes etc.
1005 if time_last[0] + 1. < time_now:
1006 qw.qApp.processEvents()
1007 else:
1008 # redraw about once a second
1009 if time_last[0] + 1. < time_now:
1010 viewer.repaint()
1012 time_last[0] = time.time() # use time after drawing
1014 abort = pb.set_status(
1015 label, batch.i*100./batch.n, responsive)
1016 abort |= viewer.window().is_closing()
1018 return abort
1020 if markers:
1021 for imarker, marker in enumerate(markers):
1022 try:
1023 if progress:
1024 label = '%s: %i/%i' % (
1025 progress, imarker+1, len(markers))
1027 pb.set_status(label, 0, responsive)
1029 if not marker.nslc_ids:
1030 trace_selector_marker = rtrue
1031 else:
1032 def trace_selector_marker(tr):
1033 return marker.match_nslc(tr.nslc_id)
1035 def trace_selector(tr):
1036 return trace_selector_arg(tr) \
1037 and trace_selector_viewer(tr) \
1038 and trace_selector_marker(tr)
1040 for batch in pile.chopper(
1041 tmin=marker.tmin,
1042 tmax=marker.tmax,
1043 trace_selector=trace_selector,
1044 style='batch',
1045 *args,
1046 **kwargs):
1048 if progress:
1049 abort = update_progress(label, batch)
1050 if abort:
1051 return
1053 batch.traces = apply_filters(batch.traces)
1054 if style_arg == 'batch':
1055 yield batch
1056 else:
1057 yield batch.traces
1059 finally:
1060 if progress:
1061 pb.set_status(label, 100., responsive)
1063 elif fallback:
1064 def trace_selector(tr):
1065 return trace_selector_arg(tr) \
1066 and trace_selector_viewer(tr)
1068 tmin, tmax = viewer.get_time_range()
1070 if not pile.is_empty():
1071 ptmin = pile.get_tmin()
1072 tpad = kwargs.get('tpad', 0.0)
1073 if ptmin > tmin:
1074 tmin = ptmin + tpad
1075 ptmax = pile.get_tmax()
1076 if ptmax < tmax:
1077 tmax = ptmax - tpad
1079 try:
1080 if progress:
1081 label = progress
1082 pb.set_status(label, 0, responsive)
1084 for batch in pile.chopper(
1085 tmin=tmin,
1086 tmax=tmax,
1087 trace_selector=trace_selector,
1088 style='batch',
1089 *args,
1090 **kwargs):
1092 if progress:
1093 abort = update_progress(label, batch)
1095 if abort:
1096 return
1098 batch.traces = apply_filters(batch.traces)
1100 if style_arg == 'batch':
1101 yield batch
1102 else:
1103 yield batch.traces
1105 finally:
1106 if progress:
1107 pb.set_status(label, 100., responsive)
1109 else:
1110 raise NoTracesSelected()
1112 except NoViewerSet:
1113 pile = self.get_pile()
1114 return pile.chopper(*args, **kwargs)
1116 def get_selected_time_range(self, fallback=False):
1117 '''
1118 Get the time range spanning all selected markers.
1120 :param fallback: if ``True`` and no marker is selected return begin and
1121 end of visible time range
1122 '''
1124 viewer = self.get_viewer()
1125 markers = viewer.selected_markers()
1126 mins = [marker.tmin for marker in markers]
1127 maxs = [marker.tmax for marker in markers]
1129 if mins and maxs:
1130 tmin = min(mins)
1131 tmax = max(maxs)
1133 elif fallback:
1134 tmin, tmax = viewer.get_time_range()
1136 else:
1137 raise NoTracesSelected()
1139 return tmin, tmax
1141 def panel_visibility_changed(self, bool):
1142 '''
1143 Called when the snuffling's panel becomes visible or is hidden.
1145 Can be overloaded in subclass, e.g. to perform additional setup actions
1146 when the panel is activated the first time.
1147 '''
1149 pass
1151 def make_pile(self):
1152 '''
1153 Create a pile.
1155 To be overloaded in subclass. The default implementation just calls
1156 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1157 arguments.
1158 '''
1160 cachedirname = config.config().cache_dir
1161 sources = self._cli_params.get('sources', sys.argv[1:])
1162 return pile.make_pile(
1163 sources,
1164 cachedirname=cachedirname,
1165 regex=self._cli_params['regex'],
1166 fileformat=self._cli_params['format'])
1168 def make_panel(self, parent):
1169 '''
1170 Create a widget for the snuffling's control panel.
1172 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1173 if no panel is needed (e.g. if the snuffling has no adjustable
1174 parameters).
1175 '''
1177 params = self.get_parameters()
1178 self._param_controls = {}
1179 if params or self._force_panel:
1180 sarea = MyScrollArea(parent.get_panel_parent_widget())
1181 sarea.setFrameStyle(qw.QFrame.NoFrame)
1182 sarea.setSizePolicy(qw.QSizePolicy(
1183 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1184 frame = MyFrame(sarea)
1185 frame.widgetVisibilityChanged.connect(
1186 self.panel_visibility_changed)
1188 frame.setSizePolicy(qw.QSizePolicy(
1189 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1190 frame.setFrameStyle(qw.QFrame.NoFrame)
1191 sarea.setWidget(frame)
1192 sarea.setWidgetResizable(True)
1193 layout = qw.QGridLayout()
1194 layout.setContentsMargins(0, 0, 0, 0)
1195 layout.setSpacing(0)
1196 frame.setLayout(layout)
1198 parlayout = qw.QGridLayout()
1200 irow = 0
1201 ipar = 0
1202 have_switches = False
1203 have_params = False
1204 for iparam, param in enumerate(params):
1205 if isinstance(param, Param):
1206 if param.minimum <= 0.0:
1207 param_control = LinValControl(
1208 high_is_none=param.high_is_none,
1209 low_is_none=param.low_is_none,
1210 type=param.type)
1211 else:
1212 param_control = ValControl(
1213 high_is_none=param.high_is_none,
1214 low_is_none=param.low_is_none,
1215 low_is_zero=param.low_is_zero,
1216 type=param.type)
1218 param_control.setup(
1219 param.name,
1220 param.minimum,
1221 param.maximum,
1222 param.default,
1223 iparam)
1225 param_control.set_tracking(param.tracking)
1226 param_control.valchange.connect(
1227 self.modified_snuffling_panel)
1229 self._param_controls[param.ident] = param_control
1230 for iw, w in enumerate(param_control.widgets()):
1231 parlayout.addWidget(w, ipar, iw)
1233 ipar += 1
1234 have_params = True
1236 elif isinstance(param, Choice):
1237 param_widget = ChoiceControl(
1238 param.ident, param.default, param.choices, param.name)
1239 param_widget.choosen.connect(
1240 self.choose_on_snuffling_panel)
1242 self._param_controls[param.ident] = param_widget
1243 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1244 ipar += 1
1245 have_params = True
1247 elif isinstance(param, Switch):
1248 have_switches = True
1250 if have_params:
1251 parframe = qw.QFrame(sarea)
1252 parframe.setSizePolicy(qw.QSizePolicy(
1253 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1254 parframe.setLayout(parlayout)
1255 layout.addWidget(parframe, irow, 0)
1256 irow += 1
1258 if have_switches:
1259 swlayout = qw.QGridLayout()
1260 isw = 0
1261 for iparam, param in enumerate(params):
1262 if isinstance(param, Switch):
1263 param_widget = SwitchControl(
1264 param.ident, param.default, param.name)
1265 param_widget.sw_toggled.connect(
1266 self.switch_on_snuffling_panel)
1268 self._param_controls[param.ident] = param_widget
1269 swlayout.addWidget(param_widget, isw//10, isw % 10)
1270 isw += 1
1272 swframe = qw.QFrame(sarea)
1273 swframe.setSizePolicy(qw.QSizePolicy(
1274 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1275 swframe.setLayout(swlayout)
1276 layout.addWidget(swframe, irow, 0)
1277 irow += 1
1279 butframe = qw.QFrame(sarea)
1280 butframe.setSizePolicy(qw.QSizePolicy(
1281 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1282 butlayout = qw.QHBoxLayout()
1283 butframe.setLayout(butlayout)
1285 live_update_checkbox = qw.QCheckBox('Auto-Run')
1286 if self._live_update:
1287 live_update_checkbox.setCheckState(qc.Qt.Checked)
1289 butlayout.addWidget(live_update_checkbox)
1290 live_update_checkbox.toggled.connect(
1291 self.live_update_toggled)
1293 help_button = qw.QPushButton('Help')
1294 butlayout.addWidget(help_button)
1295 help_button.clicked.connect(
1296 self.help_button_triggered)
1298 clear_button = qw.QPushButton('Clear')
1299 butlayout.addWidget(clear_button)
1300 clear_button.clicked.connect(
1301 self.clear_button_triggered)
1303 call_button = qw.QPushButton('Run')
1304 butlayout.addWidget(call_button)
1305 call_button.clicked.connect(
1306 self.call_button_triggered)
1308 for name, method in self._triggers:
1309 but = qw.QPushButton(name)
1311 def call_and_update(method):
1312 def f():
1313 self.check_call(method)
1314 self.get_viewer().update()
1315 return f
1317 but.clicked.connect(
1318 call_and_update(method))
1320 butlayout.addWidget(but)
1322 layout.addWidget(butframe, irow, 0)
1324 irow += 1
1325 spacer = qw.QSpacerItem(
1326 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1328 layout.addItem(spacer, irow, 0)
1330 return sarea
1332 else:
1333 return None
1335 def make_helpmenuitem(self, parent):
1336 '''
1337 Create the help menu item for the snuffling.
1338 '''
1340 item = qw.QAction(self.get_name(), None)
1342 item.triggered.connect(
1343 self.help_button_triggered)
1345 return item
1347 def make_menuitem(self, parent):
1348 '''
1349 Create the menu item for the snuffling.
1351 This method may be overloaded in subclass and return ``None``, if no
1352 menu entry is wanted.
1353 '''
1355 item = qw.QAction(self.get_name(), None)
1356 item.setCheckable(
1357 self._have_pre_process_hook or self._have_post_process_hook)
1359 item.triggered.connect(
1360 self.menuitem_triggered)
1362 return item
1364 def output_filename(
1365 self,
1366 caption='Save File',
1367 dir='',
1368 filter='',
1369 selected_filter=None):
1371 '''
1372 Query user for an output filename.
1374 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1375 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1376 dialog.
1377 '''
1379 if not dir and self._previous_output_filename:
1380 dir = self._previous_output_filename
1382 fn = getSaveFileName(
1383 self.get_viewer(), caption, dir, filter, selected_filter)
1384 if not fn:
1385 raise UserCancelled()
1387 self._previous_output_filename = fn
1388 return str(fn)
1390 def input_directory(self, caption='Open Directory', dir=''):
1391 '''
1392 Query user for an input directory.
1394 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1395 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1396 dialog.
1397 '''
1399 if not dir and self._previous_input_directory:
1400 dir = self._previous_input_directory
1402 dn = qw.QFileDialog.getExistingDirectory(
1403 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1405 if not dn:
1406 raise UserCancelled()
1408 self._previous_input_directory = dn
1409 return str(dn)
1411 def input_filename(self, caption='Open File', dir='', filter='',
1412 selected_filter=None):
1413 '''
1414 Query user for an input filename.
1416 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1417 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1418 dialog.
1419 '''
1421 if not dir and self._previous_input_filename:
1422 dir = self._previous_input_filename
1424 fn, _ = qw.QFileDialog.getOpenFileName(
1425 self.get_viewer(),
1426 caption,
1427 dir,
1428 filter)
1430 if not fn:
1431 raise UserCancelled()
1433 self._previous_input_filename = fn
1434 return str(fn)
1436 def input_dialog(self, caption='', request='', directory=False):
1437 '''
1438 Query user for a text input.
1440 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1441 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1442 dialog.
1443 '''
1445 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1447 if not ok:
1448 raise UserCancelled()
1450 return inp
1452 def modified_snuffling_panel(self, value, iparam):
1453 '''
1454 Called when the user has played with an adjustable parameter.
1456 The default implementation sets the parameter, calls the snuffling's
1457 :py:meth:`call` method and finally triggers an update on the viewer
1458 widget.
1459 '''
1461 param = self.get_parameters()[iparam]
1462 self._set_parameter_value(param.ident, value)
1463 if self._live_update:
1464 self.check_call(self.call)
1465 self.get_viewer().update()
1467 def switch_on_snuffling_panel(self, ident, state):
1468 '''
1469 Called when the user has toggled a switchable parameter.
1470 '''
1472 self._set_parameter_value(ident, state)
1473 if self._live_update:
1474 self.check_call(self.call)
1475 self.get_viewer().update()
1477 def choose_on_snuffling_panel(self, ident, state):
1478 '''
1479 Called when the user has made a choice about a choosable parameter.
1480 '''
1482 self._set_parameter_value(ident, state)
1483 if self._live_update:
1484 self.check_call(self.call)
1485 self.get_viewer().update()
1487 def menuitem_triggered(self, arg):
1488 '''
1489 Called when the user has triggered the snuffling's menu.
1491 The default implementation calls the snuffling's :py:meth:`call` method
1492 and triggers an update on the viewer widget.
1493 '''
1495 self.check_call(self.call)
1497 if self._have_pre_process_hook:
1498 self._pre_process_hook_enabled = arg
1500 if self._have_post_process_hook:
1501 self._post_process_hook_enabled = arg
1503 if self._have_pre_process_hook or self._have_post_process_hook:
1504 self.get_viewer().clean_update()
1505 else:
1506 self.get_viewer().update()
1508 def call_button_triggered(self):
1509 '''
1510 Called when the user has clicked the snuffling's call button.
1512 The default implementation calls the snuffling's :py:meth:`call` method
1513 and triggers an update on the viewer widget.
1514 '''
1516 self.check_call(self.call)
1517 self.get_viewer().update()
1519 def clear_button_triggered(self):
1520 '''
1521 Called when the user has clicked the snuffling's clear button.
1523 This calls the :py:meth:`cleanup` method and triggers an update on the
1524 viewer widget.
1525 '''
1527 self.cleanup()
1528 self.get_viewer().update()
1530 def help_button_triggered(self):
1531 '''
1532 Creates a :py:class:`QLabel` which contains the documentation as
1533 given in the snufflings' __doc__ string.
1534 '''
1536 if self.__doc__:
1537 if self.__doc__.strip().startswith('<html>'):
1538 doc = qw.QLabel(self.__doc__)
1539 else:
1540 try:
1541 import markdown
1542 doc = qw.QLabel(markdown.markdown(self.__doc__))
1544 except ImportError:
1545 logger.error(
1546 'Install Python module "markdown" for pretty help '
1547 'formatting.')
1549 doc = qw.QLabel(self.__doc__)
1550 else:
1551 doc = qw.QLabel('This snuffling does not provide any online help.')
1553 labels = [doc]
1555 if self._filename:
1556 from html import escape
1558 code = open(self._filename, 'r').read()
1560 doc_src = qw.QLabel(
1561 '''<html><body>
1562<hr />
1563<center><em>May the source be with you, young Skywalker!</em><br /><br />
1564<a href="file://%s"><code>%s</code></a></center>
1565<br />
1566<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1567<pre style="white-space: pre-wrap"><code>%s
1568</code></pre></p></body></html>'''
1569 % (
1570 quote(self._filename),
1571 escape(self._filename),
1572 escape(code)))
1574 labels.append(doc_src)
1576 for h in labels:
1577 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1578 h.setWordWrap(True)
1580 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1582 def live_update_toggled(self, on):
1583 '''
1584 Called when the checkbox for live-updates has been toggled.
1585 '''
1587 self.set_live_update(on)
1589 def add_traces(self, traces):
1590 '''
1591 Add traces to the viewer.
1593 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1595 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1596 added to the viewer's internal pile for display. Note, that unlike with
1597 the traces from the files given on the command line, these traces are
1598 kept in memory and so may quickly occupy a lot of ram if a lot of
1599 traces are added.
1601 This method should be preferred over modifying the viewer's internal
1602 pile directly, because this way, the snuffling has a chance to
1603 automatically remove its private traces again (see :py:meth:`cleanup`
1604 method).
1605 '''
1607 ticket = self.get_viewer().add_traces(traces)
1608 self._tickets.append(ticket)
1609 return ticket
1611 def add_trace(self, tr):
1612 '''
1613 Add a trace to the viewer.
1615 See :py:meth:`add_traces`.
1616 '''
1618 self.add_traces([tr])
1620 def add_markers(self, markers):
1621 '''
1622 Add some markers to the display.
1624 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1625 adds these to the viewer.
1626 '''
1628 self.get_viewer().add_markers(markers)
1629 self._markers.extend(markers)
1631 def add_marker(self, marker):
1632 '''
1633 Add a marker to the display.
1635 See :py:meth:`add_markers`.
1636 '''
1638 self.add_markers([marker])
1640 def cleanup(self):
1641 '''
1642 Remove all traces and markers which have been added so far by the
1643 snuffling.
1644 '''
1646 try:
1647 viewer = self.get_viewer()
1648 viewer.release_data(self._tickets)
1649 viewer.remove_markers(self._markers)
1651 except NoViewerSet:
1652 pass
1654 self._tickets = []
1655 self._markers = []
1657 def check_call(self, method):
1659 if method in self._call_in_progress:
1660 self.show_message('error', 'Previous action still in progress.')
1661 return
1663 try:
1664 self._call_in_progress[method] = True
1665 method()
1666 return 0
1668 except SnufflingError as e:
1669 if not isinstance(e, SnufflingCallFailed):
1670 # those have logged within error()
1671 logger.error('%s: %s' % (self._name, e))
1672 logger.error('%s: Snuffling action failed' % self._name)
1673 return 1
1675 except Exception as e:
1676 message = '%s: Snuffling action raised an exception: %s' % (
1677 self._name, str(e))
1679 logger.exception(message)
1680 self.show_message('error', message)
1682 finally:
1683 del self._call_in_progress[method]
1685 def call(self):
1686 '''
1687 Main work routine of the snuffling.
1689 This method is called when the snuffling's menu item has been triggered
1690 or when the user has played with the panel controls. To be overloaded
1691 in subclass. The default implementation does nothing useful.
1692 '''
1694 pass
1696 def pre_process_hook(self, traces):
1697 return traces
1699 def post_process_hook(self, traces):
1700 return traces
1702 def get_tpad(self):
1703 '''
1704 Return current amount of extra padding needed by live processing hooks.
1705 '''
1707 return 0.0
1709 def pre_destroy(self):
1710 '''
1711 Called when the snuffling instance is about to be deleted.
1713 Can be overloaded to do user-defined cleanup actions. The
1714 default implementation calls :py:meth:`cleanup` and deletes
1715 the snuffling`s tempory directory, if needed.
1716 '''
1718 self.cleanup()
1719 if self._tempdir is not None:
1720 import shutil
1721 shutil.rmtree(self._tempdir)
1724class SnufflingError(Exception):
1725 pass
1728class NoViewerSet(SnufflingError):
1729 '''
1730 This exception is raised, when no viewer has been set on a Snuffling.
1731 '''
1733 def __str__(self):
1734 return 'No GUI available. ' \
1735 'Maybe this Snuffling cannot be run in command line mode?'
1738class MissingStationInformation(SnufflingError):
1739 '''
1740 Raised when station information is missing.
1741 '''
1744class NoTracesSelected(SnufflingError):
1745 '''
1746 This exception is raised, when no traces have been selected in the viewer
1747 and we cannot fallback to using the current view.
1748 '''
1750 def __str__(self):
1751 return 'No traces have been selected / are available.'
1754class UserCancelled(SnufflingError):
1755 '''
1756 This exception is raised, when the user has cancelled a snuffling dialog.
1757 '''
1759 def __str__(self):
1760 return 'The user has cancelled a dialog.'
1763class SnufflingCallFailed(SnufflingError):
1764 '''
1765 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1766 :py:meth:`Snuffling.call`.
1767 '''
1770class InvalidSnufflingFilename(Exception):
1771 pass
1774class SnufflingModule(object):
1775 '''
1776 Utility class to load/reload snufflings from a file.
1778 The snufflings are created by user modules which have the special function
1779 :py:func:`__snufflings__` which return the snuffling instances to be
1780 exported. The snuffling module is attached to a handler class, which makes
1781 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1782 from ``pile_viewer.py``). The handler class must implement the methods
1783 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1784 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1785 :py:meth:`remove_snufflings` which may be called from the handler class,
1786 when needed.
1787 '''
1789 mtimes = {}
1791 def __init__(self, path, name, handler):
1792 self._path = path
1793 self._name = name
1794 self._mtime = None
1795 self._module = None
1796 self._snufflings = []
1797 self._handler = handler
1799 def load_if_needed(self):
1800 filename = os.path.join(self._path, self._name+'.py')
1802 try:
1803 mtime = os.stat(filename)[8]
1804 except OSError as e:
1805 if e.errno == 2:
1806 logger.error(e)
1807 raise BrokenSnufflingModule(filename)
1809 if self._module is None:
1810 sys.path[0:0] = [self._path]
1811 try:
1812 logger.debug('Loading snuffling module %s' % filename)
1813 if self._name in sys.modules:
1814 raise InvalidSnufflingFilename(self._name)
1816 self._module = __import__(self._name)
1817 del sys.modules[self._name]
1819 for snuffling in self._module.__snufflings__():
1820 snuffling._filename = filename
1821 self.add_snuffling(snuffling)
1823 except Exception:
1824 logger.error(traceback.format_exc())
1825 raise BrokenSnufflingModule(filename)
1827 finally:
1828 sys.path[0:1] = []
1830 elif self._mtime != mtime:
1831 logger.warning('Reloading snuffling module %s' % filename)
1832 settings = self.remove_snufflings()
1833 sys.path[0:0] = [self._path]
1834 try:
1836 sys.modules[self._name] = self._module
1838 reload(self._module)
1839 del sys.modules[self._name]
1841 for snuffling in self._module.__snufflings__():
1842 snuffling._filename = filename
1843 self.add_snuffling(snuffling, reloaded=True)
1845 if len(self._snufflings) == len(settings):
1846 for sett, snuf in zip(settings, self._snufflings):
1847 snuf.set_settings(sett)
1849 except Exception:
1850 logger.error(traceback.format_exc())
1851 raise BrokenSnufflingModule(filename)
1853 finally:
1854 sys.path[0:1] = []
1856 self._mtime = mtime
1858 def add_snuffling(self, snuffling, reloaded=False):
1859 snuffling._path = self._path
1860 snuffling.setup()
1861 self._snufflings.append(snuffling)
1862 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1864 def remove_snufflings(self):
1865 settings = []
1866 for snuffling in self._snufflings:
1867 settings.append(snuffling.get_settings())
1868 self._handler.remove_snuffling(snuffling)
1870 self._snufflings = []
1871 return settings
1874class BrokenSnufflingModule(Exception):
1875 pass
1878class MyScrollArea(qw.QScrollArea):
1880 def sizeHint(self):
1881 s = qc.QSize()
1882 s.setWidth(self.widget().sizeHint().width())
1883 s.setHeight(self.widget().sizeHint().height())
1884 return s
1887class SwitchControl(qw.QCheckBox):
1888 sw_toggled = qc.pyqtSignal(object, bool)
1890 def __init__(self, ident, default, *args):
1891 qw.QCheckBox.__init__(self, *args)
1892 self.ident = ident
1893 self.setChecked(default)
1894 self.toggled.connect(self._sw_toggled)
1896 def _sw_toggled(self, state):
1897 self.sw_toggled.emit(self.ident, state)
1899 def set_value(self, state):
1900 self.blockSignals(True)
1901 self.setChecked(state)
1902 self.blockSignals(False)
1905class ChoiceControl(qw.QFrame):
1906 choosen = qc.pyqtSignal(object, object)
1908 def __init__(self, ident, default, choices, name, *args):
1909 qw.QFrame.__init__(self, *args)
1910 self.label = qw.QLabel(name, self)
1911 self.label.setMinimumWidth(120)
1912 self.cbox = qw.QComboBox(self)
1913 self.layout = qw.QHBoxLayout(self)
1914 self.layout.addWidget(self.label)
1915 self.layout.addWidget(self.cbox)
1916 self.layout.setContentsMargins(0, 0, 0, 0)
1917 self.layout.setSpacing(0)
1918 self.ident = ident
1919 self.choices = choices
1920 for ichoice, choice in enumerate(choices):
1921 self.cbox.addItem(choice)
1923 self.set_value(default)
1924 self.cbox.activated.connect(self.emit_choosen)
1926 def set_choices(self, choices):
1927 icur = self.cbox.currentIndex()
1928 if icur != -1:
1929 selected_choice = choices[icur]
1930 else:
1931 selected_choice = None
1933 self.choices = choices
1934 self.cbox.clear()
1935 for ichoice, choice in enumerate(choices):
1936 self.cbox.addItem(qc.QString(choice))
1938 if selected_choice is not None and selected_choice in choices:
1939 self.set_value(selected_choice)
1940 return selected_choice
1941 else:
1942 self.set_value(choices[0])
1943 return choices[0]
1945 def emit_choosen(self, i):
1946 self.choosen.emit(
1947 self.ident,
1948 self.choices[i])
1950 def set_value(self, v):
1951 self.cbox.blockSignals(True)
1952 for i, choice in enumerate(self.choices):
1953 if choice == v:
1954 self.cbox.setCurrentIndex(i)
1955 self.cbox.blockSignals(False)