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'''
12import os
13import sys
14import time
15import logging
16import traceback
17import tempfile
19from .qt_compat import qc, qw, getSaveFileName
21from pyrocko import pile, config
22from pyrocko.util import quote
24from .util import (ValControl, LinValControl, FigureFrame, SmartplotFrame,
25 WebKitFrame, VTKFrame, PixmapFrame, Marker, EventMarker,
26 PhaseMarker, load_markers, save_markers)
29from importlib import reload
31Marker, load_markers, save_markers # noqa
33logger = logging.getLogger('pyrocko.gui.snuffling')
36class MyFrame(qw.QFrame):
37 widgetVisibilityChanged = qc.pyqtSignal(bool)
39 def showEvent(self, ev):
40 self.widgetVisibilityChanged.emit(True)
42 def hideEvent(self, ev):
43 self.widgetVisibilityChanged.emit(False)
46class Param(object):
47 '''
48 Definition of an adjustable floating point parameter for the
49 snuffling. The snuffling may display controls for user input for
50 such parameters.
52 :param name: labels the parameter on the snuffling's control panel
53 :param ident: identifier of the parameter
54 :param default: default value
55 :param minimum: minimum value for the parameter
56 :param maximum: maximum value for the parameter
57 :param low_is_none: if ``True``: parameter is set to None at lowest value
58 of parameter range (optional)
59 :param high_is_none: if ``True``: parameter is set to None at highest value
60 of parameter range (optional)
61 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
62 value of parameter range (optional)
63 '''
65 def __init__(
66 self, name, ident, default, minimum, maximum,
67 low_is_none=None,
68 high_is_none=None,
69 low_is_zero=False,
70 tracking=True,
71 type=float):
73 if low_is_none and default == minimum:
74 default = None
75 if high_is_none and default == maximum:
76 default = None
78 self.name = name
79 self.ident = ident
80 self.default = default
81 self.minimum = minimum
82 self.maximum = maximum
83 self.low_is_none = low_is_none
84 self.high_is_none = high_is_none
85 self.low_is_zero = low_is_zero
86 self.tracking = tracking
87 self.type = type
89 self._control = None
92class Switch(object):
93 '''
94 Definition of a boolean switch for the snuffling. The snuffling
95 may display a checkbox for such a switch.
97 :param name: labels the switch on the snuffling's control panel
98 :param ident: identifier of the parameter
99 :param default: default value
100 '''
102 def __init__(self, name, ident, default):
103 self.name = name
104 self.ident = ident
105 self.default = default
108class Choice(object):
109 '''
110 Definition of a string choice for the snuffling. The snuffling
111 may display a menu for such a choice.
113 :param name: labels the menu on the snuffling's control panel
114 :param ident: identifier of the parameter
115 :param default: default value
116 :param choices: tuple of other options
117 '''
119 def __init__(self, name, ident, default, choices):
120 self.name = name
121 self.ident = ident
122 self.default = default
123 self.choices = choices
126class Snuffling(object):
127 '''
128 Base class for user snufflings.
130 Snufflings are plugins for snuffler (and other applications using the
131 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in
132 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
133 should provide a simple way of extending the functionality of snuffler.
135 A snuffling has access to all data available in a pile viewer, can process
136 this data and can create and add new traces and markers to the viewer.
137 '''
139 def __init__(self):
140 self._path = None
142 self._name = 'Untitled Snuffling'
143 self._viewer = None
144 self._tickets = []
145 self._markers = []
147 self._delete_panel = None
148 self._delete_menuitem = None
150 self._panel_parent = None
151 self._menu_parent = None
153 self._panel = None
154 self._menuitem = None
155 self._helpmenuitem = None
156 self._parameters = []
157 self._param_controls = {}
159 self._triggers = []
161 self._live_update = True
162 self._previous_output_filename = None
163 self._previous_input_filename = None
164 self._previous_input_directory = None
166 self._tempdir = None
167 self._iplot = 0
169 self._have_pre_process_hook = False
170 self._have_post_process_hook = False
171 self._pre_process_hook_enabled = False
172 self._post_process_hook_enabled = False
174 self._no_viewer_pile = None
175 self._cli_params = {}
176 self._filename = None
177 self._force_panel = False
178 self._call_in_progress = {}
180 def setup(self):
181 '''
182 Setup the snuffling.
184 This method should be implemented in subclass and contain e.g. calls to
185 :py:meth:`set_name` and :py:meth:`add_parameter`.
186 '''
188 pass
190 def module_dir(self):
191 '''
192 Returns the path of the directory where snufflings are stored.
194 The default path is ``$HOME/.snufflings``.
195 '''
197 return self._path
199 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
200 '''
201 Set parent viewer and hooks to add panel and menu entry.
203 This method is called from the
204 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls
205 :py:meth:`setup_gui`.
206 '''
208 self._viewer = viewer
209 self._panel_parent = panel_parent
210 self._menu_parent = menu_parent
212 self.setup_gui(reloaded=reloaded)
214 def setup_gui(self, reloaded=False):
215 '''
216 Create and add gui elements to the viewer.
218 This method is initially called from :py:meth:`init_gui`. It is also
219 called, e.g. when new parameters have been added or if the name of the
220 snuffling has been changed.
221 '''
223 if self._panel_parent is not None:
224 self._panel = self.make_panel(self._panel_parent)
225 if self._panel:
226 self._panel_parent.add_panel(
227 self.get_name(), self._panel, reloaded)
229 if self._menu_parent is not None:
230 self._menuitem = self.make_menuitem(self._menu_parent)
231 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
232 if self._menuitem:
233 self._menu_parent.add_snuffling_menuitem(self._menuitem)
235 if self._helpmenuitem:
236 self._menu_parent.add_snuffling_help_menuitem(
237 self._helpmenuitem)
239 def set_force_panel(self, bool=True):
240 '''
241 Force to create a panel.
243 :param bool: if ``True`` will create a panel with Help, Clear and Run
244 button.
245 '''
247 self._force_panel = bool
249 def make_cli_parser1(self):
250 import optparse
252 class MyOptionParser(optparse.OptionParser):
253 def error(self, msg):
254 logger.error(msg)
255 self.exit(1)
257 parser = MyOptionParser()
259 parser.add_option(
260 '--format',
261 dest='format',
262 default='from_extension',
263 choices=(
264 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
265 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
266 'from_extension', 'detect'),
267 help="assume files are of given FORMAT [default: '%default']")
269 parser.add_option(
270 '--pattern',
271 dest='regex',
272 metavar='REGEX',
273 help='only include files whose paths match REGEX')
275 self.add_params_to_cli_parser(parser)
276 self.configure_cli_parser(parser)
277 return parser
279 def configure_cli_parser(self, parser):
280 pass
282 def cli_usage(self):
283 return None
285 def add_params_to_cli_parser(self, parser):
287 for param in self._parameters:
288 if isinstance(param, Param):
289 parser.add_option(
290 '--' + param.ident,
291 dest=param.ident,
292 default=param.default,
293 type={float: 'float', int: 'int'}[param.type],
294 help=param.name)
296 def setup_cli(self):
297 self.setup()
298 parser = self.make_cli_parser1()
299 (options, args) = parser.parse_args()
301 for param in self._parameters:
302 if isinstance(param, Param):
303 setattr(self, param.ident, getattr(options, param.ident))
305 self._cli_params['regex'] = options.regex
306 self._cli_params['format'] = options.format
307 self._cli_params['sources'] = args
309 return options, args, parser
311 def delete_gui(self):
312 '''
313 Remove the gui elements of the snuffling.
315 This removes the panel and menu entry of the widget from the viewer and
316 also removes all traces and markers added with the
317 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
318 '''
320 self.cleanup()
322 if self._panel is not None:
323 self._panel_parent.remove_panel(self._panel)
324 self._panel = None
326 if self._menuitem is not None:
327 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
328 self._menuitem = None
330 if self._helpmenuitem is not None:
331 self._menu_parent.remove_snuffling_help_menuitem(
332 self._helpmenuitem)
334 def set_name(self, name):
335 '''
336 Set the snuffling's name.
338 The snuffling's name is shown as a menu entry and in the panel header.
339 '''
341 self._name = name
342 self.reset_gui()
344 def get_name(self):
345 '''
346 Get the snuffling's name.
347 '''
349 return self._name
351 def set_have_pre_process_hook(self, bool):
352 self._have_pre_process_hook = bool
353 self._live_update = False
354 self._pre_process_hook_enabled = False
355 self.reset_gui()
357 def set_have_post_process_hook(self, bool):
358 self._have_post_process_hook = bool
359 self._live_update = False
360 self._post_process_hook_enabled = False
361 self.reset_gui()
363 def set_have_pile_changed_hook(self, bool):
364 self._pile_ = False
366 def enable_pile_changed_notifications(self):
367 '''
368 Get informed when pile changed.
370 When activated, the :py:meth:`pile_changed` method is called on every
371 update in the viewer's pile.
372 '''
374 viewer = self.get_viewer()
375 viewer.pile_has_changed_signal.connect(
376 self.pile_changed)
378 def disable_pile_changed_notifications(self):
379 '''
380 Stop getting informed about changes in viewer's pile.
381 '''
383 viewer = self.get_viewer()
384 viewer.pile_has_changed_signal.disconnect(
385 self.pile_changed)
387 def pile_changed(self):
388 '''
389 Called when the connected viewer's pile has changed.
391 Must be activated with a call to
392 :py:meth:`enable_pile_changed_notifications`.
393 '''
395 pass
397 def reset_gui(self, reloaded=False):
398 '''
399 Delete and recreate the snuffling's panel.
400 '''
402 if self._panel or self._menuitem:
403 sett = self.get_settings()
404 self.delete_gui()
405 self.setup_gui(reloaded=reloaded)
406 self.set_settings(sett)
408 def show_message(self, kind, message):
409 '''
410 Display a message box.
412 :param kind: string defining kind of message
413 :param message: the message to be displayed
414 '''
416 try:
417 box = qw.QMessageBox(self.get_viewer())
418 box.setText('%s: %s' % (kind.capitalize(), message))
419 box.exec_()
420 except NoViewerSet:
421 pass
423 def error(self, message):
424 '''
425 Show an error message box.
427 :param message: specifying the error
428 '''
430 logger.error('%s: %s' % (self._name, message))
431 self.show_message('error', message)
433 def warn(self, message):
434 '''
435 Display a warning message.
437 :param message: specifying the warning
438 '''
440 logger.warning('%s: %s' % (self._name, message))
441 self.show_message('warning', message)
443 def fail(self, message):
444 '''
445 Show an error message box and raise :py:exc:`SnufflingCallFailed`
446 exception.
448 :param message: specifying the error
449 '''
451 self.error(message)
452 raise SnufflingCallFailed(message)
454 def pylab(self, name=None, get='axes', figure_cls=None):
455 '''
456 Create a :py:class:`FigureFrame` and return either the frame,
457 a :py:class:`matplotlib.figure.Figure` instance or a
458 :py:class:`matplotlib.axes.Axes` instance.
460 :param name: labels the figure frame's tab
461 :param get: 'axes'|'figure'|'frame' (optional)
462 '''
464 if name is None:
465 self._iplot += 1
466 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
468 fframe = FigureFrame(figure_cls=figure_cls)
469 self._panel_parent.add_tab(name, fframe)
470 if get == 'axes':
471 return fframe.gca()
472 elif get == 'figure':
473 return fframe.gcf()
474 elif get == 'figure_frame':
475 return fframe
477 def figure(self, name=None):
478 '''
479 Returns a :py:class:`matplotlib.figure.Figure` instance
480 which can be displayed within snuffler by calling
481 :py:meth:`canvas.draw`.
483 :param name: labels the tab of the figure
484 '''
486 return self.pylab(name=name, get='figure')
488 def axes(self, name=None):
489 '''
490 Returns a :py:class:`matplotlib.axes.Axes` instance.
492 :param name: labels the tab of axes
493 '''
495 return self.pylab(name=name, get='axes')
497 def figure_frame(self, name=None, figure_cls=None):
498 '''
499 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
501 :param name: labels the tab figure frame
502 '''
504 return self.pylab(name=name, get='figure_frame', figure_cls=figure_cls)
506 def smartplot_frame(self, name, *args, plot_cls=None, **kwargs):
507 '''
508 Create a :py:class:`pyrocko.gui.util.SmartplotFrame`.
510 :param name: labels the tab
511 :param *args, **kwargs:
512 passed to :py:class:`pyrocko.plot.smartplot.Plot`
513 :param plot_cls:
514 if given, subclass to be used instead of
515 :py:class:`pyrocko.plot.smartplot.Plot`
516 '''
517 frame = SmartplotFrame(
518 plot_args=args,
519 plot_cls=plot_cls,
520 plot_kwargs=kwargs)
522 self._panel_parent.add_tab(name, frame)
523 return frame
525 def pixmap_frame(self, filename=None, name=None):
526 '''
527 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
529 :param name: labels the tab
530 :param filename: name of file to be displayed
531 '''
533 f = PixmapFrame(filename)
535 scroll_area = qw.QScrollArea()
536 scroll_area.setWidget(f)
537 scroll_area.setWidgetResizable(True)
539 self._panel_parent.add_tab(name or 'Pixmap', scroll_area)
540 return f
542 def web_frame(self, url=None, name=None):
543 '''
544 Creates a :py:class:`WebKitFrame` which can be used as a browser
545 within snuffler.
547 :param url: url to open
548 :param name: labels the tab
549 '''
551 if name is None:
552 self._iplot += 1
553 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
555 f = WebKitFrame(url)
556 self._panel_parent.add_tab(name, f)
557 return f
559 def vtk_frame(self, name=None, actors=None):
560 '''
561 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
562 graphics.
564 :param actors: list of VTKActors
565 :param name: labels the tab
567 Initialize the interactive rendering by calling the frames'
568 :py:meth`initialize` method after having added all actors to the frames
569 renderer.
571 Requires installation of vtk including python wrapper.
572 '''
573 if name is None:
574 self._iplot += 1
575 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
577 try:
578 f = VTKFrame(actors=actors)
579 except ImportError as e:
580 self.fail(e)
582 self._panel_parent.add_tab(name, f)
583 return f
585 def tempdir(self):
586 '''
587 Create a temporary directory and return its absolute path.
589 The directory and all its contents are removed when the Snuffling
590 instance is deleted.
591 '''
593 if self._tempdir is None:
594 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
596 return self._tempdir
598 def set_live_update(self, live_update):
599 '''
600 Enable/disable live updating.
602 When live updates are enabled, the :py:meth:`call` method is called
603 whenever the user changes a parameter. If it is disabled, the user has
604 to initiate such a call manually by triggering the snuffling's menu
605 item or pressing the call button.
606 '''
608 self._live_update = live_update
609 if self._have_pre_process_hook:
610 self._pre_process_hook_enabled = live_update
611 if self._have_post_process_hook:
612 self._post_process_hook_enabled = live_update
614 try:
615 self.get_viewer().clean_update()
616 except NoViewerSet:
617 pass
619 def add_parameter(self, param):
620 '''
621 Add an adjustable parameter to the snuffling.
623 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
624 :py:class:`Choice`.
626 For each parameter added, controls are added to the snuffling's panel,
627 so that the parameter can be adjusted from the gui.
628 '''
630 self._parameters.append(param)
631 self._set_parameter_value(param.ident, param.default)
633 if self._panel is not None:
634 self.delete_gui()
635 self.setup_gui()
637 def add_trigger(self, name, method):
638 '''
639 Add a button to the snuffling's panel.
641 :param name: string that labels the button
642 :param method: method associated with the button
643 '''
645 self._triggers.append((name, method))
647 if self._panel is not None:
648 self.delete_gui()
649 self.setup_gui()
651 def get_parameters(self):
652 '''
653 Get the snuffling's adjustable parameter definitions.
655 Returns a list of objects of type :py:class:`Param`.
656 '''
658 return self._parameters
660 def get_parameter(self, ident):
661 '''
662 Get one of the snuffling's adjustable parameter definitions.
664 :param ident: identifier of the parameter
666 Returns an object of type :py:class:`Param` or ``None``.
667 '''
669 for param in self._parameters:
670 if param.ident == ident:
671 return param
672 return None
674 def set_parameter(self, ident, value):
675 '''
676 Set one of the snuffling's adjustable parameters.
678 :param ident: identifier of the parameter
679 :param value: new value of the parameter
681 Adjusts the control of a parameter without calling :py:meth:`call`.
682 '''
684 self._set_parameter_value(ident, value)
686 control = self._param_controls.get(ident, None)
687 if control:
688 control.set_value(value)
690 def set_parameter_range(self, ident, vmin, vmax):
691 '''
692 Set the range of one of the snuffling's adjustable parameters.
694 :param ident: identifier of the parameter
695 :param vmin,vmax: new minimum and maximum value for the parameter
697 Adjusts the control of a parameter without calling :py:meth:`call`.
698 '''
700 control = self._param_controls.get(ident, None)
701 if control:
702 control.set_range(vmin, vmax)
704 def set_parameter_choices(self, ident, choices):
705 '''
706 Update the choices of a Choice parameter.
708 :param ident: identifier of the parameter
709 :param choices: list of strings
710 '''
712 control = self._param_controls.get(ident, None)
713 if control:
714 selected_choice = control.set_choices(choices)
715 self._set_parameter_value(ident, selected_choice)
717 def _set_parameter_value(self, ident, value):
718 setattr(self, ident, value)
720 def get_parameter_value(self, ident):
721 '''
722 Get the current value of a parameter.
724 :param ident: identifier of the parameter
725 '''
726 return getattr(self, ident)
728 def get_settings(self):
729 '''
730 Returns a dictionary with identifiers of all parameters as keys and
731 their values as the dictionaries values.
732 '''
734 params = self.get_parameters()
735 settings = {}
736 for param in params:
737 settings[param.ident] = self.get_parameter_value(param.ident)
739 return settings
741 def set_settings(self, settings):
742 params = self.get_parameters()
743 dparams = dict([(param.ident, param) for param in params])
744 for k, v in settings.items():
745 if k in dparams:
746 self._set_parameter_value(k, v)
747 if k in self._param_controls:
748 control = self._param_controls[k]
749 control.set_value(v)
751 def get_viewer(self):
752 '''
753 Get the parent viewer.
755 Returns a reference to an object of type :py:class:`PileOverview`,
756 which is the main viewer widget.
758 If no gui has been initialized for the snuffling, a
759 :py:exc:`NoViewerSet` exception is raised.
760 '''
762 if self._viewer is None:
763 raise NoViewerSet()
764 return self._viewer
766 def get_pile(self):
767 '''
768 Get the pile.
770 If a gui has been initialized, a reference to the viewer's internal
771 pile is returned. If not, the :py:meth:`make_pile` method (which may be
772 overloaded in subclass) is called to create a pile. This can be
773 utilized to make hybrid snufflings, which may work also in a standalone
774 mode.
775 '''
777 try:
778 p = self.get_viewer().get_pile()
779 except NoViewerSet:
780 if self._no_viewer_pile is None:
781 self._no_viewer_pile = self.make_pile()
783 p = self._no_viewer_pile
785 return p
787 def get_active_event_and_stations(
788 self, trange=(-3600., 3600.), missing='warn'):
790 '''
791 Get event and stations with available data for active event.
793 :param trange: (begin, end), time range around event origin time to
794 query for available data
795 :param missing: string, what to do in case of missing station
796 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
798 :returns: ``(event, stations)``
799 '''
801 p = self.get_pile()
802 v = self.get_viewer()
804 event = v.get_active_event()
805 if event is None:
806 self.fail(
807 'No active event set. Select an event and press "e" to make '
808 'it the "active event"')
810 stations = {}
811 for traces in p.chopper(
812 event.time+trange[0],
813 event.time+trange[1],
814 load_data=False,
815 degap=False):
817 for tr in traces:
818 try:
819 for skey in v.station_keys(tr):
820 if skey in stations:
821 continue
823 station = v.get_station(skey)
824 stations[skey] = station
826 except KeyError:
827 s = 'No station information for station key "%s".' \
828 % '.'.join(skey)
830 if missing == 'warn':
831 logger.warning(s)
832 elif missing == 'raise':
833 raise MissingStationInformation(s)
834 elif missing == 'ignore':
835 pass
836 else:
837 assert False, 'invalid argument to "missing"'
839 stations[skey] = None
841 return event, list(set(
842 st for st in stations.values() if st is not None))
844 def get_stations(self):
845 '''
846 Get all stations known to the viewer.
847 '''
849 v = self.get_viewer()
850 stations = list(v.stations.values())
851 return stations
853 def get_markers(self):
854 '''
855 Get all markers from the viewer.
856 '''
858 return self.get_viewer().get_markers()
860 def get_event_markers(self):
861 '''
862 Get all event markers from the viewer.
863 '''
865 return [m for m in self.get_viewer().get_markers()
866 if isinstance(m, EventMarker)]
868 def get_selected_markers(self):
869 '''
870 Get all selected markers from the viewer.
871 '''
873 return self.get_viewer().selected_markers()
875 def get_selected_event_markers(self):
876 '''
877 Get all selected event markers from the viewer.
878 '''
880 return [m for m in self.get_viewer().selected_markers()
881 if isinstance(m, EventMarker)]
883 def get_active_event_and_phase_markers(self):
884 '''
885 Get the marker of the active event and any associated phase markers
886 '''
888 viewer = self.get_viewer()
889 markers = viewer.get_markers()
890 event_marker = viewer.get_active_event_marker()
891 if event_marker is None:
892 self.fail(
893 'No active event set. '
894 'Select an event and press "e" to make it the "active event"')
896 event = event_marker.get_event()
898 selection = []
899 for m in markers:
900 if isinstance(m, PhaseMarker):
901 if m.get_event() is event:
902 selection.append(m)
904 return (
905 event_marker,
906 [m for m in markers if isinstance(m, PhaseMarker) and
907 m.get_event() == event])
909 def get_viewer_trace_selector(self, mode='inview'):
910 '''
911 Get currently active trace selector from viewer.
913 :param mode: set to ``'inview'`` (default) to only include selections
914 currently shown in the viewer, ``'visible' to include all traces
915 not currenly hidden by hide or quick-select commands, or ``'all'``
916 to disable any restrictions.
917 '''
919 viewer = self.get_viewer()
921 def rtrue(tr):
922 return True
924 if mode == 'inview':
925 return viewer.trace_selector or rtrue
926 elif mode == 'visible':
927 return viewer.trace_filter or rtrue
928 elif mode == 'all':
929 return rtrue
930 else:
931 raise Exception('invalid mode argument')
933 def chopper_selected_traces(self, fallback=False, marker_selector=None,
934 mode='inview', main_bandpass=False,
935 progress=None, responsive=False,
936 *args, **kwargs):
937 '''
938 Iterate over selected traces.
940 Shortcut to get all trace data contained in the selected markers in the
941 running snuffler. For each selected marker,
942 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
943 *tmin*, *tmax*, and *trace_selector* set to values according to the
944 marker. Additional arguments to the chopper are handed over from
945 *\\*args* and *\\*\\*kwargs*.
947 :param fallback:
948 If ``True``, if no selection has been marked, use the content
949 currently visible in the viewer.
951 :param marker_selector:
952 If not ``None`` a callback to filter markers.
954 :param mode:
955 Set to ``'inview'`` (default) to only include selections currently
956 shown in the viewer (excluding traces accessible through vertical
957 scrolling), ``'visible'`` to include all traces not currently
958 hidden by hide or quick-select commands (including traces
959 accessible through vertical scrolling), or ``'all'`` to disable any
960 restrictions.
962 :param main_bandpass:
963 If ``True``, apply main control high- and lowpass filters to
964 traces. Note: use with caution. Processing is fixed to use 4th
965 order Butterworth highpass and lowpass and the signal is always
966 demeaned before filtering. FFT filtering, rotation, demean and
967 bandpass settings from the graphical interface are not respected
968 here. Padding is not automatically adjusted so results may include
969 artifacts.
971 :param progress:
972 If given a string a progress bar is shown to the user. The string
973 is used as the label for the progress bar.
975 :param responsive:
976 If set to ``True``, occasionally allow UI events to be processed.
977 If used in combination with ``progress``, this allows the iterator
978 to be aborted by the user.
979 '''
981 try:
982 viewer = self.get_viewer()
983 markers = [
984 m for m in viewer.selected_markers()
985 if not isinstance(m, EventMarker)]
987 if marker_selector is not None:
988 markers = [
989 marker for marker in markers if marker_selector(marker)]
991 pile = self.get_pile()
993 def rtrue(tr):
994 return True
996 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
997 trace_selector_viewer = self.get_viewer_trace_selector(mode)
999 style_arg = kwargs.pop('style', None)
1001 if main_bandpass:
1002 def apply_filters(traces):
1003 for tr in traces:
1004 if viewer.highpass is not None:
1005 tr.highpass(4, viewer.highpass)
1006 if viewer.lowpass is not None:
1007 tr.lowpass(4, viewer.lowpass)
1008 return traces
1009 else:
1010 def apply_filters(traces):
1011 return traces
1013 pb = viewer.parent().get_progressbars()
1015 time_last = [time.time()]
1017 def update_progress(label, batch):
1018 time_now = time.time()
1019 if responsive:
1020 # start processing events with one second delay, so that
1021 # e.g. cleanup actions at startup do not cause track number
1022 # changes etc.
1023 if time_last[0] + 1. < time_now:
1024 qw.qApp.processEvents()
1025 else:
1026 # redraw about once a second
1027 if time_last[0] + 1. < time_now:
1028 viewer.repaint()
1030 time_last[0] = time.time() # use time after drawing
1032 abort = pb.set_status(
1033 label, batch.i*100./batch.n, responsive)
1034 abort |= viewer.window().is_closing()
1036 return abort
1038 if markers:
1039 for imarker, marker in enumerate(markers):
1040 try:
1041 if progress:
1042 label = '%s: %i/%i' % (
1043 progress, imarker+1, len(markers))
1045 pb.set_status(label, 0, responsive)
1047 if not marker.nslc_ids:
1048 trace_selector_marker = rtrue
1049 else:
1050 def trace_selector_marker(tr):
1051 return marker.match_nslc(tr.nslc_id)
1053 def trace_selector(tr):
1054 return trace_selector_arg(tr) \
1055 and trace_selector_viewer(tr) \
1056 and trace_selector_marker(tr)
1058 for batch in pile.chopper(
1059 tmin=marker.tmin,
1060 tmax=marker.tmax,
1061 trace_selector=trace_selector,
1062 style='batch',
1063 *args,
1064 **kwargs):
1066 if progress:
1067 abort = update_progress(label, batch)
1068 if abort:
1069 return
1071 batch.traces = apply_filters(batch.traces)
1072 if style_arg == 'batch':
1073 yield batch
1074 else:
1075 yield batch.traces
1077 finally:
1078 if progress:
1079 pb.set_status(label, 100., responsive)
1081 elif fallback:
1082 def trace_selector(tr):
1083 return trace_selector_arg(tr) \
1084 and trace_selector_viewer(tr)
1086 tmin, tmax = viewer.get_time_range()
1088 if not pile.is_empty():
1089 ptmin = pile.get_tmin()
1090 tpad = kwargs.get('tpad', 0.0)
1091 if ptmin > tmin:
1092 tmin = ptmin + tpad
1093 ptmax = pile.get_tmax()
1094 if ptmax < tmax:
1095 tmax = ptmax - tpad
1097 try:
1098 if progress:
1099 label = progress
1100 pb.set_status(label, 0, responsive)
1102 for batch in pile.chopper(
1103 tmin=tmin,
1104 tmax=tmax,
1105 trace_selector=trace_selector,
1106 style='batch',
1107 *args,
1108 **kwargs):
1110 if progress:
1111 abort = update_progress(label, batch)
1113 if abort:
1114 return
1116 batch.traces = apply_filters(batch.traces)
1118 if style_arg == 'batch':
1119 yield batch
1120 else:
1121 yield batch.traces
1123 finally:
1124 if progress:
1125 pb.set_status(label, 100., responsive)
1127 else:
1128 raise NoTracesSelected()
1130 except NoViewerSet:
1131 pile = self.get_pile()
1132 return pile.chopper(*args, **kwargs)
1134 def get_selected_time_range(self, fallback=False):
1135 '''
1136 Get the time range spanning all selected markers.
1138 :param fallback: if ``True`` and no marker is selected return begin and
1139 end of visible time range
1140 '''
1142 viewer = self.get_viewer()
1143 markers = viewer.selected_markers()
1144 mins = [marker.tmin for marker in markers]
1145 maxs = [marker.tmax for marker in markers]
1147 if mins and maxs:
1148 tmin = min(mins)
1149 tmax = max(maxs)
1151 elif fallback:
1152 tmin, tmax = viewer.get_time_range()
1154 else:
1155 raise NoTracesSelected()
1157 return tmin, tmax
1159 def panel_visibility_changed(self, bool):
1160 '''
1161 Called when the snuffling's panel becomes visible or is hidden.
1163 Can be overloaded in subclass, e.g. to perform additional setup actions
1164 when the panel is activated the first time.
1165 '''
1167 pass
1169 def make_pile(self):
1170 '''
1171 Create a pile.
1173 To be overloaded in subclass. The default implementation just calls
1174 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1175 arguments.
1176 '''
1178 cachedirname = config.config().cache_dir
1179 sources = self._cli_params.get('sources', sys.argv[1:])
1180 return pile.make_pile(
1181 sources,
1182 cachedirname=cachedirname,
1183 regex=self._cli_params['regex'],
1184 fileformat=self._cli_params['format'])
1186 def make_panel(self, parent):
1187 '''
1188 Create a widget for the snuffling's control panel.
1190 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1191 if no panel is needed (e.g. if the snuffling has no adjustable
1192 parameters).
1193 '''
1195 params = self.get_parameters()
1196 self._param_controls = {}
1197 if params or self._force_panel:
1198 sarea = MyScrollArea(parent.get_panel_parent_widget())
1199 sarea.setFrameStyle(qw.QFrame.NoFrame)
1200 sarea.setSizePolicy(qw.QSizePolicy(
1201 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1202 frame = MyFrame(sarea)
1203 frame.widgetVisibilityChanged.connect(
1204 self.panel_visibility_changed)
1206 frame.setSizePolicy(qw.QSizePolicy(
1207 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1208 frame.setFrameStyle(qw.QFrame.NoFrame)
1209 sarea.setWidget(frame)
1210 sarea.setWidgetResizable(True)
1211 layout = qw.QGridLayout()
1212 layout.setContentsMargins(0, 0, 0, 0)
1213 layout.setSpacing(0)
1214 frame.setLayout(layout)
1216 parlayout = qw.QGridLayout()
1218 irow = 0
1219 ipar = 0
1220 have_switches = False
1221 have_params = False
1222 for iparam, param in enumerate(params):
1223 if isinstance(param, Param):
1224 if param.minimum <= 0.0:
1225 param_control = LinValControl(
1226 high_is_none=param.high_is_none,
1227 low_is_none=param.low_is_none,
1228 type=param.type)
1229 else:
1230 param_control = ValControl(
1231 high_is_none=param.high_is_none,
1232 low_is_none=param.low_is_none,
1233 low_is_zero=param.low_is_zero,
1234 type=param.type)
1236 param_control.setup(
1237 param.name,
1238 param.minimum,
1239 param.maximum,
1240 param.default,
1241 iparam)
1243 param_control.set_tracking(param.tracking)
1244 param_control.valchange.connect(
1245 self.modified_snuffling_panel)
1247 self._param_controls[param.ident] = param_control
1248 for iw, w in enumerate(param_control.widgets()):
1249 parlayout.addWidget(w, ipar, iw)
1251 ipar += 1
1252 have_params = True
1254 elif isinstance(param, Choice):
1255 param_widget = ChoiceControl(
1256 param.ident, param.default, param.choices, param.name)
1257 param_widget.choosen.connect(
1258 self.choose_on_snuffling_panel)
1260 self._param_controls[param.ident] = param_widget
1261 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1262 ipar += 1
1263 have_params = True
1265 elif isinstance(param, Switch):
1266 have_switches = True
1268 if have_params:
1269 parframe = qw.QFrame(sarea)
1270 parframe.setSizePolicy(qw.QSizePolicy(
1271 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1272 parframe.setLayout(parlayout)
1273 layout.addWidget(parframe, irow, 0)
1274 irow += 1
1276 if have_switches:
1277 swlayout = qw.QGridLayout()
1278 isw = 0
1279 for iparam, param in enumerate(params):
1280 if isinstance(param, Switch):
1281 param_widget = SwitchControl(
1282 param.ident, param.default, param.name)
1283 param_widget.sw_toggled.connect(
1284 self.switch_on_snuffling_panel)
1286 self._param_controls[param.ident] = param_widget
1287 swlayout.addWidget(param_widget, isw//10, isw % 10)
1288 isw += 1
1290 swframe = qw.QFrame(sarea)
1291 swframe.setSizePolicy(qw.QSizePolicy(
1292 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1293 swframe.setLayout(swlayout)
1294 layout.addWidget(swframe, irow, 0)
1295 irow += 1
1297 butframe = qw.QFrame(sarea)
1298 butframe.setSizePolicy(qw.QSizePolicy(
1299 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1300 butlayout = qw.QHBoxLayout()
1301 butframe.setLayout(butlayout)
1303 live_update_checkbox = qw.QCheckBox('Auto-Run')
1304 if self._live_update:
1305 live_update_checkbox.setCheckState(qc.Qt.Checked)
1307 butlayout.addWidget(live_update_checkbox)
1308 live_update_checkbox.toggled.connect(
1309 self.live_update_toggled)
1311 help_button = qw.QPushButton('Help')
1312 butlayout.addWidget(help_button)
1313 help_button.clicked.connect(
1314 self.help_button_triggered)
1316 clear_button = qw.QPushButton('Clear')
1317 butlayout.addWidget(clear_button)
1318 clear_button.clicked.connect(
1319 self.clear_button_triggered)
1321 call_button = qw.QPushButton('Run')
1322 butlayout.addWidget(call_button)
1323 call_button.clicked.connect(
1324 self.call_button_triggered)
1326 for name, method in self._triggers:
1327 but = qw.QPushButton(name)
1329 def call_and_update(method):
1330 def f():
1331 self.check_call(method)
1332 self.get_viewer().update()
1333 return f
1335 but.clicked.connect(
1336 call_and_update(method))
1338 butlayout.addWidget(but)
1340 layout.addWidget(butframe, irow, 0)
1342 irow += 1
1343 spacer = qw.QSpacerItem(
1344 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1346 layout.addItem(spacer, irow, 0)
1348 return sarea
1350 else:
1351 return None
1353 def make_helpmenuitem(self, parent):
1354 '''
1355 Create the help menu item for the snuffling.
1356 '''
1358 item = qw.QAction(self.get_name(), None)
1360 item.triggered.connect(
1361 self.help_button_triggered)
1363 return item
1365 def make_menuitem(self, parent):
1366 '''
1367 Create the menu item for the snuffling.
1369 This method may be overloaded in subclass and return ``None``, if no
1370 menu entry is wanted.
1371 '''
1373 item = qw.QAction(self.get_name(), None)
1374 item.setCheckable(
1375 self._have_pre_process_hook or self._have_post_process_hook)
1377 item.triggered.connect(
1378 self.menuitem_triggered)
1380 return item
1382 def output_filename(
1383 self,
1384 caption='Save File',
1385 dir='',
1386 filter='',
1387 selected_filter=None):
1389 '''
1390 Query user for an output filename.
1392 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1393 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1394 dialog.
1395 '''
1397 if not dir and self._previous_output_filename:
1398 dir = self._previous_output_filename
1400 fn = getSaveFileName(
1401 self.get_viewer(), caption, dir, filter, selected_filter)
1402 if not fn:
1403 raise UserCancelled()
1405 self._previous_output_filename = fn
1406 return str(fn)
1408 def input_directory(self, caption='Open Directory', dir=''):
1409 '''
1410 Query user for an input directory.
1412 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1413 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1414 dialog.
1415 '''
1417 if not dir and self._previous_input_directory:
1418 dir = self._previous_input_directory
1420 dn = qw.QFileDialog.getExistingDirectory(
1421 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1423 if not dn:
1424 raise UserCancelled()
1426 self._previous_input_directory = dn
1427 return str(dn)
1429 def input_filename(self, caption='Open File', dir='', filter='',
1430 selected_filter=None):
1431 '''
1432 Query user for an input filename.
1434 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1435 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1436 dialog.
1437 '''
1439 if not dir and self._previous_input_filename:
1440 dir = self._previous_input_filename
1442 fn, _ = qw.QFileDialog.getOpenFileName(
1443 self.get_viewer(),
1444 caption,
1445 dir,
1446 filter)
1448 if not fn:
1449 raise UserCancelled()
1451 self._previous_input_filename = fn
1452 return str(fn)
1454 def input_dialog(self, caption='', request='', directory=False):
1455 '''
1456 Query user for a text input.
1458 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1459 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1460 dialog.
1461 '''
1463 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1465 if not ok:
1466 raise UserCancelled()
1468 return inp
1470 def modified_snuffling_panel(self, value, iparam):
1471 '''
1472 Called when the user has played with an adjustable parameter.
1474 The default implementation sets the parameter, calls the snuffling's
1475 :py:meth:`call` method and finally triggers an update on the viewer
1476 widget.
1477 '''
1479 param = self.get_parameters()[iparam]
1480 self._set_parameter_value(param.ident, value)
1481 if self._live_update:
1482 self.check_call(self.call)
1483 self.get_viewer().update()
1485 def switch_on_snuffling_panel(self, ident, state):
1486 '''
1487 Called when the user has toggled a switchable parameter.
1488 '''
1490 self._set_parameter_value(ident, state)
1491 if self._live_update:
1492 self.check_call(self.call)
1493 self.get_viewer().update()
1495 def choose_on_snuffling_panel(self, ident, state):
1496 '''
1497 Called when the user has made a choice about a choosable parameter.
1498 '''
1500 self._set_parameter_value(ident, state)
1501 if self._live_update:
1502 self.check_call(self.call)
1503 self.get_viewer().update()
1505 def menuitem_triggered(self, arg):
1506 '''
1507 Called when the user has triggered the snuffling's menu.
1509 The default implementation calls the snuffling's :py:meth:`call` method
1510 and triggers an update on the viewer widget.
1511 '''
1513 self.check_call(self.call)
1515 if self._have_pre_process_hook:
1516 self._pre_process_hook_enabled = arg
1518 if self._have_post_process_hook:
1519 self._post_process_hook_enabled = arg
1521 if self._have_pre_process_hook or self._have_post_process_hook:
1522 self.get_viewer().clean_update()
1523 else:
1524 self.get_viewer().update()
1526 def call_button_triggered(self):
1527 '''
1528 Called when the user has clicked the snuffling's call button.
1530 The default implementation calls the snuffling's :py:meth:`call` method
1531 and triggers an update on the viewer widget.
1532 '''
1534 self.check_call(self.call)
1535 self.get_viewer().update()
1537 def clear_button_triggered(self):
1538 '''
1539 Called when the user has clicked the snuffling's clear button.
1541 This calls the :py:meth:`cleanup` method and triggers an update on the
1542 viewer widget.
1543 '''
1545 self.cleanup()
1546 self.get_viewer().update()
1548 def help_button_triggered(self):
1549 '''
1550 Creates a :py:class:`QLabel` which contains the documentation as
1551 given in the snufflings' __doc__ string.
1552 '''
1554 if self.__doc__:
1555 if self.__doc__.strip().startswith('<html>'):
1556 doc = qw.QLabel(self.__doc__)
1557 else:
1558 try:
1559 import markdown
1560 doc = qw.QLabel(markdown.markdown(self.__doc__))
1562 except ImportError:
1563 logger.error(
1564 'Install Python module "markdown" for pretty help '
1565 'formatting.')
1567 doc = qw.QLabel(self.__doc__)
1568 else:
1569 doc = qw.QLabel('This snuffling does not provide any online help.')
1571 labels = [doc]
1573 if self._filename:
1574 from html import escape
1576 code = open(self._filename, 'r').read()
1578 doc_src = qw.QLabel(
1579 '''<html><body>
1580<hr />
1581<center><em>May the source be with you, young Skywalker!</em><br /><br />
1582<a href="file://%s"><code>%s</code></a></center>
1583<br />
1584<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1585<pre style="white-space: pre-wrap"><code>%s
1586</code></pre></p></body></html>'''
1587 % (
1588 quote(self._filename),
1589 escape(self._filename),
1590 escape(code)))
1592 labels.append(doc_src)
1594 for h in labels:
1595 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1596 h.setWordWrap(True)
1598 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1600 def live_update_toggled(self, on):
1601 '''
1602 Called when the checkbox for live-updates has been toggled.
1603 '''
1605 self.set_live_update(on)
1607 def add_traces(self, traces):
1608 '''
1609 Add traces to the viewer.
1611 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1613 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1614 added to the viewer's internal pile for display. Note, that unlike with
1615 the traces from the files given on the command line, these traces are
1616 kept in memory and so may quickly occupy a lot of ram if a lot of
1617 traces are added.
1619 This method should be preferred over modifying the viewer's internal
1620 pile directly, because this way, the snuffling has a chance to
1621 automatically remove its private traces again (see :py:meth:`cleanup`
1622 method).
1623 '''
1625 ticket = self.get_viewer().add_traces(traces)
1626 self._tickets.append(ticket)
1627 return ticket
1629 def add_trace(self, tr):
1630 '''
1631 Add a trace to the viewer.
1633 See :py:meth:`add_traces`.
1634 '''
1636 self.add_traces([tr])
1638 def add_markers(self, markers):
1639 '''
1640 Add some markers to the display.
1642 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1643 adds these to the viewer.
1644 '''
1646 self.get_viewer().add_markers(markers)
1647 self._markers.extend(markers)
1649 def add_marker(self, marker):
1650 '''
1651 Add a marker to the display.
1653 See :py:meth:`add_markers`.
1654 '''
1656 self.add_markers([marker])
1658 def cleanup(self):
1659 '''
1660 Remove all traces and markers which have been added so far by the
1661 snuffling.
1662 '''
1664 try:
1665 viewer = self.get_viewer()
1666 viewer.release_data(self._tickets)
1667 viewer.remove_markers(self._markers)
1669 except NoViewerSet:
1670 pass
1672 self._tickets = []
1673 self._markers = []
1675 def check_call(self, method):
1677 if method in self._call_in_progress:
1678 self.show_message('error', 'Previous action still in progress.')
1679 return
1681 try:
1682 self._call_in_progress[method] = True
1683 method()
1684 return 0
1686 except SnufflingError as e:
1687 if not isinstance(e, SnufflingCallFailed):
1688 # those have logged within error()
1689 logger.error('%s: %s' % (self._name, e))
1690 logger.error('%s: Snuffling action failed' % self._name)
1691 return 1
1693 except Exception as e:
1694 message = '%s: Snuffling action raised an exception: %s' % (
1695 self._name, str(e))
1697 logger.exception(message)
1698 self.show_message('error', message)
1700 finally:
1701 del self._call_in_progress[method]
1703 def call(self):
1704 '''
1705 Main work routine of the snuffling.
1707 This method is called when the snuffling's menu item has been triggered
1708 or when the user has played with the panel controls. To be overloaded
1709 in subclass. The default implementation does nothing useful.
1710 '''
1712 pass
1714 def pre_process_hook(self, traces):
1715 return traces
1717 def post_process_hook(self, traces):
1718 return traces
1720 def get_tpad(self):
1721 '''
1722 Return current amount of extra padding needed by live processing hooks.
1723 '''
1725 return 0.0
1727 def pre_destroy(self):
1728 '''
1729 Called when the snuffling instance is about to be deleted.
1731 Can be overloaded to do user-defined cleanup actions. The
1732 default implementation calls :py:meth:`cleanup` and deletes
1733 the snuffling`s tempory directory, if needed.
1734 '''
1736 self.cleanup()
1737 if self._tempdir is not None:
1738 import shutil
1739 shutil.rmtree(self._tempdir)
1742class SnufflingError(Exception):
1743 pass
1746class NoViewerSet(SnufflingError):
1747 '''
1748 This exception is raised, when no viewer has been set on a Snuffling.
1749 '''
1751 def __str__(self):
1752 return 'No GUI available. ' \
1753 'Maybe this Snuffling cannot be run in command line mode?'
1756class MissingStationInformation(SnufflingError):
1757 '''
1758 Raised when station information is missing.
1759 '''
1762class NoTracesSelected(SnufflingError):
1763 '''
1764 This exception is raised, when no traces have been selected in the viewer
1765 and we cannot fallback to using the current view.
1766 '''
1768 def __str__(self):
1769 return 'No traces have been selected / are available.'
1772class UserCancelled(SnufflingError):
1773 '''
1774 This exception is raised, when the user has cancelled a snuffling dialog.
1775 '''
1777 def __str__(self):
1778 return 'The user has cancelled a dialog.'
1781class SnufflingCallFailed(SnufflingError):
1782 '''
1783 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1784 :py:meth:`Snuffling.call`.
1785 '''
1788class InvalidSnufflingFilename(Exception):
1789 pass
1792class SnufflingModule(object):
1793 '''
1794 Utility class to load/reload snufflings from a file.
1796 The snufflings are created by user modules which have the special function
1797 :py:func:`__snufflings__` which return the snuffling instances to be
1798 exported. The snuffling module is attached to a handler class, which makes
1799 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1800 from ``pile_viewer.py``). The handler class must implement the methods
1801 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1802 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1803 :py:meth:`remove_snufflings` which may be called from the handler class,
1804 when needed.
1805 '''
1807 mtimes = {}
1809 def __init__(self, path, name, handler):
1810 self._path = path
1811 self._name = name
1812 self._mtime = None
1813 self._module = None
1814 self._snufflings = []
1815 self._handler = handler
1817 def load_if_needed(self):
1818 filename = os.path.join(self._path, self._name+'.py')
1820 try:
1821 mtime = os.stat(filename)[8]
1822 except OSError as e:
1823 if e.errno == 2:
1824 logger.error(e)
1825 raise BrokenSnufflingModule(filename)
1827 if self._module is None:
1828 sys.path[0:0] = [self._path]
1829 try:
1830 logger.debug('Loading snuffling module %s' % filename)
1831 if self._name in sys.modules:
1832 raise InvalidSnufflingFilename(self._name)
1834 self._module = __import__(self._name)
1835 del sys.modules[self._name]
1837 for snuffling in self._module.__snufflings__():
1838 snuffling._filename = filename
1839 self.add_snuffling(snuffling)
1841 except Exception:
1842 logger.error(traceback.format_exc())
1843 raise BrokenSnufflingModule(filename)
1845 finally:
1846 sys.path[0:1] = []
1848 elif self._mtime != mtime:
1849 logger.warning('Reloading snuffling module %s' % filename)
1850 settings = self.remove_snufflings()
1851 sys.path[0:0] = [self._path]
1852 try:
1854 sys.modules[self._name] = self._module
1856 reload(self._module)
1857 del sys.modules[self._name]
1859 for snuffling in self._module.__snufflings__():
1860 snuffling._filename = filename
1861 self.add_snuffling(snuffling, reloaded=True)
1863 if len(self._snufflings) == len(settings):
1864 for sett, snuf in zip(settings, self._snufflings):
1865 snuf.set_settings(sett)
1867 except Exception:
1868 logger.error(traceback.format_exc())
1869 raise BrokenSnufflingModule(filename)
1871 finally:
1872 sys.path[0:1] = []
1874 self._mtime = mtime
1876 def add_snuffling(self, snuffling, reloaded=False):
1877 snuffling._path = self._path
1878 snuffling.setup()
1879 self._snufflings.append(snuffling)
1880 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1882 def remove_snufflings(self):
1883 settings = []
1884 for snuffling in self._snufflings:
1885 settings.append(snuffling.get_settings())
1886 self._handler.remove_snuffling(snuffling)
1888 self._snufflings = []
1889 return settings
1892class BrokenSnufflingModule(Exception):
1893 pass
1896class MyScrollArea(qw.QScrollArea):
1898 def sizeHint(self):
1899 s = qc.QSize()
1900 s.setWidth(self.widget().sizeHint().width())
1901 s.setHeight(self.widget().sizeHint().height())
1902 return s
1905class SwitchControl(qw.QCheckBox):
1906 sw_toggled = qc.pyqtSignal(object, bool)
1908 def __init__(self, ident, default, *args):
1909 qw.QCheckBox.__init__(self, *args)
1910 self.ident = ident
1911 self.setChecked(default)
1912 self.toggled.connect(self._sw_toggled)
1914 def _sw_toggled(self, state):
1915 self.sw_toggled.emit(self.ident, state)
1917 def set_value(self, state):
1918 self.blockSignals(True)
1919 self.setChecked(state)
1920 self.blockSignals(False)
1923class ChoiceControl(qw.QFrame):
1924 choosen = qc.pyqtSignal(object, object)
1926 def __init__(self, ident, default, choices, name, *args):
1927 qw.QFrame.__init__(self, *args)
1928 self.label = qw.QLabel(name, self)
1929 self.label.setMinimumWidth(120)
1930 self.cbox = qw.QComboBox(self)
1931 self.layout = qw.QHBoxLayout(self)
1932 self.layout.addWidget(self.label)
1933 self.layout.addWidget(self.cbox)
1934 self.layout.setContentsMargins(0, 0, 0, 0)
1935 self.layout.setSpacing(0)
1936 self.ident = ident
1937 self.choices = choices
1938 for ichoice, choice in enumerate(choices):
1939 self.cbox.addItem(choice)
1941 self.set_value(default)
1942 self.cbox.activated.connect(self.emit_choosen)
1944 def set_choices(self, choices):
1945 icur = self.cbox.currentIndex()
1946 if icur != -1:
1947 selected_choice = choices[icur]
1948 else:
1949 selected_choice = None
1951 self.choices = choices
1952 self.cbox.clear()
1953 for ichoice, choice in enumerate(choices):
1954 self.cbox.addItem(qc.QString(choice))
1956 if selected_choice is not None and selected_choice in choices:
1957 self.set_value(selected_choice)
1958 return selected_choice
1959 else:
1960 self.set_value(choices[0])
1961 return choices[0]
1963 def emit_choosen(self, i):
1964 self.choosen.emit(
1965 self.ident,
1966 self.choices[i])
1968 def set_value(self, v):
1969 self.cbox.blockSignals(True)
1970 for i, choice in enumerate(self.choices):
1971 if choice == v:
1972 self.cbox.setCurrentIndex(i)
1973 self.cbox.blockSignals(False)