1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Snuffling infrastructure
8This module provides the base class :py:class:`Snuffling` for user-defined
9snufflings and some utilities for their handling.
10'''
11from __future__ import absolute_import
13import os
14import sys
15import time
16import logging
17import traceback
18import tempfile
20from .qt_compat import qc, qw, getSaveFileName, use_pyqt5
22from pyrocko import pile, config
23from pyrocko.util import quote
25from .util import (ValControl, LinValControl, FigureFrame, WebKitFrame,
26 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker,
27 load_markers, save_markers)
30if sys.version_info >= (3, 0):
31 from importlib import reload
34Marker, load_markers, save_markers # noqa
36logger = logging.getLogger('pyrocko.gui.snuffling')
39def fnpatch(x):
40 if use_pyqt5:
41 return x
42 else:
43 return x, None
46class MyFrame(qw.QFrame):
47 widgetVisibilityChanged = qc.pyqtSignal(bool)
49 def showEvent(self, ev):
50 self.widgetVisibilityChanged.emit(True)
52 def hideEvent(self, ev):
53 self.widgetVisibilityChanged.emit(False)
56class Param(object):
57 '''
58 Definition of an adjustable floating point parameter for the
59 snuffling. The snuffling may display controls for user input for
60 such parameters.
62 :param name: labels the parameter on the snuffling's control panel
63 :param ident: identifier of the parameter
64 :param default: default value
65 :param minimum: minimum value for the parameter
66 :param maximum: maximum value for the parameter
67 :param low_is_none: if ``True``: parameter is set to None at lowest value
68 of parameter range (optional)
69 :param high_is_none: if ``True``: parameter is set to None at highest value
70 of parameter range (optional)
71 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
72 value of parameter range (optional)
73 '''
75 def __init__(
76 self, name, ident, default, minimum, maximum,
77 low_is_none=None,
78 high_is_none=None,
79 low_is_zero=False,
80 tracking=True,
81 type=float):
83 if low_is_none and default == minimum:
84 default = None
85 if high_is_none and default == maximum:
86 default = None
88 self.name = name
89 self.ident = ident
90 self.default = default
91 self.minimum = minimum
92 self.maximum = maximum
93 self.low_is_none = low_is_none
94 self.high_is_none = high_is_none
95 self.low_is_zero = low_is_zero
96 self.tracking = tracking
97 self.type = type
99 self._control = None
102class Switch(object):
103 '''
104 Definition of a boolean switch for the snuffling. The snuffling
105 may display a checkbox for such a switch.
107 :param name: labels the switch on the snuffling's control panel
108 :param ident: identifier of the parameter
109 :param default: default value
110 '''
112 def __init__(self, name, ident, default):
113 self.name = name
114 self.ident = ident
115 self.default = default
118class Choice(object):
119 '''
120 Definition of a string choice for the snuffling. The snuffling
121 may display a menu for such a choice.
123 :param name: labels the menu on the snuffling's control panel
124 :param ident: identifier of the parameter
125 :param default: default value
126 :param choices: tuple of other options
127 '''
129 def __init__(self, name, ident, default, choices):
130 self.name = name
131 self.ident = ident
132 self.default = default
133 self.choices = choices
136class Snuffling(object):
137 '''
138 Base class for user snufflings.
140 Snufflings are plugins for snuffler (and other applications using the
141 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in
142 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
143 should provide a simple way of extending the functionality of snuffler.
145 A snuffling has access to all data available in a pile viewer, can process
146 this data and can create and add new traces and markers to the viewer.
147 '''
149 def __init__(self):
150 self._path = None
152 self._name = 'Untitled Snuffling'
153 self._viewer = None
154 self._tickets = []
155 self._markers = []
157 self._delete_panel = None
158 self._delete_menuitem = None
160 self._panel_parent = None
161 self._menu_parent = None
163 self._panel = None
164 self._menuitem = None
165 self._helpmenuitem = None
166 self._parameters = []
167 self._param_controls = {}
169 self._triggers = []
171 self._live_update = True
172 self._previous_output_filename = None
173 self._previous_input_filename = None
174 self._previous_input_directory = None
176 self._tempdir = None
177 self._iplot = 0
179 self._have_pre_process_hook = False
180 self._have_post_process_hook = False
181 self._pre_process_hook_enabled = False
182 self._post_process_hook_enabled = False
184 self._no_viewer_pile = None
185 self._cli_params = {}
186 self._filename = None
187 self._force_panel = False
188 self._call_in_progress = False
190 def setup(self):
191 '''
192 Setup the snuffling.
194 This method should be implemented in subclass and contain e.g. calls to
195 :py:meth:`set_name` and :py:meth:`add_parameter`.
196 '''
198 pass
200 def module_dir(self):
201 '''
202 Returns the path of the directory where snufflings are stored.
204 The default path is ``$HOME/.snufflings``.
205 '''
207 return self._path
209 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
210 '''
211 Set parent viewer and hooks to add panel and menu entry.
213 This method is called from the
214 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls
215 :py:meth:`setup_gui`.
216 '''
218 self._viewer = viewer
219 self._panel_parent = panel_parent
220 self._menu_parent = menu_parent
222 self.setup_gui(reloaded=reloaded)
224 def setup_gui(self, reloaded=False):
225 '''
226 Create and add gui elements to the viewer.
228 This method is initially called from :py:meth:`init_gui`. It is also
229 called, e.g. when new parameters have been added or if the name of the
230 snuffling has been changed.
231 '''
233 if self._panel_parent is not None:
234 self._panel = self.make_panel(self._panel_parent)
235 if self._panel:
236 self._panel_parent.add_panel(
237 self.get_name(), self._panel, reloaded)
239 if self._menu_parent is not None:
240 self._menuitem = self.make_menuitem(self._menu_parent)
241 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
242 if self._menuitem:
243 self._menu_parent.add_snuffling_menuitem(self._menuitem)
245 if self._helpmenuitem:
246 self._menu_parent.add_snuffling_help_menuitem(
247 self._helpmenuitem)
249 def set_force_panel(self, bool=True):
250 '''
251 Force to create a panel.
253 :param bool: if ``True`` will create a panel with Help, Clear and Run
254 button.
255 '''
257 self._force_panel = bool
259 def make_cli_parser1(self):
260 import optparse
262 class MyOptionParser(optparse.OptionParser):
263 def error(self, msg):
264 logger.error(msg)
265 self.exit(1)
267 parser = MyOptionParser()
269 parser.add_option(
270 '--format',
271 dest='format',
272 default='from_extension',
273 choices=(
274 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
275 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
276 'from_extension', 'detect'),
277 help='assume files are of given FORMAT [default: \'%default\']')
279 parser.add_option(
280 '--pattern',
281 dest='regex',
282 metavar='REGEX',
283 help='only include files whose paths match REGEX')
285 self.add_params_to_cli_parser(parser)
286 self.configure_cli_parser(parser)
287 return parser
289 def configure_cli_parser(self, parser):
290 pass
292 def cli_usage(self):
293 return None
295 def add_params_to_cli_parser(self, parser):
297 for param in self._parameters:
298 if isinstance(param, Param):
299 parser.add_option(
300 '--' + param.ident,
301 dest=param.ident,
302 default=param.default,
303 type={float: 'float', int: 'int'}[param.type],
304 help=param.name)
306 def setup_cli(self):
307 self.setup()
308 parser = self.make_cli_parser1()
309 (options, args) = parser.parse_args()
311 for param in self._parameters:
312 if isinstance(param, Param):
313 setattr(self, param.ident, getattr(options, param.ident))
315 self._cli_params['regex'] = options.regex
316 self._cli_params['format'] = options.format
317 self._cli_params['sources'] = args
319 return options, args, parser
321 def delete_gui(self):
322 '''
323 Remove the gui elements of the snuffling.
325 This removes the panel and menu entry of the widget from the viewer and
326 also removes all traces and markers added with the
327 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
328 '''
330 self.cleanup()
332 if self._panel is not None:
333 self._panel_parent.remove_panel(self._panel)
334 self._panel = None
336 if self._menuitem is not None:
337 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
338 self._menuitem = None
340 if self._helpmenuitem is not None:
341 self._menu_parent.remove_snuffling_help_menuitem(
342 self._helpmenuitem)
344 def set_name(self, name):
345 '''
346 Set the snuffling's name.
348 The snuffling's name is shown as a menu entry and in the panel header.
349 '''
351 self._name = name
352 self.reset_gui()
354 def get_name(self):
355 '''
356 Get the snuffling's name.
357 '''
359 return self._name
361 def set_have_pre_process_hook(self, bool):
362 self._have_pre_process_hook = bool
363 self._live_update = False
364 self._pre_process_hook_enabled = False
365 self.reset_gui()
367 def set_have_post_process_hook(self, bool):
368 self._have_post_process_hook = bool
369 self._live_update = False
370 self._post_process_hook_enabled = False
371 self.reset_gui()
373 def set_have_pile_changed_hook(self, bool):
374 self._pile_ = False
376 def enable_pile_changed_notifications(self):
377 '''
378 Get informed when pile changed.
380 When activated, the :py:meth:`pile_changed` method is called on every
381 update in the viewer's pile.
382 '''
384 viewer = self.get_viewer()
385 viewer.pile_has_changed_signal.connect(
386 self.pile_changed)
388 def disable_pile_changed_notifications(self):
389 '''
390 Stop getting informed about changes in viewer's pile.
391 '''
393 viewer = self.get_viewer()
394 viewer.pile_has_changed_signal.disconnect(
395 self.pile_changed)
397 def pile_changed(self):
398 '''
399 Called when the connected viewer's pile has changed.
401 Must be activated with a call to
402 :py:meth:`enable_pile_changed_notifications`.
403 '''
405 pass
407 def reset_gui(self, reloaded=False):
408 '''
409 Delete and recreate the snuffling's panel.
410 '''
412 if self._panel or self._menuitem:
413 sett = self.get_settings()
414 self.delete_gui()
415 self.setup_gui(reloaded=reloaded)
416 self.set_settings(sett)
418 def show_message(self, kind, message):
419 '''
420 Display a message box.
422 :param kind: string defining kind of message
423 :param message: the message to be displayed
424 '''
426 try:
427 box = qw.QMessageBox(self.get_viewer())
428 box.setText('%s: %s' % (kind.capitalize(), message))
429 box.exec_()
430 except NoViewerSet:
431 pass
433 def error(self, message):
434 '''
435 Show an error message box.
437 :param message: specifying the error
438 '''
440 logger.error('%s: %s' % (self._name, message))
441 self.show_message('error', message)
443 def warn(self, message):
444 '''
445 Display a warning message.
447 :param message: specifying the warning
448 '''
450 logger.warning('%s: %s' % (self._name, message))
451 self.show_message('warning', message)
453 def fail(self, message):
454 '''
455 Show an error message box and raise :py:exc:`SnufflingCallFailed`
456 exception.
458 :param message: specifying the error
459 '''
461 self.error(message)
462 raise SnufflingCallFailed(message)
464 def pylab(self, name=None, get='axes'):
465 '''
466 Create a :py:class:`FigureFrame` and return either the frame,
467 a :py:class:`matplotlib.figure.Figure` instance or a
468 :py:class:`matplotlib.axes.Axes` instance.
470 :param name: labels the figure frame's tab
471 :param get: 'axes'|'figure'|'frame' (optional)
472 '''
474 if name is None:
475 self._iplot += 1
476 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
478 fframe = FigureFrame()
479 self._panel_parent.add_tab(name, fframe)
480 if get == 'axes':
481 return fframe.gca()
482 elif get == 'figure':
483 return fframe.gcf()
484 elif get == 'figure_frame':
485 return fframe
487 def figure(self, name=None):
488 '''
489 Returns a :py:class:`matplotlib.figure.Figure` instance
490 which can be displayed within snuffler by calling
491 :py:meth:`canvas.draw`.
493 :param name: labels the tab of the figure
494 '''
496 return self.pylab(name=name, get='figure')
498 def axes(self, name=None):
499 '''
500 Returns a :py:class:`matplotlib.axes.Axes` instance.
502 :param name: labels the tab of axes
503 '''
505 return self.pylab(name=name, get='axes')
507 def figure_frame(self, name=None):
508 '''
509 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
511 :param name: labels the tab figure frame
512 '''
514 return self.pylab(name=name, get='figure_frame')
516 def pixmap_frame(self, filename=None, name=None):
517 '''
518 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
520 :param name: labels the tab
521 :param filename: name of file to be displayed
522 '''
524 f = PixmapFrame(filename)
526 scroll_area = qw.QScrollArea()
527 scroll_area.setWidget(f)
528 scroll_area.setWidgetResizable(True)
530 self._panel_parent.add_tab(name or "Pixmap", scroll_area)
531 return f
533 def web_frame(self, url=None, name=None):
534 '''
535 Creates a :py:class:`WebKitFrame` which can be used as a browser
536 within snuffler.
538 :param url: url to open
539 :param name: labels the tab
540 '''
542 if name is None:
543 self._iplot += 1
544 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
546 f = WebKitFrame(url)
547 self._panel_parent.add_tab(name, f)
548 return f
550 def vtk_frame(self, name=None, actors=None):
551 '''
552 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
553 graphics.
555 :param actors: list of VTKActors
556 :param name: labels the tab
558 Initialize the interactive rendering by calling the frames'
559 :py:meth`initialize` method after having added all actors to the frames
560 renderer.
562 Requires installation of vtk including python wrapper.
563 '''
564 if name is None:
565 self._iplot += 1
566 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
568 try:
569 f = VTKFrame(actors=actors)
570 except ImportError as e:
571 self.fail(e)
573 self._panel_parent.add_tab(name, f)
574 return f
576 def tempdir(self):
577 '''
578 Create a temporary directory and return its absolute path.
580 The directory and all its contents are removed when the Snuffling
581 instance is deleted.
582 '''
584 if self._tempdir is None:
585 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
587 return self._tempdir
589 def set_live_update(self, live_update):
590 '''
591 Enable/disable live updating.
593 When live updates are enabled, the :py:meth:`call` method is called
594 whenever the user changes a parameter. If it is disabled, the user has
595 to initiate such a call manually by triggering the snuffling's menu
596 item or pressing the call button.
597 '''
599 self._live_update = live_update
600 if self._have_pre_process_hook:
601 self._pre_process_hook_enabled = live_update
602 if self._have_post_process_hook:
603 self._post_process_hook_enabled = live_update
605 try:
606 self.get_viewer().clean_update()
607 except NoViewerSet:
608 pass
610 def add_parameter(self, param):
611 '''
612 Add an adjustable parameter to the snuffling.
614 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
615 :py:class:`Choice`.
617 For each parameter added, controls are added to the snuffling's panel,
618 so that the parameter can be adjusted from the gui.
619 '''
621 self._parameters.append(param)
622 self._set_parameter_value(param.ident, param.default)
624 if self._panel is not None:
625 self.delete_gui()
626 self.setup_gui()
628 def add_trigger(self, name, method):
629 '''
630 Add a button to the snuffling's panel.
632 :param name: string that labels the button
633 :param method: method associated with the button
634 '''
636 self._triggers.append((name, method))
638 if self._panel is not None:
639 self.delete_gui()
640 self.setup_gui()
642 def get_parameters(self):
643 '''
644 Get the snuffling's adjustable parameter definitions.
646 Returns a list of objects of type :py:class:`Param`.
647 '''
649 return self._parameters
651 def get_parameter(self, ident):
652 '''
653 Get one of the snuffling's adjustable parameter definitions.
655 :param ident: identifier of the parameter
657 Returns an object of type :py:class:`Param` or ``None``.
658 '''
660 for param in self._parameters:
661 if param.ident == ident:
662 return param
663 return None
665 def set_parameter(self, ident, value):
666 '''
667 Set one of the snuffling's adjustable parameters.
669 :param ident: identifier of the parameter
670 :param value: new value of the parameter
672 Adjusts the control of a parameter without calling :py:meth:`call`.
673 '''
675 self._set_parameter_value(ident, value)
677 control = self._param_controls.get(ident, None)
678 if control:
679 control.set_value(value)
681 def set_parameter_range(self, ident, vmin, vmax):
682 '''
683 Set the range of one of the snuffling's adjustable parameters.
685 :param ident: identifier of the parameter
686 :param vmin,vmax: new minimum and maximum value for the parameter
688 Adjusts the control of a parameter without calling :py:meth:`call`.
689 '''
691 control = self._param_controls.get(ident, None)
692 if control:
693 control.set_range(vmin, vmax)
695 def set_parameter_choices(self, ident, choices):
696 '''
697 Update the choices of a Choice parameter.
699 :param ident: identifier of the parameter
700 :param choices: list of strings
701 '''
703 control = self._param_controls.get(ident, None)
704 if control:
705 selected_choice = control.set_choices(choices)
706 self._set_parameter_value(ident, selected_choice)
708 def _set_parameter_value(self, ident, value):
709 setattr(self, ident, value)
711 def get_parameter_value(self, ident):
712 '''
713 Get the current value of a parameter.
715 :param ident: identifier of the parameter
716 '''
717 return getattr(self, ident)
719 def get_settings(self):
720 '''
721 Returns a dictionary with identifiers of all parameters as keys and
722 their values as the dictionaries values.
723 '''
725 params = self.get_parameters()
726 settings = {}
727 for param in params:
728 settings[param.ident] = self.get_parameter_value(param.ident)
730 return settings
732 def set_settings(self, settings):
733 params = self.get_parameters()
734 dparams = dict([(param.ident, param) for param in params])
735 for k, v in settings.items():
736 if k in dparams:
737 self._set_parameter_value(k, v)
738 if k in self._param_controls:
739 control = self._param_controls[k]
740 control.set_value(v)
742 def get_viewer(self):
743 '''
744 Get the parent viewer.
746 Returns a reference to an object of type :py:class:`PileOverview`,
747 which is the main viewer widget.
749 If no gui has been initialized for the snuffling, a
750 :py:exc:`NoViewerSet` exception is raised.
751 '''
753 if self._viewer is None:
754 raise NoViewerSet()
755 return self._viewer
757 def get_pile(self):
758 '''
759 Get the pile.
761 If a gui has been initialized, a reference to the viewer's internal
762 pile is returned. If not, the :py:meth:`make_pile` method (which may be
763 overloaded in subclass) is called to create a pile. This can be
764 utilized to make hybrid snufflings, which may work also in a standalone
765 mode.
766 '''
768 try:
769 p = self.get_viewer().get_pile()
770 except NoViewerSet:
771 if self._no_viewer_pile is None:
772 self._no_viewer_pile = self.make_pile()
774 p = self._no_viewer_pile
776 return p
778 def get_active_event_and_stations(
779 self, trange=(-3600., 3600.), missing='warn'):
781 '''
782 Get event and stations with available data for active event.
784 :param trange: (begin, end), time range around event origin time to
785 query for available data
786 :param missing: string, what to do in case of missing station
787 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
789 :returns: ``(event, stations)``
790 '''
792 p = self.get_pile()
793 v = self.get_viewer()
795 event = v.get_active_event()
796 if event is None:
797 self.fail(
798 'No active event set. Select an event and press "e" to make '
799 'it the "active event"')
801 stations = {}
802 for traces in p.chopper(
803 event.time+trange[0],
804 event.time+trange[1],
805 load_data=False,
806 degap=False):
808 for tr in traces:
809 try:
810 for skey in v.station_keys(tr):
811 if skey in stations:
812 continue
814 station = v.get_station(skey)
815 stations[skey] = station
817 except KeyError:
818 s = 'No station information for station key "%s".' \
819 % '.'.join(skey)
821 if missing == 'warn':
822 logger.warning(s)
823 elif missing == 'raise':
824 raise MissingStationInformation(s)
825 elif missing == 'ignore':
826 pass
827 else:
828 assert False, 'invalid argument to "missing"'
830 stations[skey] = None
832 return event, list(set(
833 st for st in stations.values() if st is not None))
835 def get_stations(self):
836 '''
837 Get all stations known to the viewer.
838 '''
840 v = self.get_viewer()
841 stations = list(v.stations.values())
842 return stations
844 def get_markers(self):
845 '''
846 Get all markers from the viewer.
847 '''
849 return self.get_viewer().get_markers()
851 def get_event_markers(self):
852 '''
853 Get all event markers from the viewer.
854 '''
856 return [m for m in self.get_viewer().get_markers()
857 if isinstance(m, EventMarker)]
859 def get_selected_markers(self):
860 '''
861 Get all selected markers from the viewer.
862 '''
864 return self.get_viewer().selected_markers()
866 def get_selected_event_markers(self):
867 '''
868 Get all selected event markers from the viewer.
869 '''
871 return [m for m in self.get_viewer().selected_markers()
872 if isinstance(m, EventMarker)]
874 def get_active_event_and_phase_markers(self):
875 '''
876 Get the marker of the active event and any associated phase markers
877 '''
879 viewer = self.get_viewer()
880 markers = viewer.get_markers()
881 event_marker = viewer.get_active_event_marker()
882 if event_marker is None:
883 self.fail(
884 'No active event set. '
885 'Select an event and press "e" to make it the "active event"')
887 event = event_marker.get_event()
889 selection = []
890 for m in markers:
891 if isinstance(m, PhaseMarker):
892 if m.get_event() is event:
893 selection.append(m)
895 return (
896 event_marker,
897 [m for m in markers if isinstance(m, PhaseMarker) and
898 m.get_event() == event])
900 def get_viewer_trace_selector(self, mode='inview'):
901 '''
902 Get currently active trace selector from viewer.
904 :param mode: set to ``'inview'`` (default) to only include selections
905 currently shown in the viewer, ``'visible' to include all traces
906 not currenly hidden by hide or quick-select commands, or ``'all'``
907 to disable any restrictions.
908 '''
910 viewer = self.get_viewer()
912 def rtrue(tr):
913 return True
915 if mode == 'inview':
916 return viewer.trace_selector or rtrue
917 elif mode == 'visible':
918 return viewer.trace_filter or rtrue
919 elif mode == 'all':
920 return rtrue
921 else:
922 raise Exception('invalid mode argument')
924 def chopper_selected_traces(self, fallback=False, marker_selector=None,
925 mode='inview', main_bandpass=False,
926 progress=None, responsive=False,
927 *args, **kwargs):
928 '''
929 Iterate over selected traces.
931 Shortcut to get all trace data contained in the selected markers in the
932 running snuffler. For each selected marker,
933 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
934 *tmin*, *tmax*, and *trace_selector* set to values according to the
935 marker. Additional arguments to the chopper are handed over from
936 *\\*args* and *\\*\\*kwargs*.
938 :param fallback:
939 If ``True``, if no selection has been marked, use the content
940 currently visible in the viewer.
942 :param marker_selector:
943 If not ``None`` a callback to filter markers.
945 :param mode:
946 Set to ``'inview'`` (default) to only include selections currently
947 shown in the viewer (excluding traces accessible through vertical
948 scrolling), ``'visible'`` to include all traces not currently
949 hidden by hide or quick-select commands (including traces
950 accessible through vertical scrolling), or ``'all'`` to disable any
951 restrictions.
953 :param main_bandpass:
954 If ``True``, apply main control high- and lowpass filters to
955 traces. Note: use with caution. Processing is fixed to use 4th
956 order Butterworth highpass and lowpass and the signal is always
957 demeaned before filtering. FFT filtering, rotation, demean and
958 bandpass settings from the graphical interface are not respected
959 here. Padding is not automatically adjusted so results may include
960 artifacts.
962 :param progress:
963 If given a string a progress bar is shown to the user. The string
964 is used as the label for the progress bar.
966 :param responsive:
967 If set to ``True``, occasionally allow UI events to be processed.
968 If used in combination with ``progress``, this allows the iterator
969 to be aborted by the user.
970 '''
972 try:
973 viewer = self.get_viewer()
974 markers = [
975 m for m in viewer.selected_markers()
976 if not isinstance(m, EventMarker)]
978 if marker_selector is not None:
979 markers = [
980 marker for marker in markers if marker_selector(marker)]
982 pile = self.get_pile()
984 def rtrue(tr):
985 return True
987 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
988 trace_selector_viewer = self.get_viewer_trace_selector(mode)
990 style_arg = kwargs.pop('style', None)
992 if main_bandpass:
993 def apply_filters(traces):
994 for tr in traces:
995 if viewer.highpass is not None:
996 tr.highpass(4, viewer.highpass)
997 if viewer.lowpass is not None:
998 tr.lowpass(4, viewer.lowpass)
999 return traces
1000 else:
1001 def apply_filters(traces):
1002 return traces
1004 pb = viewer.parent().get_progressbars()
1006 time_last = [time.time()]
1008 def update_progress(label, batch):
1009 time_now = time.time()
1010 if responsive:
1011 # start processing events with one second delay, so that
1012 # e.g. cleanup actions at startup do not cause track number
1013 # changes etc.
1014 if time_last[0] + 1. < time_now:
1015 qw.qApp.processEvents()
1016 else:
1017 # redraw about once a second
1018 if time_last[0] + 1. < time_now:
1019 viewer.repaint()
1021 time_last[0] = time.time() # use time after drawing
1023 abort = pb.set_status(
1024 label, batch.i*100./batch.n, responsive)
1025 abort |= viewer.window().is_closing()
1027 return abort
1029 if markers:
1030 for imarker, marker in enumerate(markers):
1031 try:
1032 if progress:
1033 label = '%s: %i/%i' % (
1034 progress, imarker+1, len(markers))
1036 pb.set_status(label, 0, responsive)
1038 if not marker.nslc_ids:
1039 trace_selector_marker = rtrue
1040 else:
1041 def trace_selector_marker(tr):
1042 return marker.match_nslc(tr.nslc_id)
1044 def trace_selector(tr):
1045 return trace_selector_arg(tr) \
1046 and trace_selector_viewer(tr) \
1047 and trace_selector_marker(tr)
1049 for batch in pile.chopper(
1050 tmin=marker.tmin,
1051 tmax=marker.tmax,
1052 trace_selector=trace_selector,
1053 style='batch',
1054 *args,
1055 **kwargs):
1057 if progress:
1058 abort = update_progress(label, batch)
1059 if abort:
1060 return
1062 batch.traces = apply_filters(batch.traces)
1063 if style_arg == 'batch':
1064 yield batch
1065 else:
1066 yield batch.traces
1068 finally:
1069 if progress:
1070 pb.set_status(label, 100., responsive)
1072 elif fallback:
1073 def trace_selector(tr):
1074 return trace_selector_arg(tr) \
1075 and trace_selector_viewer(tr)
1077 tmin, tmax = viewer.get_time_range()
1079 if not pile.is_empty():
1080 ptmin = pile.get_tmin()
1081 tpad = kwargs.get('tpad', 0.0)
1082 if ptmin > tmin:
1083 tmin = ptmin + tpad
1084 ptmax = pile.get_tmax()
1085 if ptmax < tmax:
1086 tmax = ptmax - tpad
1088 try:
1089 if progress:
1090 label = progress
1091 pb.set_status(label, 0, responsive)
1093 for batch in pile.chopper(
1094 tmin=tmin,
1095 tmax=tmax,
1096 trace_selector=trace_selector,
1097 style='batch',
1098 *args,
1099 **kwargs):
1101 if progress:
1102 abort = update_progress(label, batch)
1104 if abort:
1105 return
1107 batch.traces = apply_filters(batch.traces)
1109 if style_arg == 'batch':
1110 yield batch
1111 else:
1112 yield batch.traces
1114 finally:
1115 if progress:
1116 pb.set_status(label, 100., responsive)
1118 else:
1119 raise NoTracesSelected()
1121 except NoViewerSet:
1122 pile = self.get_pile()
1123 return pile.chopper(*args, **kwargs)
1125 def get_selected_time_range(self, fallback=False):
1126 '''
1127 Get the time range spanning all selected markers.
1129 :param fallback: if ``True`` and no marker is selected return begin and
1130 end of visible time range
1131 '''
1133 viewer = self.get_viewer()
1134 markers = viewer.selected_markers()
1135 mins = [marker.tmin for marker in markers]
1136 maxs = [marker.tmax for marker in markers]
1138 if mins and maxs:
1139 tmin = min(mins)
1140 tmax = max(maxs)
1142 elif fallback:
1143 tmin, tmax = viewer.get_time_range()
1145 else:
1146 raise NoTracesSelected()
1148 return tmin, tmax
1150 def panel_visibility_changed(self, bool):
1151 '''
1152 Called when the snuffling's panel becomes visible or is hidden.
1154 Can be overloaded in subclass, e.g. to perform additional setup actions
1155 when the panel is activated the first time.
1156 '''
1158 pass
1160 def make_pile(self):
1161 '''
1162 Create a pile.
1164 To be overloaded in subclass. The default implementation just calls
1165 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1166 arguments.
1167 '''
1169 cachedirname = config.config().cache_dir
1170 sources = self._cli_params.get('sources', sys.argv[1:])
1171 return pile.make_pile(
1172 sources,
1173 cachedirname=cachedirname,
1174 regex=self._cli_params['regex'],
1175 fileformat=self._cli_params['format'])
1177 def make_panel(self, parent):
1178 '''
1179 Create a widget for the snuffling's control panel.
1181 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1182 if no panel is needed (e.g. if the snuffling has no adjustable
1183 parameters).
1184 '''
1186 params = self.get_parameters()
1187 self._param_controls = {}
1188 if params or self._force_panel:
1189 sarea = MyScrollArea(parent.get_panel_parent_widget())
1190 sarea.setFrameStyle(qw.QFrame.NoFrame)
1191 sarea.setSizePolicy(qw.QSizePolicy(
1192 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1193 frame = MyFrame(sarea)
1194 frame.widgetVisibilityChanged.connect(
1195 self.panel_visibility_changed)
1197 frame.setSizePolicy(qw.QSizePolicy(
1198 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1199 frame.setFrameStyle(qw.QFrame.NoFrame)
1200 sarea.setWidget(frame)
1201 sarea.setWidgetResizable(True)
1202 layout = qw.QGridLayout()
1203 layout.setContentsMargins(0, 0, 0, 0)
1204 layout.setSpacing(0)
1205 frame.setLayout(layout)
1207 parlayout = qw.QGridLayout()
1209 irow = 0
1210 ipar = 0
1211 have_switches = False
1212 have_params = False
1213 for iparam, param in enumerate(params):
1214 if isinstance(param, Param):
1215 if param.minimum <= 0.0:
1216 param_control = LinValControl(
1217 high_is_none=param.high_is_none,
1218 low_is_none=param.low_is_none,
1219 type=param.type)
1220 else:
1221 param_control = ValControl(
1222 high_is_none=param.high_is_none,
1223 low_is_none=param.low_is_none,
1224 low_is_zero=param.low_is_zero,
1225 type=param.type)
1227 param_control.setup(
1228 param.name,
1229 param.minimum,
1230 param.maximum,
1231 param.default,
1232 iparam)
1234 param_control.set_tracking(param.tracking)
1235 param_control.valchange.connect(
1236 self.modified_snuffling_panel)
1238 self._param_controls[param.ident] = param_control
1239 for iw, w in enumerate(param_control.widgets()):
1240 parlayout.addWidget(w, ipar, iw)
1242 ipar += 1
1243 have_params = True
1245 elif isinstance(param, Choice):
1246 param_widget = ChoiceControl(
1247 param.ident, param.default, param.choices, param.name)
1248 param_widget.choosen.connect(
1249 self.choose_on_snuffling_panel)
1251 self._param_controls[param.ident] = param_widget
1252 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1253 ipar += 1
1254 have_params = True
1256 elif isinstance(param, Switch):
1257 have_switches = True
1259 if have_params:
1260 parframe = qw.QFrame(sarea)
1261 parframe.setSizePolicy(qw.QSizePolicy(
1262 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1263 parframe.setLayout(parlayout)
1264 layout.addWidget(parframe, irow, 0)
1265 irow += 1
1267 if have_switches:
1268 swlayout = qw.QGridLayout()
1269 isw = 0
1270 for iparam, param in enumerate(params):
1271 if isinstance(param, Switch):
1272 param_widget = SwitchControl(
1273 param.ident, param.default, param.name)
1274 param_widget.sw_toggled.connect(
1275 self.switch_on_snuffling_panel)
1277 self._param_controls[param.ident] = param_widget
1278 swlayout.addWidget(param_widget, isw/10, isw % 10)
1279 isw += 1
1281 swframe = qw.QFrame(sarea)
1282 swframe.setSizePolicy(qw.QSizePolicy(
1283 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1284 swframe.setLayout(swlayout)
1285 layout.addWidget(swframe, irow, 0)
1286 irow += 1
1288 butframe = qw.QFrame(sarea)
1289 butframe.setSizePolicy(qw.QSizePolicy(
1290 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1291 butlayout = qw.QHBoxLayout()
1292 butframe.setLayout(butlayout)
1294 live_update_checkbox = qw.QCheckBox('Auto-Run')
1295 if self._live_update:
1296 live_update_checkbox.setCheckState(qc.Qt.Checked)
1298 butlayout.addWidget(live_update_checkbox)
1299 live_update_checkbox.toggled.connect(
1300 self.live_update_toggled)
1302 help_button = qw.QPushButton('Help')
1303 butlayout.addWidget(help_button)
1304 help_button.clicked.connect(
1305 self.help_button_triggered)
1307 clear_button = qw.QPushButton('Clear')
1308 butlayout.addWidget(clear_button)
1309 clear_button.clicked.connect(
1310 self.clear_button_triggered)
1312 call_button = qw.QPushButton('Run')
1313 butlayout.addWidget(call_button)
1314 call_button.clicked.connect(
1315 self.call_button_triggered)
1317 for name, method in self._triggers:
1318 but = qw.QPushButton(name)
1320 def call_and_update(method):
1321 def f():
1322 try:
1323 method()
1324 except SnufflingError as e:
1325 if not isinstance(e, SnufflingCallFailed):
1326 # those have logged within error()
1327 logger.error('%s: %s' % (self._name, e))
1328 logger.error(
1329 '%s: Snuffling action failed' % self._name)
1331 self.get_viewer().update()
1332 return f
1334 but.clicked.connect(
1335 call_and_update(method))
1337 butlayout.addWidget(but)
1339 layout.addWidget(butframe, irow, 0)
1341 irow += 1
1342 spacer = qw.QSpacerItem(
1343 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1345 layout.addItem(spacer, irow, 0)
1347 return sarea
1349 else:
1350 return None
1352 def make_helpmenuitem(self, parent):
1353 '''
1354 Create the help menu item for the snuffling.
1355 '''
1357 item = qw.QAction(self.get_name(), None)
1359 item.triggered.connect(
1360 self.help_button_triggered)
1362 return item
1364 def make_menuitem(self, parent):
1365 '''
1366 Create the menu item for the snuffling.
1368 This method may be overloaded in subclass and return ``None``, if no
1369 menu entry is wanted.
1370 '''
1372 item = qw.QAction(self.get_name(), None)
1373 item.setCheckable(
1374 self._have_pre_process_hook or self._have_post_process_hook)
1376 item.triggered.connect(
1377 self.menuitem_triggered)
1379 return item
1381 def output_filename(
1382 self,
1383 caption='Save File',
1384 dir='',
1385 filter='',
1386 selected_filter=None):
1388 '''
1389 Query user for an output filename.
1391 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1392 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1393 dialog.
1394 '''
1396 if not dir and self._previous_output_filename:
1397 dir = self._previous_output_filename
1399 fn = getSaveFileName(
1400 self.get_viewer(), caption, dir, filter, selected_filter)
1401 if not fn:
1402 raise UserCancelled()
1404 self._previous_output_filename = fn
1405 return str(fn)
1407 def input_directory(self, caption='Open Directory', dir=''):
1408 '''
1409 Query user for an input directory.
1411 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1412 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1413 dialog.
1414 '''
1416 if not dir and self._previous_input_directory:
1417 dir = self._previous_input_directory
1419 dn = qw.QFileDialog.getExistingDirectory(
1420 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1422 if not dn:
1423 raise UserCancelled()
1425 self._previous_input_directory = dn
1426 return str(dn)
1428 def input_filename(self, caption='Open File', dir='', filter='',
1429 selected_filter=None):
1430 '''
1431 Query user for an input filename.
1433 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1434 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1435 dialog.
1436 '''
1438 if not dir and self._previous_input_filename:
1439 dir = self._previous_input_filename
1441 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1442 self.get_viewer(),
1443 caption,
1444 dir,
1445 filter)) # selected_filter)
1447 if not fn:
1448 raise UserCancelled()
1450 self._previous_input_filename = fn
1451 return str(fn)
1453 def input_dialog(self, caption='', request='', directory=False):
1454 '''
1455 Query user for a text input.
1457 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1458 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1459 dialog.
1460 '''
1462 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1464 if not ok:
1465 raise UserCancelled()
1467 return inp
1469 def modified_snuffling_panel(self, value, iparam):
1470 '''
1471 Called when the user has played with an adjustable parameter.
1473 The default implementation sets the parameter, calls the snuffling's
1474 :py:meth:`call` method and finally triggers an update on the viewer
1475 widget.
1476 '''
1478 param = self.get_parameters()[iparam]
1479 self._set_parameter_value(param.ident, value)
1480 if self._live_update:
1481 self.check_call()
1482 self.get_viewer().update()
1484 def switch_on_snuffling_panel(self, ident, state):
1485 '''
1486 Called when the user has toggled a switchable parameter.
1487 '''
1489 self._set_parameter_value(ident, state)
1490 if self._live_update:
1491 self.check_call()
1492 self.get_viewer().update()
1494 def choose_on_snuffling_panel(self, ident, state):
1495 '''
1496 Called when the user has made a choice about a choosable parameter.
1497 '''
1499 self._set_parameter_value(ident, state)
1500 if self._live_update:
1501 self.check_call()
1502 self.get_viewer().update()
1504 def menuitem_triggered(self, arg):
1505 '''
1506 Called when the user has triggered the snuffling's menu.
1508 The default implementation calls the snuffling's :py:meth:`call` method
1509 and triggers an update on the viewer widget.
1510 '''
1512 self.check_call()
1514 if self._have_pre_process_hook:
1515 self._pre_process_hook_enabled = arg
1517 if self._have_post_process_hook:
1518 self._post_process_hook_enabled = arg
1520 if self._have_pre_process_hook or self._have_post_process_hook:
1521 self.get_viewer().clean_update()
1522 else:
1523 self.get_viewer().update()
1525 def call_button_triggered(self):
1526 '''
1527 Called when the user has clicked the snuffling's call button.
1529 The default implementation calls the snuffling's :py:meth:`call` method
1530 and triggers an update on the viewer widget.
1531 '''
1533 self.check_call()
1534 self.get_viewer().update()
1536 def clear_button_triggered(self):
1537 '''
1538 Called when the user has clicked the snuffling's clear button.
1540 This calls the :py:meth:`cleanup` method and triggers an update on the
1541 viewer widget.
1542 '''
1544 self.cleanup()
1545 self.get_viewer().update()
1547 def help_button_triggered(self):
1548 '''
1549 Creates a :py:class:`QLabel` which contains the documentation as
1550 given in the snufflings' __doc__ string.
1551 '''
1553 if self.__doc__:
1554 if self.__doc__.strip().startswith('<html>'):
1555 doc = qw.QLabel(self.__doc__)
1556 else:
1557 try:
1558 import markdown
1559 doc = qw.QLabel(markdown.markdown(self.__doc__))
1561 except ImportError:
1562 logger.error(
1563 'Install Python module "markdown" for pretty help '
1564 'formatting.')
1566 doc = qw.QLabel(self.__doc__)
1567 else:
1568 doc = qw.QLabel('This snuffling does not provide any online help.')
1570 labels = [doc]
1572 if self._filename:
1573 from html import escape
1575 code = open(self._filename, 'r').read()
1577 doc_src = qw.QLabel(
1578 '''<html><body>
1579<hr />
1580<center><em>May the source be with you, young Skywalker!</em><br /><br />
1581<a href="file://%s"><code>%s</code></a></center>
1582<br />
1583<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1584<pre style="white-space: pre-wrap"><code>%s
1585</code></pre></p></body></html>'''
1586 % (
1587 quote(self._filename),
1588 escape(self._filename),
1589 escape(code)))
1591 labels.append(doc_src)
1593 for h in labels:
1594 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1595 h.setWordWrap(True)
1597 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1599 def live_update_toggled(self, on):
1600 '''
1601 Called when the checkbox for live-updates has been toggled.
1602 '''
1604 self.set_live_update(on)
1606 def add_traces(self, traces):
1607 '''
1608 Add traces to the viewer.
1610 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1612 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1613 added to the viewer's internal pile for display. Note, that unlike with
1614 the traces from the files given on the command line, these traces are
1615 kept in memory and so may quickly occupy a lot of ram if a lot of
1616 traces are added.
1618 This method should be preferred over modifying the viewer's internal
1619 pile directly, because this way, the snuffling has a chance to
1620 automatically remove its private traces again (see :py:meth:`cleanup`
1621 method).
1622 '''
1624 ticket = self.get_viewer().add_traces(traces)
1625 self._tickets.append(ticket)
1626 return ticket
1628 def add_trace(self, tr):
1629 '''
1630 Add a trace to the viewer.
1632 See :py:meth:`add_traces`.
1633 '''
1635 self.add_traces([tr])
1637 def add_markers(self, markers):
1638 '''
1639 Add some markers to the display.
1641 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1642 adds these to the viewer.
1643 '''
1645 self.get_viewer().add_markers(markers)
1646 self._markers.extend(markers)
1648 def add_marker(self, marker):
1649 '''
1650 Add a marker to the display.
1652 See :py:meth:`add_markers`.
1653 '''
1655 self.add_markers([marker])
1657 def cleanup(self):
1658 '''
1659 Remove all traces and markers which have been added so far by the
1660 snuffling.
1661 '''
1663 try:
1664 viewer = self.get_viewer()
1665 viewer.release_data(self._tickets)
1666 viewer.remove_markers(self._markers)
1668 except NoViewerSet:
1669 pass
1671 self._tickets = []
1672 self._markers = []
1674 def check_call(self):
1676 if self._call_in_progress:
1677 self.show_message('error', 'Previous action still in progress.')
1678 return
1680 try:
1681 self._call_in_progress = True
1682 self.call()
1683 return 0
1685 except SnufflingError as e:
1686 if not isinstance(e, SnufflingCallFailed):
1687 # those have logged within error()
1688 logger.error('%s: %s' % (self._name, e))
1689 logger.error('%s: Snuffling action failed' % self._name)
1690 return 1
1692 except Exception as e:
1693 message = '%s: Snuffling action raised an exception: %s' % (
1694 self._name, str(e))
1696 logger.exception(message)
1697 self.show_message('error', message)
1699 finally:
1700 self._call_in_progress = False
1702 def call(self):
1703 '''
1704 Main work routine of the snuffling.
1706 This method is called when the snuffling's menu item has been triggered
1707 or when the user has played with the panel controls. To be overloaded
1708 in subclass. The default implementation does nothing useful.
1709 '''
1711 pass
1713 def pre_process_hook(self, traces):
1714 return traces
1716 def post_process_hook(self, traces):
1717 return traces
1719 def get_tpad(self):
1720 '''
1721 Return current amount of extra padding needed by live processing hooks.
1722 '''
1724 return 0.0
1726 def pre_destroy(self):
1727 '''
1728 Called when the snuffling instance is about to be deleted.
1730 Can be overloaded to do user-defined cleanup actions. The
1731 default implementation calls :py:meth:`cleanup` and deletes
1732 the snuffling`s tempory directory, if needed.
1733 '''
1735 self.cleanup()
1736 if self._tempdir is not None:
1737 import shutil
1738 shutil.rmtree(self._tempdir)
1741class SnufflingError(Exception):
1742 pass
1745class NoViewerSet(SnufflingError):
1746 '''
1747 This exception is raised, when no viewer has been set on a Snuffling.
1748 '''
1750 def __str__(self):
1751 return 'No GUI available. ' \
1752 'Maybe this Snuffling cannot be run in command line mode?'
1755class MissingStationInformation(SnufflingError):
1756 '''
1757 Raised when station information is missing.
1758 '''
1761class NoTracesSelected(SnufflingError):
1762 '''
1763 This exception is raised, when no traces have been selected in the viewer
1764 and we cannot fallback to using the current view.
1765 '''
1767 def __str__(self):
1768 return 'No traces have been selected / are available.'
1771class UserCancelled(SnufflingError):
1772 '''
1773 This exception is raised, when the user has cancelled a snuffling dialog.
1774 '''
1776 def __str__(self):
1777 return 'The user has cancelled a dialog.'
1780class SnufflingCallFailed(SnufflingError):
1781 '''
1782 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1783 :py:meth:`Snuffling.call`.
1784 '''
1787class InvalidSnufflingFilename(Exception):
1788 pass
1791class SnufflingModule(object):
1792 '''
1793 Utility class to load/reload snufflings from a file.
1795 The snufflings are created by user modules which have the special function
1796 :py:func:`__snufflings__` which return the snuffling instances to be
1797 exported. The snuffling module is attached to a handler class, which makes
1798 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1799 from ``pile_viewer.py``). The handler class must implement the methods
1800 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1801 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1802 :py:meth:`remove_snufflings` which may be called from the handler class,
1803 when needed.
1804 '''
1806 mtimes = {}
1808 def __init__(self, path, name, handler):
1809 self._path = path
1810 self._name = name
1811 self._mtime = None
1812 self._module = None
1813 self._snufflings = []
1814 self._handler = handler
1816 def load_if_needed(self):
1817 filename = os.path.join(self._path, self._name+'.py')
1819 try:
1820 mtime = os.stat(filename)[8]
1821 except OSError as e:
1822 if e.errno == 2:
1823 logger.error(e)
1824 raise BrokenSnufflingModule(filename)
1826 if self._module is None:
1827 sys.path[0:0] = [self._path]
1828 try:
1829 logger.debug('Loading snuffling module %s' % filename)
1830 if self._name in sys.modules:
1831 raise InvalidSnufflingFilename(self._name)
1833 self._module = __import__(self._name)
1834 del sys.modules[self._name]
1836 for snuffling in self._module.__snufflings__():
1837 snuffling._filename = filename
1838 self.add_snuffling(snuffling)
1840 except Exception:
1841 logger.error(traceback.format_exc())
1842 raise BrokenSnufflingModule(filename)
1844 finally:
1845 sys.path[0:1] = []
1847 elif self._mtime != mtime:
1848 logger.warning('Reloading snuffling module %s' % filename)
1849 settings = self.remove_snufflings()
1850 sys.path[0:0] = [self._path]
1851 try:
1853 sys.modules[self._name] = self._module
1855 reload(self._module)
1856 del sys.modules[self._name]
1858 for snuffling in self._module.__snufflings__():
1859 snuffling._filename = filename
1860 self.add_snuffling(snuffling, reloaded=True)
1862 if len(self._snufflings) == len(settings):
1863 for sett, snuf in zip(settings, self._snufflings):
1864 snuf.set_settings(sett)
1866 except Exception:
1867 logger.error(traceback.format_exc())
1868 raise BrokenSnufflingModule(filename)
1870 finally:
1871 sys.path[0:1] = []
1873 self._mtime = mtime
1875 def add_snuffling(self, snuffling, reloaded=False):
1876 snuffling._path = self._path
1877 snuffling.setup()
1878 self._snufflings.append(snuffling)
1879 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1881 def remove_snufflings(self):
1882 settings = []
1883 for snuffling in self._snufflings:
1884 settings.append(snuffling.get_settings())
1885 self._handler.remove_snuffling(snuffling)
1887 self._snufflings = []
1888 return settings
1891class BrokenSnufflingModule(Exception):
1892 pass
1895class MyScrollArea(qw.QScrollArea):
1897 def sizeHint(self):
1898 s = qc.QSize()
1899 s.setWidth(self.widget().sizeHint().width())
1900 s.setHeight(self.widget().sizeHint().height())
1901 return s
1904class SwitchControl(qw.QCheckBox):
1905 sw_toggled = qc.pyqtSignal(object, bool)
1907 def __init__(self, ident, default, *args):
1908 qw.QCheckBox.__init__(self, *args)
1909 self.ident = ident
1910 self.setChecked(default)
1911 self.toggled.connect(self._sw_toggled)
1913 def _sw_toggled(self, state):
1914 self.sw_toggled.emit(self.ident, state)
1916 def set_value(self, state):
1917 self.blockSignals(True)
1918 self.setChecked(state)
1919 self.blockSignals(False)
1922class ChoiceControl(qw.QFrame):
1923 choosen = qc.pyqtSignal(object, object)
1925 def __init__(self, ident, default, choices, name, *args):
1926 qw.QFrame.__init__(self, *args)
1927 self.label = qw.QLabel(name, self)
1928 self.label.setMinimumWidth(120)
1929 self.cbox = qw.QComboBox(self)
1930 self.layout = qw.QHBoxLayout(self)
1931 self.layout.addWidget(self.label)
1932 self.layout.addWidget(self.cbox)
1933 self.layout.setContentsMargins(0, 0, 0, 0)
1934 self.layout.setSpacing(0)
1935 self.ident = ident
1936 self.choices = choices
1937 for ichoice, choice in enumerate(choices):
1938 self.cbox.addItem(choice)
1940 self.set_value(default)
1941 self.cbox.activated.connect(self.emit_choosen)
1943 def set_choices(self, choices):
1944 icur = self.cbox.currentIndex()
1945 if icur != -1:
1946 selected_choice = choices[icur]
1947 else:
1948 selected_choice = None
1950 self.choices = choices
1951 self.cbox.clear()
1952 for ichoice, choice in enumerate(choices):
1953 self.cbox.addItem(qc.QString(choice))
1955 if selected_choice is not None and selected_choice in choices:
1956 self.set_value(selected_choice)
1957 return selected_choice
1958 else:
1959 self.set_value(choices[0])
1960 return choices[0]
1962 def emit_choosen(self, i):
1963 self.choosen.emit(
1964 self.ident,
1965 self.choices[i])
1967 def set_value(self, v):
1968 self.cbox.blockSignals(True)
1969 for i, choice in enumerate(self.choices):
1970 if choice == v:
1971 self.cbox.setCurrentIndex(i)
1972 self.cbox.blockSignals(False)