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 = False
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'):
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()
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):
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')
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 try:
1314 method()
1315 except SnufflingError as e:
1316 if not isinstance(e, SnufflingCallFailed):
1317 # those have logged within error()
1318 logger.error('%s: %s' % (self._name, e))
1319 logger.error(
1320 '%s: Snuffling action failed' % self._name)
1322 self.get_viewer().update()
1323 return f
1325 but.clicked.connect(
1326 call_and_update(method))
1328 butlayout.addWidget(but)
1330 layout.addWidget(butframe, irow, 0)
1332 irow += 1
1333 spacer = qw.QSpacerItem(
1334 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1336 layout.addItem(spacer, irow, 0)
1338 return sarea
1340 else:
1341 return None
1343 def make_helpmenuitem(self, parent):
1344 '''
1345 Create the help menu item for the snuffling.
1346 '''
1348 item = qw.QAction(self.get_name(), None)
1350 item.triggered.connect(
1351 self.help_button_triggered)
1353 return item
1355 def make_menuitem(self, parent):
1356 '''
1357 Create the menu item for the snuffling.
1359 This method may be overloaded in subclass and return ``None``, if no
1360 menu entry is wanted.
1361 '''
1363 item = qw.QAction(self.get_name(), None)
1364 item.setCheckable(
1365 self._have_pre_process_hook or self._have_post_process_hook)
1367 item.triggered.connect(
1368 self.menuitem_triggered)
1370 return item
1372 def output_filename(
1373 self,
1374 caption='Save File',
1375 dir='',
1376 filter='',
1377 selected_filter=None):
1379 '''
1380 Query user for an output filename.
1382 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1383 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1384 dialog.
1385 '''
1387 if not dir and self._previous_output_filename:
1388 dir = self._previous_output_filename
1390 fn = getSaveFileName(
1391 self.get_viewer(), caption, dir, filter, selected_filter)
1392 if not fn:
1393 raise UserCancelled()
1395 self._previous_output_filename = fn
1396 return str(fn)
1398 def input_directory(self, caption='Open Directory', dir=''):
1399 '''
1400 Query user for an input directory.
1402 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1403 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1404 dialog.
1405 '''
1407 if not dir and self._previous_input_directory:
1408 dir = self._previous_input_directory
1410 dn = qw.QFileDialog.getExistingDirectory(
1411 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1413 if not dn:
1414 raise UserCancelled()
1416 self._previous_input_directory = dn
1417 return str(dn)
1419 def input_filename(self, caption='Open File', dir='', filter='',
1420 selected_filter=None):
1421 '''
1422 Query user for an input filename.
1424 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1425 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1426 dialog.
1427 '''
1429 if not dir and self._previous_input_filename:
1430 dir = self._previous_input_filename
1432 fn, _ = qw.QFileDialog.getOpenFileName(
1433 self.get_viewer(),
1434 caption,
1435 dir,
1436 filter)
1438 if not fn:
1439 raise UserCancelled()
1441 self._previous_input_filename = fn
1442 return str(fn)
1444 def input_dialog(self, caption='', request='', directory=False):
1445 '''
1446 Query user for a text input.
1448 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1449 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1450 dialog.
1451 '''
1453 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1455 if not ok:
1456 raise UserCancelled()
1458 return inp
1460 def modified_snuffling_panel(self, value, iparam):
1461 '''
1462 Called when the user has played with an adjustable parameter.
1464 The default implementation sets the parameter, calls the snuffling's
1465 :py:meth:`call` method and finally triggers an update on the viewer
1466 widget.
1467 '''
1469 param = self.get_parameters()[iparam]
1470 self._set_parameter_value(param.ident, value)
1471 if self._live_update:
1472 self.check_call()
1473 self.get_viewer().update()
1475 def switch_on_snuffling_panel(self, ident, state):
1476 '''
1477 Called when the user has toggled a switchable parameter.
1478 '''
1480 self._set_parameter_value(ident, state)
1481 if self._live_update:
1482 self.check_call()
1483 self.get_viewer().update()
1485 def choose_on_snuffling_panel(self, ident, state):
1486 '''
1487 Called when the user has made a choice about a choosable parameter.
1488 '''
1490 self._set_parameter_value(ident, state)
1491 if self._live_update:
1492 self.check_call()
1493 self.get_viewer().update()
1495 def menuitem_triggered(self, arg):
1496 '''
1497 Called when the user has triggered the snuffling's menu.
1499 The default implementation calls the snuffling's :py:meth:`call` method
1500 and triggers an update on the viewer widget.
1501 '''
1503 self.check_call()
1505 if self._have_pre_process_hook:
1506 self._pre_process_hook_enabled = arg
1508 if self._have_post_process_hook:
1509 self._post_process_hook_enabled = arg
1511 if self._have_pre_process_hook or self._have_post_process_hook:
1512 self.get_viewer().clean_update()
1513 else:
1514 self.get_viewer().update()
1516 def call_button_triggered(self):
1517 '''
1518 Called when the user has clicked the snuffling's call button.
1520 The default implementation calls the snuffling's :py:meth:`call` method
1521 and triggers an update on the viewer widget.
1522 '''
1524 self.check_call()
1525 self.get_viewer().update()
1527 def clear_button_triggered(self):
1528 '''
1529 Called when the user has clicked the snuffling's clear button.
1531 This calls the :py:meth:`cleanup` method and triggers an update on the
1532 viewer widget.
1533 '''
1535 self.cleanup()
1536 self.get_viewer().update()
1538 def help_button_triggered(self):
1539 '''
1540 Creates a :py:class:`QLabel` which contains the documentation as
1541 given in the snufflings' __doc__ string.
1542 '''
1544 if self.__doc__:
1545 if self.__doc__.strip().startswith('<html>'):
1546 doc = qw.QLabel(self.__doc__)
1547 else:
1548 try:
1549 import markdown
1550 doc = qw.QLabel(markdown.markdown(self.__doc__))
1552 except ImportError:
1553 logger.error(
1554 'Install Python module "markdown" for pretty help '
1555 'formatting.')
1557 doc = qw.QLabel(self.__doc__)
1558 else:
1559 doc = qw.QLabel('This snuffling does not provide any online help.')
1561 labels = [doc]
1563 if self._filename:
1564 from html import escape
1566 code = open(self._filename, 'r').read()
1568 doc_src = qw.QLabel(
1569 '''<html><body>
1570<hr />
1571<center><em>May the source be with you, young Skywalker!</em><br /><br />
1572<a href="file://%s"><code>%s</code></a></center>
1573<br />
1574<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1575<pre style="white-space: pre-wrap"><code>%s
1576</code></pre></p></body></html>'''
1577 % (
1578 quote(self._filename),
1579 escape(self._filename),
1580 escape(code)))
1582 labels.append(doc_src)
1584 for h in labels:
1585 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1586 h.setWordWrap(True)
1588 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1590 def live_update_toggled(self, on):
1591 '''
1592 Called when the checkbox for live-updates has been toggled.
1593 '''
1595 self.set_live_update(on)
1597 def add_traces(self, traces):
1598 '''
1599 Add traces to the viewer.
1601 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1603 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1604 added to the viewer's internal pile for display. Note, that unlike with
1605 the traces from the files given on the command line, these traces are
1606 kept in memory and so may quickly occupy a lot of ram if a lot of
1607 traces are added.
1609 This method should be preferred over modifying the viewer's internal
1610 pile directly, because this way, the snuffling has a chance to
1611 automatically remove its private traces again (see :py:meth:`cleanup`
1612 method).
1613 '''
1615 ticket = self.get_viewer().add_traces(traces)
1616 self._tickets.append(ticket)
1617 return ticket
1619 def add_trace(self, tr):
1620 '''
1621 Add a trace to the viewer.
1623 See :py:meth:`add_traces`.
1624 '''
1626 self.add_traces([tr])
1628 def add_markers(self, markers):
1629 '''
1630 Add some markers to the display.
1632 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1633 adds these to the viewer.
1634 '''
1636 self.get_viewer().add_markers(markers)
1637 self._markers.extend(markers)
1639 def add_marker(self, marker):
1640 '''
1641 Add a marker to the display.
1643 See :py:meth:`add_markers`.
1644 '''
1646 self.add_markers([marker])
1648 def cleanup(self):
1649 '''
1650 Remove all traces and markers which have been added so far by the
1651 snuffling.
1652 '''
1654 try:
1655 viewer = self.get_viewer()
1656 viewer.release_data(self._tickets)
1657 viewer.remove_markers(self._markers)
1659 except NoViewerSet:
1660 pass
1662 self._tickets = []
1663 self._markers = []
1665 def check_call(self):
1667 if self._call_in_progress:
1668 self.show_message('error', 'Previous action still in progress.')
1669 return
1671 try:
1672 self._call_in_progress = True
1673 self.call()
1674 return 0
1676 except SnufflingError as e:
1677 if not isinstance(e, SnufflingCallFailed):
1678 # those have logged within error()
1679 logger.error('%s: %s' % (self._name, e))
1680 logger.error('%s: Snuffling action failed' % self._name)
1681 return 1
1683 except Exception as e:
1684 message = '%s: Snuffling action raised an exception: %s' % (
1685 self._name, str(e))
1687 logger.exception(message)
1688 self.show_message('error', message)
1690 finally:
1691 self._call_in_progress = False
1693 def call(self):
1694 '''
1695 Main work routine of the snuffling.
1697 This method is called when the snuffling's menu item has been triggered
1698 or when the user has played with the panel controls. To be overloaded
1699 in subclass. The default implementation does nothing useful.
1700 '''
1702 pass
1704 def pre_process_hook(self, traces):
1705 return traces
1707 def post_process_hook(self, traces):
1708 return traces
1710 def get_tpad(self):
1711 '''
1712 Return current amount of extra padding needed by live processing hooks.
1713 '''
1715 return 0.0
1717 def pre_destroy(self):
1718 '''
1719 Called when the snuffling instance is about to be deleted.
1721 Can be overloaded to do user-defined cleanup actions. The
1722 default implementation calls :py:meth:`cleanup` and deletes
1723 the snuffling`s tempory directory, if needed.
1724 '''
1726 self.cleanup()
1727 if self._tempdir is not None:
1728 import shutil
1729 shutil.rmtree(self._tempdir)
1732class SnufflingError(Exception):
1733 pass
1736class NoViewerSet(SnufflingError):
1737 '''
1738 This exception is raised, when no viewer has been set on a Snuffling.
1739 '''
1741 def __str__(self):
1742 return 'No GUI available. ' \
1743 'Maybe this Snuffling cannot be run in command line mode?'
1746class MissingStationInformation(SnufflingError):
1747 '''
1748 Raised when station information is missing.
1749 '''
1752class NoTracesSelected(SnufflingError):
1753 '''
1754 This exception is raised, when no traces have been selected in the viewer
1755 and we cannot fallback to using the current view.
1756 '''
1758 def __str__(self):
1759 return 'No traces have been selected / are available.'
1762class UserCancelled(SnufflingError):
1763 '''
1764 This exception is raised, when the user has cancelled a snuffling dialog.
1765 '''
1767 def __str__(self):
1768 return 'The user has cancelled a dialog.'
1771class SnufflingCallFailed(SnufflingError):
1772 '''
1773 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1774 :py:meth:`Snuffling.call`.
1775 '''
1778class InvalidSnufflingFilename(Exception):
1779 pass
1782class SnufflingModule(object):
1783 '''
1784 Utility class to load/reload snufflings from a file.
1786 The snufflings are created by user modules which have the special function
1787 :py:func:`__snufflings__` which return the snuffling instances to be
1788 exported. The snuffling module is attached to a handler class, which makes
1789 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1790 from ``pile_viewer.py``). The handler class must implement the methods
1791 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1792 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1793 :py:meth:`remove_snufflings` which may be called from the handler class,
1794 when needed.
1795 '''
1797 mtimes = {}
1799 def __init__(self, path, name, handler):
1800 self._path = path
1801 self._name = name
1802 self._mtime = None
1803 self._module = None
1804 self._snufflings = []
1805 self._handler = handler
1807 def load_if_needed(self):
1808 filename = os.path.join(self._path, self._name+'.py')
1810 try:
1811 mtime = os.stat(filename)[8]
1812 except OSError as e:
1813 if e.errno == 2:
1814 logger.error(e)
1815 raise BrokenSnufflingModule(filename)
1817 if self._module is None:
1818 sys.path[0:0] = [self._path]
1819 try:
1820 logger.debug('Loading snuffling module %s' % filename)
1821 if self._name in sys.modules:
1822 raise InvalidSnufflingFilename(self._name)
1824 self._module = __import__(self._name)
1825 del sys.modules[self._name]
1827 for snuffling in self._module.__snufflings__():
1828 snuffling._filename = filename
1829 self.add_snuffling(snuffling)
1831 except Exception:
1832 logger.error(traceback.format_exc())
1833 raise BrokenSnufflingModule(filename)
1835 finally:
1836 sys.path[0:1] = []
1838 elif self._mtime != mtime:
1839 logger.warning('Reloading snuffling module %s' % filename)
1840 settings = self.remove_snufflings()
1841 sys.path[0:0] = [self._path]
1842 try:
1844 sys.modules[self._name] = self._module
1846 reload(self._module)
1847 del sys.modules[self._name]
1849 for snuffling in self._module.__snufflings__():
1850 snuffling._filename = filename
1851 self.add_snuffling(snuffling, reloaded=True)
1853 if len(self._snufflings) == len(settings):
1854 for sett, snuf in zip(settings, self._snufflings):
1855 snuf.set_settings(sett)
1857 except Exception:
1858 logger.error(traceback.format_exc())
1859 raise BrokenSnufflingModule(filename)
1861 finally:
1862 sys.path[0:1] = []
1864 self._mtime = mtime
1866 def add_snuffling(self, snuffling, reloaded=False):
1867 snuffling._path = self._path
1868 snuffling.setup()
1869 self._snufflings.append(snuffling)
1870 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1872 def remove_snufflings(self):
1873 settings = []
1874 for snuffling in self._snufflings:
1875 settings.append(snuffling.get_settings())
1876 self._handler.remove_snuffling(snuffling)
1878 self._snufflings = []
1879 return settings
1882class BrokenSnufflingModule(Exception):
1883 pass
1886class MyScrollArea(qw.QScrollArea):
1888 def sizeHint(self):
1889 s = qc.QSize()
1890 s.setWidth(self.widget().sizeHint().width())
1891 s.setHeight(self.widget().sizeHint().height())
1892 return s
1895class SwitchControl(qw.QCheckBox):
1896 sw_toggled = qc.pyqtSignal(object, bool)
1898 def __init__(self, ident, default, *args):
1899 qw.QCheckBox.__init__(self, *args)
1900 self.ident = ident
1901 self.setChecked(default)
1902 self.toggled.connect(self._sw_toggled)
1904 def _sw_toggled(self, state):
1905 self.sw_toggled.emit(self.ident, state)
1907 def set_value(self, state):
1908 self.blockSignals(True)
1909 self.setChecked(state)
1910 self.blockSignals(False)
1913class ChoiceControl(qw.QFrame):
1914 choosen = qc.pyqtSignal(object, object)
1916 def __init__(self, ident, default, choices, name, *args):
1917 qw.QFrame.__init__(self, *args)
1918 self.label = qw.QLabel(name, self)
1919 self.label.setMinimumWidth(120)
1920 self.cbox = qw.QComboBox(self)
1921 self.layout = qw.QHBoxLayout(self)
1922 self.layout.addWidget(self.label)
1923 self.layout.addWidget(self.cbox)
1924 self.layout.setContentsMargins(0, 0, 0, 0)
1925 self.layout.setSpacing(0)
1926 self.ident = ident
1927 self.choices = choices
1928 for ichoice, choice in enumerate(choices):
1929 self.cbox.addItem(choice)
1931 self.set_value(default)
1932 self.cbox.activated.connect(self.emit_choosen)
1934 def set_choices(self, choices):
1935 icur = self.cbox.currentIndex()
1936 if icur != -1:
1937 selected_choice = choices[icur]
1938 else:
1939 selected_choice = None
1941 self.choices = choices
1942 self.cbox.clear()
1943 for ichoice, choice in enumerate(choices):
1944 self.cbox.addItem(qc.QString(choice))
1946 if selected_choice is not None and selected_choice in choices:
1947 self.set_value(selected_choice)
1948 return selected_choice
1949 else:
1950 self.set_value(choices[0])
1951 return choices[0]
1953 def emit_choosen(self, i):
1954 self.choosen.emit(
1955 self.ident,
1956 self.choices[i])
1958 def set_value(self, v):
1959 self.cbox.blockSignals(True)
1960 for i, choice in enumerate(self.choices):
1961 if choice == v:
1962 self.cbox.setCurrentIndex(i)
1963 self.cbox.blockSignals(False)