1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Snuffling infrastructure
8This module provides the base class :py:class:`Snuffling` for user-defined
9snufflings and some utilities for their handling.
10'''
11from __future__ import absolute_import
13import os
14import sys
15import time
16import logging
17import traceback
18import tempfile
20from .qt_compat import qc, qw, getSaveFileName, use_pyqt5
22from pyrocko import pile, config
23from pyrocko.util import quote
25from .util import (ValControl, LinValControl, FigureFrame, WebKitFrame,
26 VTKFrame, PixmapFrame, Marker, EventMarker, PhaseMarker,
27 load_markers, save_markers)
30if sys.version_info >= (3, 0):
31 from importlib import reload
34Marker, load_markers, save_markers # noqa
36logger = logging.getLogger('pyrocko.gui.snuffling')
39def fnpatch(x):
40 if use_pyqt5:
41 return x
42 else:
43 return x, None
46class MyFrame(qw.QFrame):
47 widgetVisibilityChanged = qc.pyqtSignal(bool)
49 def showEvent(self, ev):
50 self.widgetVisibilityChanged.emit(True)
52 def hideEvent(self, ev):
53 self.widgetVisibilityChanged.emit(False)
56class Param(object):
57 '''
58 Definition of an adjustable floating point parameter for the
59 snuffling. The snuffling may display controls for user input for
60 such parameters.
62 :param name: labels the parameter on the snuffling's control panel
63 :param ident: identifier of the parameter
64 :param default: default value
65 :param minimum: minimum value for the parameter
66 :param maximum: maximum value for the parameter
67 :param low_is_none: if ``True``: parameter is set to None at lowest value
68 of parameter range (optional)
69 :param high_is_none: if ``True``: parameter is set to None at highest value
70 of parameter range (optional)
71 :param low_is_zero: if ``True``: parameter is set to value 0 at lowest
72 value of parameter range (optional)
73 '''
75 def __init__(
76 self, name, ident, default, minimum, maximum,
77 low_is_none=None,
78 high_is_none=None,
79 low_is_zero=False,
80 tracking=True):
82 if low_is_none and default == minimum:
83 default = None
84 if high_is_none and default == maximum:
85 default = None
87 self.name = name
88 self.ident = ident
89 self.default = default
90 self.minimum = minimum
91 self.maximum = maximum
92 self.low_is_none = low_is_none
93 self.high_is_none = high_is_none
94 self.low_is_zero = low_is_zero
95 self.tracking = tracking
97 self.type = type(default)
98 self._control = None
101class Switch(object):
102 '''
103 Definition of a boolean switch for the snuffling. The snuffling
104 may display a checkbox for such a switch.
106 :param name: labels the switch on the snuffling's control panel
107 :param ident: identifier of the parameter
108 :param default: default value
109 '''
111 def __init__(self, name, ident, default):
112 self.name = name
113 self.ident = ident
114 self.default = default
117class Choice(object):
118 '''
119 Definition of a string choice for the snuffling. The snuffling
120 may display a menu for such a choice.
122 :param name: labels the menu on the snuffling's control panel
123 :param ident: identifier of the parameter
124 :param default: default value
125 :param choices: tuple of other options
126 '''
128 def __init__(self, name, ident, default, choices):
129 self.name = name
130 self.ident = ident
131 self.default = default
132 self.choices = choices
135class Snuffling(object):
136 '''
137 Base class for user snufflings.
139 Snufflings are plugins for snuffler (and other applications using the
140 :py:class:`pyrocko.pile_viewer.PileOverview` class defined in
141 ``pile_viewer.py``). They can be added, removed and reloaded at runtime and
142 should provide a simple way of extending the functionality of snuffler.
144 A snuffling has access to all data available in a pile viewer, can process
145 this data and can create and add new traces and markers to the viewer.
146 '''
148 def __init__(self):
149 self._path = None
151 self._name = 'Untitled Snuffling'
152 self._viewer = None
153 self._tickets = []
154 self._markers = []
156 self._delete_panel = None
157 self._delete_menuitem = None
159 self._panel_parent = None
160 self._menu_parent = None
162 self._panel = None
163 self._menuitem = None
164 self._helpmenuitem = None
165 self._parameters = []
166 self._param_controls = {}
168 self._triggers = []
170 self._live_update = True
171 self._previous_output_filename = None
172 self._previous_input_filename = None
173 self._previous_input_directory = None
175 self._tempdir = None
176 self._iplot = 0
178 self._have_pre_process_hook = False
179 self._have_post_process_hook = False
180 self._pre_process_hook_enabled = False
181 self._post_process_hook_enabled = False
183 self._no_viewer_pile = None
184 self._cli_params = {}
185 self._filename = None
186 self._force_panel = False
187 self._call_in_progress = False
189 def setup(self):
190 '''
191 Setup the snuffling.
193 This method should be implemented in subclass and contain e.g. calls to
194 :py:meth:`set_name` and :py:meth:`add_parameter`.
195 '''
197 pass
199 def module_dir(self):
200 '''
201 Returns the path of the directory where snufflings are stored.
203 The default path is ``$HOME/.snufflings``.
204 '''
206 return self._path
208 def init_gui(self, viewer, panel_parent, menu_parent, reloaded=False):
209 '''
210 Set parent viewer and hooks to add panel and menu entry.
212 This method is called from the
213 :py:class:`pyrocko.pile_viewer.PileOverview` object. Calls
214 :py:meth:`setup_gui`.
215 '''
217 self._viewer = viewer
218 self._panel_parent = panel_parent
219 self._menu_parent = menu_parent
221 self.setup_gui(reloaded=reloaded)
223 def setup_gui(self, reloaded=False):
224 '''
225 Create and add gui elements to the viewer.
227 This method is initially called from :py:meth:`init_gui`. It is also
228 called, e.g. when new parameters have been added or if the name of the
229 snuffling has been changed.
230 '''
232 if self._panel_parent is not None:
233 self._panel = self.make_panel(self._panel_parent)
234 if self._panel:
235 self._panel_parent.add_panel(
236 self.get_name(), self._panel, reloaded)
238 if self._menu_parent is not None:
239 self._menuitem = self.make_menuitem(self._menu_parent)
240 self._helpmenuitem = self.make_helpmenuitem(self._menu_parent)
241 if self._menuitem:
242 self._menu_parent.add_snuffling_menuitem(self._menuitem)
244 if self._helpmenuitem:
245 self._menu_parent.add_snuffling_help_menuitem(
246 self._helpmenuitem)
248 def set_force_panel(self, bool=True):
249 '''
250 Force to create a panel.
252 :param bool: if ``True`` will create a panel with Help, Clear and Run
253 button.
254 '''
256 self._force_panel = bool
258 def make_cli_parser1(self):
259 import optparse
261 class MyOptionParser(optparse.OptionParser):
262 def error(self, msg):
263 logger.error(msg)
264 self.exit(1)
266 parser = MyOptionParser()
268 parser.add_option(
269 '--format',
270 dest='format',
271 default='from_extension',
272 choices=(
273 'mseed', 'sac', 'kan', 'segy', 'seisan', 'seisan.l',
274 'seisan.b', 'gse1', 'gcf', 'yaff', 'datacube',
275 'from_extension', 'detect'),
276 help='assume files are of given FORMAT [default: \'%default\']')
278 parser.add_option(
279 '--pattern',
280 dest='regex',
281 metavar='REGEX',
282 help='only include files whose paths match REGEX')
284 self.add_params_to_cli_parser(parser)
285 self.configure_cli_parser(parser)
286 return parser
288 def configure_cli_parser(self, parser):
289 pass
291 def cli_usage(self):
292 return None
294 def add_params_to_cli_parser(self, parser):
296 for param in self._parameters:
297 if isinstance(param, Param):
298 parser.add_option(
299 '--' + param.ident,
300 dest=param.ident,
301 default=param.default,
302 type='float',
303 help=param.name)
305 def setup_cli(self):
306 self.setup()
307 parser = self.make_cli_parser1()
308 (options, args) = parser.parse_args()
310 for param in self._parameters:
311 if isinstance(param, Param):
312 setattr(self, param.ident, getattr(options, param.ident))
314 self._cli_params['regex'] = options.regex
315 self._cli_params['format'] = options.format
316 self._cli_params['sources'] = args
318 return options, args, parser
320 def delete_gui(self):
321 '''
322 Remove the gui elements of the snuffling.
324 This removes the panel and menu entry of the widget from the viewer and
325 also removes all traces and markers added with the
326 :py:meth:`add_traces` and :py:meth:`add_markers` methods.
327 '''
329 self.cleanup()
331 if self._panel is not None:
332 self._panel_parent.remove_panel(self._panel)
333 self._panel = None
335 if self._menuitem is not None:
336 self._menu_parent.remove_snuffling_menuitem(self._menuitem)
337 self._menuitem = None
339 if self._helpmenuitem is not None:
340 self._menu_parent.remove_snuffling_help_menuitem(
341 self._helpmenuitem)
343 def set_name(self, name):
344 '''
345 Set the snuffling's name.
347 The snuffling's name is shown as a menu entry and in the panel header.
348 '''
350 self._name = name
351 self.reset_gui()
353 def get_name(self):
354 '''
355 Get the snuffling's name.
356 '''
358 return self._name
360 def set_have_pre_process_hook(self, bool):
361 self._have_pre_process_hook = bool
362 self._live_update = False
363 self._pre_process_hook_enabled = False
364 self.reset_gui()
366 def set_have_post_process_hook(self, bool):
367 self._have_post_process_hook = bool
368 self._live_update = False
369 self._post_process_hook_enabled = False
370 self.reset_gui()
372 def set_have_pile_changed_hook(self, bool):
373 self._pile_ = False
375 def enable_pile_changed_notifications(self):
376 '''
377 Get informed when pile changed.
379 When activated, the :py:meth:`pile_changed` method is called on every
380 update in the viewer's pile.
381 '''
383 viewer = self.get_viewer()
384 viewer.pile_has_changed_signal.connect(
385 self.pile_changed)
387 def disable_pile_changed_notifications(self):
388 '''
389 Stop getting informed about changes in viewer's pile.
390 '''
392 viewer = self.get_viewer()
393 viewer.pile_has_changed_signal.disconnect(
394 self.pile_changed)
396 def pile_changed(self):
397 '''
398 Called when the connected viewer's pile has changed.
400 Must be activated with a call to
401 :py:meth:`enable_pile_changed_notifications`.
402 '''
404 pass
406 def reset_gui(self, reloaded=False):
407 '''
408 Delete and recreate the snuffling's panel.
409 '''
411 if self._panel or self._menuitem:
412 sett = self.get_settings()
413 self.delete_gui()
414 self.setup_gui(reloaded=reloaded)
415 self.set_settings(sett)
417 def show_message(self, kind, message):
418 '''
419 Display a message box.
421 :param kind: string defining kind of message
422 :param message: the message to be displayed
423 '''
425 try:
426 box = qw.QMessageBox(self.get_viewer())
427 box.setText('%s: %s' % (kind.capitalize(), message))
428 box.exec_()
429 except NoViewerSet:
430 pass
432 def error(self, message):
433 '''
434 Show an error message box.
436 :param message: specifying the error
437 '''
439 logger.error('%s: %s' % (self._name, message))
440 self.show_message('error', message)
442 def warn(self, message):
443 '''
444 Display a warning message.
446 :param message: specifying the warning
447 '''
449 logger.warning('%s: %s' % (self._name, message))
450 self.show_message('warning', message)
452 def fail(self, message):
453 '''
454 Show an error message box and raise :py:exc:`SnufflingCallFailed`
455 exception.
457 :param message: specifying the error
458 '''
460 self.error(message)
461 raise SnufflingCallFailed(message)
463 def pylab(self, name=None, get='axes'):
464 '''
465 Create a :py:class:`FigureFrame` and return either the frame,
466 a :py:class:`matplotlib.figure.Figure` instance or a
467 :py:class:`matplotlib.axes.Axes` instance.
469 :param name: labels the figure frame's tab
470 :param get: 'axes'|'figure'|'frame' (optional)
471 '''
473 if name is None:
474 self._iplot += 1
475 name = 'Plot %i (%s)' % (self._iplot, self.get_name())
477 fframe = FigureFrame()
478 self._panel_parent.add_tab(name, fframe)
479 if get == 'axes':
480 return fframe.gca()
481 elif get == 'figure':
482 return fframe.gcf()
483 elif get == 'figure_frame':
484 return fframe
486 def figure(self, name=None):
487 '''
488 Returns a :py:class:`matplotlib.figure.Figure` instance
489 which can be displayed within snuffler by calling
490 :py:meth:`canvas.draw`.
492 :param name: labels the tab of the figure
493 '''
495 return self.pylab(name=name, get='figure')
497 def axes(self, name=None):
498 '''
499 Returns a :py:class:`matplotlib.axes.Axes` instance.
501 :param name: labels the tab of axes
502 '''
504 return self.pylab(name=name, get='axes')
506 def figure_frame(self, name=None):
507 '''
508 Create a :py:class:`pyrocko.gui.util.FigureFrame`.
510 :param name: labels the tab figure frame
511 '''
513 return self.pylab(name=name, get='figure_frame')
515 def pixmap_frame(self, filename=None, name=None):
516 '''
517 Create a :py:class:`pyrocko.gui.util.PixmapFrame`.
519 :param name: labels the tab
520 :param filename: name of file to be displayed
521 '''
523 f = PixmapFrame(filename)
525 scroll_area = qw.QScrollArea()
526 scroll_area.setWidget(f)
527 scroll_area.setWidgetResizable(True)
529 self._panel_parent.add_tab(name or "Pixmap", scroll_area)
530 return f
532 def web_frame(self, url=None, name=None):
533 '''
534 Creates a :py:class:`WebKitFrame` which can be used as a browser
535 within snuffler.
537 :param url: url to open
538 :param name: labels the tab
539 '''
541 if name is None:
542 self._iplot += 1
543 name = 'Web browser %i (%s)' % (self._iplot, self.get_name())
545 f = WebKitFrame(url)
546 self._panel_parent.add_tab(name, f)
547 return f
549 def vtk_frame(self, name=None, actors=None):
550 '''
551 Create a :py:class:`pyrocko.gui.util.VTKFrame` to render interactive 3D
552 graphics.
554 :param actors: list of VTKActors
555 :param name: labels the tab
557 Initialize the interactive rendering by calling the frames'
558 :py:meth`initialize` method after having added all actors to the frames
559 renderer.
561 Requires installation of vtk including python wrapper.
562 '''
563 if name is None:
564 self._iplot += 1
565 name = 'VTK %i (%s)' % (self._iplot, self.get_name())
567 try:
568 f = VTKFrame(actors=actors)
569 except ImportError as e:
570 self.fail(e)
572 self._panel_parent.add_tab(name, f)
573 return f
575 def tempdir(self):
576 '''
577 Create a temporary directory and return its absolute path.
579 The directory and all its contents are removed when the Snuffling
580 instance is deleted.
581 '''
583 if self._tempdir is None:
584 self._tempdir = tempfile.mkdtemp('', 'snuffling-tmp-')
586 return self._tempdir
588 def set_live_update(self, live_update):
589 '''
590 Enable/disable live updating.
592 When live updates are enabled, the :py:meth:`call` method is called
593 whenever the user changes a parameter. If it is disabled, the user has
594 to initiate such a call manually by triggering the snuffling's menu
595 item or pressing the call button.
596 '''
598 self._live_update = live_update
599 if self._have_pre_process_hook:
600 self._pre_process_hook_enabled = live_update
601 if self._have_post_process_hook:
602 self._post_process_hook_enabled = live_update
604 try:
605 self.get_viewer().clean_update()
606 except NoViewerSet:
607 pass
609 def add_parameter(self, param):
610 '''
611 Add an adjustable parameter to the snuffling.
613 :param param: object of type :py:class:`Param`, :py:class:`Switch`, or
614 :py:class:`Choice`.
616 For each parameter added, controls are added to the snuffling's panel,
617 so that the parameter can be adjusted from the gui.
618 '''
620 self._parameters.append(param)
621 self._set_parameter_value(param.ident, param.default)
623 if self._panel is not None:
624 self.delete_gui()
625 self.setup_gui()
627 def add_trigger(self, name, method):
628 '''
629 Add a button to the snuffling's panel.
631 :param name: string that labels the button
632 :param method: method associated with the button
633 '''
635 self._triggers.append((name, method))
637 if self._panel is not None:
638 self.delete_gui()
639 self.setup_gui()
641 def get_parameters(self):
642 '''
643 Get the snuffling's adjustable parameter definitions.
645 Returns a list of objects of type :py:class:`Param`.
646 '''
648 return self._parameters
650 def get_parameter(self, ident):
651 '''
652 Get one of the snuffling's adjustable parameter definitions.
654 :param ident: identifier of the parameter
656 Returns an object of type :py:class:`Param` or ``None``.
657 '''
659 for param in self._parameters:
660 if param.ident == ident:
661 return param
662 return None
664 def set_parameter(self, ident, value):
665 '''
666 Set one of the snuffling's adjustable parameters.
668 :param ident: identifier of the parameter
669 :param value: new value of the parameter
671 Adjusts the control of a parameter without calling :py:meth:`call`.
672 '''
674 self._set_parameter_value(ident, value)
676 control = self._param_controls.get(ident, None)
677 if control:
678 control.set_value(value)
680 def set_parameter_range(self, ident, vmin, vmax):
681 '''
682 Set the range of one of the snuffling's adjustable parameters.
684 :param ident: identifier of the parameter
685 :param vmin,vmax: new minimum and maximum value for the parameter
687 Adjusts the control of a parameter without calling :py:meth:`call`.
688 '''
690 control = self._param_controls.get(ident, None)
691 if control:
692 control.set_range(vmin, vmax)
694 def set_parameter_choices(self, ident, choices):
695 '''
696 Update the choices of a Choice parameter.
698 :param ident: identifier of the parameter
699 :param choices: list of strings
700 '''
702 control = self._param_controls.get(ident, None)
703 if control:
704 selected_choice = control.set_choices(choices)
705 self._set_parameter_value(ident, selected_choice)
707 def _set_parameter_value(self, ident, value):
708 setattr(self, ident, value)
710 def get_parameter_value(self, ident):
711 '''
712 Get the current value of a parameter.
714 :param ident: identifier of the parameter
715 '''
716 return getattr(self, ident)
718 def get_settings(self):
719 '''
720 Returns a dictionary with identifiers of all parameters as keys and
721 their values as the dictionaries values.
722 '''
724 params = self.get_parameters()
725 settings = {}
726 for param in params:
727 settings[param.ident] = self.get_parameter_value(param.ident)
729 return settings
731 def set_settings(self, settings):
732 params = self.get_parameters()
733 dparams = dict([(param.ident, param) for param in params])
734 for k, v in settings.items():
735 if k in dparams:
736 self._set_parameter_value(k, v)
737 if k in self._param_controls:
738 control = self._param_controls[k]
739 control.set_value(v)
741 def get_viewer(self):
742 '''
743 Get the parent viewer.
745 Returns a reference to an object of type :py:class:`PileOverview`,
746 which is the main viewer widget.
748 If no gui has been initialized for the snuffling, a
749 :py:exc:`NoViewerSet` exception is raised.
750 '''
752 if self._viewer is None:
753 raise NoViewerSet()
754 return self._viewer
756 def get_pile(self):
757 '''
758 Get the pile.
760 If a gui has been initialized, a reference to the viewer's internal
761 pile is returned. If not, the :py:meth:`make_pile` method (which may be
762 overloaded in subclass) is called to create a pile. This can be
763 utilized to make hybrid snufflings, which may work also in a standalone
764 mode.
765 '''
767 try:
768 p = self.get_viewer().get_pile()
769 except NoViewerSet:
770 if self._no_viewer_pile is None:
771 self._no_viewer_pile = self.make_pile()
773 p = self._no_viewer_pile
775 return p
777 def get_active_event_and_stations(
778 self, trange=(-3600., 3600.), missing='warn'):
780 '''
781 Get event and stations with available data for active event.
783 :param trange: (begin, end), time range around event origin time to
784 query for available data
785 :param missing: string, what to do in case of missing station
786 information: ``'warn'``, ``'raise'`` or ``'ignore'``.
788 :returns: ``(event, stations)``
789 '''
791 p = self.get_pile()
792 v = self.get_viewer()
794 event = v.get_active_event()
795 if event is None:
796 self.fail(
797 'No active event set. Select an event and press "e" to make '
798 'it the "active event"')
800 stations = {}
801 for traces in p.chopper(
802 event.time+trange[0],
803 event.time+trange[1],
804 load_data=False,
805 degap=False):
807 for tr in traces:
808 try:
809 for skey in v.station_keys(tr):
810 if skey in stations:
811 continue
813 station = v.get_station(skey)
814 stations[skey] = station
816 except KeyError:
817 s = 'No station information for station key "%s".' \
818 % '.'.join(skey)
820 if missing == 'warn':
821 logger.warning(s)
822 elif missing == 'raise':
823 raise MissingStationInformation(s)
824 elif missing == 'ignore':
825 pass
826 else:
827 assert False, 'invalid argument to "missing"'
829 stations[skey] = None
831 return event, list(set(
832 st for st in stations.values() if st is not None))
834 def get_stations(self):
835 '''
836 Get all stations known to the viewer.
837 '''
839 v = self.get_viewer()
840 stations = list(v.stations.values())
841 return stations
843 def get_markers(self):
844 '''
845 Get all markers from the viewer.
846 '''
848 return self.get_viewer().get_markers()
850 def get_event_markers(self):
851 '''
852 Get all event markers from the viewer.
853 '''
855 return [m for m in self.get_viewer().get_markers()
856 if isinstance(m, EventMarker)]
858 def get_selected_markers(self):
859 '''
860 Get all selected markers from the viewer.
861 '''
863 return self.get_viewer().selected_markers()
865 def get_selected_event_markers(self):
866 '''
867 Get all selected event markers from the viewer.
868 '''
870 return [m for m in self.get_viewer().selected_markers()
871 if isinstance(m, EventMarker)]
873 def get_active_event_and_phase_markers(self):
874 '''
875 Get the marker of the active event and any associated phase markers
876 '''
878 viewer = self.get_viewer()
879 markers = viewer.get_markers()
880 event_marker = viewer.get_active_event_marker()
881 if event_marker is None:
882 self.fail(
883 'No active event set. '
884 'Select an event and press "e" to make it the "active event"')
886 event = event_marker.get_event()
888 selection = []
889 for m in markers:
890 if isinstance(m, PhaseMarker):
891 if m.get_event() is event:
892 selection.append(m)
894 return (
895 event_marker,
896 [m for m in markers if isinstance(m, PhaseMarker) and
897 m.get_event() == event])
899 def get_viewer_trace_selector(self, mode='inview'):
900 '''
901 Get currently active trace selector from viewer.
903 :param mode: set to ``'inview'`` (default) to only include selections
904 currently shown in the viewer, ``'visible' to include all traces
905 not currenly hidden by hide or quick-select commands, or ``'all'``
906 to disable any restrictions.
907 '''
909 viewer = self.get_viewer()
911 def rtrue(tr):
912 return True
914 if mode == 'inview':
915 return viewer.trace_selector or rtrue
916 elif mode == 'visible':
917 return viewer.trace_filter or rtrue
918 elif mode == 'all':
919 return rtrue
920 else:
921 raise Exception('invalid mode argument')
923 def chopper_selected_traces(self, fallback=False, marker_selector=None,
924 mode='inview', main_bandpass=False,
925 progress=None, responsive=False,
926 *args, **kwargs):
927 '''
928 Iterate over selected traces.
930 Shortcut to get all trace data contained in the selected markers in the
931 running snuffler. For each selected marker,
932 :py:meth:`pyrocko.pile.Pile.chopper` is called with the arguments
933 *tmin*, *tmax*, and *trace_selector* set to values according to the
934 marker. Additional arguments to the chopper are handed over from
935 *\\*args* and *\\*\\*kwargs*.
937 :param fallback:
938 If ``True``, if no selection has been marked, use the content
939 currently visible in the viewer.
941 :param marker_selector:
942 If not ``None`` a callback to filter markers.
944 :param mode:
945 Set to ``'inview'`` (default) to only include selections currently
946 shown in the viewer (excluding traces accessible through vertical
947 scrolling), ``'visible'`` to include all traces not currently
948 hidden by hide or quick-select commands (including traces
949 accessible through vertical scrolling), or ``'all'`` to disable any
950 restrictions.
952 :param main_bandpass:
953 If ``True``, apply main control high- and lowpass filters to
954 traces. Note: use with caution. Processing is fixed to use 4th
955 order Butterworth highpass and lowpass and the signal is always
956 demeaned before filtering. FFT filtering, rotation, demean and
957 bandpass settings from the graphical interface are not respected
958 here. Padding is not automatically adjusted so results may include
959 artifacts.
961 :param progress:
962 If given a string a progress bar is shown to the user. The string
963 is used as the label for the progress bar.
965 :param responsive:
966 If set to ``True``, occasionally allow UI events to be processed.
967 If used in combination with ``progress``, this allows the iterator
968 to be aborted by the user.
969 '''
971 try:
972 viewer = self.get_viewer()
973 markers = [
974 m for m in viewer.selected_markers()
975 if not isinstance(m, EventMarker)]
977 if marker_selector is not None:
978 markers = [
979 marker for marker in markers if marker_selector(marker)]
981 pile = self.get_pile()
983 def rtrue(tr):
984 return True
986 trace_selector_arg = kwargs.pop('trace_selector', rtrue)
987 trace_selector_viewer = self.get_viewer_trace_selector(mode)
989 style_arg = kwargs.pop('style', None)
991 if main_bandpass:
992 def apply_filters(traces):
993 for tr in traces:
994 if viewer.highpass is not None:
995 tr.highpass(4, viewer.highpass)
996 if viewer.lowpass is not None:
997 tr.lowpass(4, viewer.lowpass)
998 return traces
999 else:
1000 def apply_filters(traces):
1001 return traces
1003 pb = viewer.parent().get_progressbars()
1005 time_last = [time.time()]
1007 def update_progress(label, batch):
1008 time_now = time.time()
1009 if responsive:
1010 # start processing events with one second delay, so that
1011 # e.g. cleanup actions at startup do not cause track number
1012 # changes etc.
1013 if time_last[0] + 1. < time_now:
1014 qw.qApp.processEvents()
1015 else:
1016 # redraw about once a second
1017 if time_last[0] + 1. < time_now:
1018 viewer.repaint()
1020 time_last[0] = time.time() # use time after drawing
1022 abort = pb.set_status(
1023 label, batch.i*100./batch.n, responsive)
1024 abort |= viewer.window().is_closing()
1026 return abort
1028 if markers:
1029 for imarker, marker in enumerate(markers):
1030 try:
1031 if progress:
1032 label = '%s: %i/%i' % (
1033 progress, imarker+1, len(markers))
1035 pb.set_status(label, 0, responsive)
1037 if not marker.nslc_ids:
1038 trace_selector_marker = rtrue
1039 else:
1040 def trace_selector_marker(tr):
1041 return marker.match_nslc(tr.nslc_id)
1043 def trace_selector(tr):
1044 return trace_selector_arg(tr) \
1045 and trace_selector_viewer(tr) \
1046 and trace_selector_marker(tr)
1048 for batch in pile.chopper(
1049 tmin=marker.tmin,
1050 tmax=marker.tmax,
1051 trace_selector=trace_selector,
1052 style='batch',
1053 *args,
1054 **kwargs):
1056 if progress:
1057 abort = update_progress(label, batch)
1058 if abort:
1059 return
1061 batch.traces = apply_filters(batch.traces)
1062 if style_arg == 'batch':
1063 yield batch
1064 else:
1065 yield batch.traces
1067 finally:
1068 if progress:
1069 pb.set_status(label, 100., responsive)
1071 elif fallback:
1072 def trace_selector(tr):
1073 return trace_selector_arg(tr) \
1074 and trace_selector_viewer(tr)
1076 tmin, tmax = viewer.get_time_range()
1078 if not pile.is_empty():
1079 ptmin = pile.get_tmin()
1080 tpad = kwargs.get('tpad', 0.0)
1081 if ptmin > tmin:
1082 tmin = ptmin + tpad
1083 ptmax = pile.get_tmax()
1084 if ptmax < tmax:
1085 tmax = ptmax - tpad
1087 try:
1088 if progress:
1089 label = progress
1090 pb.set_status(label, 0, responsive)
1092 for batch in pile.chopper(
1093 tmin=tmin,
1094 tmax=tmax,
1095 trace_selector=trace_selector,
1096 style='batch',
1097 *args,
1098 **kwargs):
1100 if progress:
1101 abort = update_progress(label, batch)
1103 if abort:
1104 return
1106 batch.traces = apply_filters(batch.traces)
1108 if style_arg == 'batch':
1109 yield batch
1110 else:
1111 yield batch.traces
1113 finally:
1114 if progress:
1115 pb.set_status(label, 100., responsive)
1117 else:
1118 raise NoTracesSelected()
1120 except NoViewerSet:
1121 pile = self.get_pile()
1122 return pile.chopper(*args, **kwargs)
1124 def get_selected_time_range(self, fallback=False):
1125 '''
1126 Get the time range spanning all selected markers.
1128 :param fallback: if ``True`` and no marker is selected return begin and
1129 end of visible time range
1130 '''
1132 viewer = self.get_viewer()
1133 markers = viewer.selected_markers()
1134 mins = [marker.tmin for marker in markers]
1135 maxs = [marker.tmax for marker in markers]
1137 if mins and maxs:
1138 tmin = min(mins)
1139 tmax = max(maxs)
1141 elif fallback:
1142 tmin, tmax = viewer.get_time_range()
1144 else:
1145 raise NoTracesSelected()
1147 return tmin, tmax
1149 def panel_visibility_changed(self, bool):
1150 '''
1151 Called when the snuffling's panel becomes visible or is hidden.
1153 Can be overloaded in subclass, e.g. to perform additional setup actions
1154 when the panel is activated the first time.
1155 '''
1157 pass
1159 def make_pile(self):
1160 '''
1161 Create a pile.
1163 To be overloaded in subclass. The default implementation just calls
1164 :py:func:`pyrocko.pile.make_pile` to create a pile from command line
1165 arguments.
1166 '''
1168 cachedirname = config.config().cache_dir
1169 sources = self._cli_params.get('sources', sys.argv[1:])
1170 return pile.make_pile(
1171 sources,
1172 cachedirname=cachedirname,
1173 regex=self._cli_params['regex'],
1174 fileformat=self._cli_params['format'])
1176 def make_panel(self, parent):
1177 '''
1178 Create a widget for the snuffling's control panel.
1180 Normally called from the :py:meth:`setup_gui` method. Returns ``None``
1181 if no panel is needed (e.g. if the snuffling has no adjustable
1182 parameters).
1183 '''
1185 params = self.get_parameters()
1186 self._param_controls = {}
1187 if params or self._force_panel:
1188 sarea = MyScrollArea(parent.get_panel_parent_widget())
1189 sarea.setFrameStyle(qw.QFrame.NoFrame)
1190 sarea.setSizePolicy(qw.QSizePolicy(
1191 qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding))
1192 frame = MyFrame(sarea)
1193 frame.widgetVisibilityChanged.connect(
1194 self.panel_visibility_changed)
1196 frame.setSizePolicy(qw.QSizePolicy(
1197 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1198 frame.setFrameStyle(qw.QFrame.NoFrame)
1199 sarea.setWidget(frame)
1200 sarea.setWidgetResizable(True)
1201 layout = qw.QGridLayout()
1202 layout.setContentsMargins(0, 0, 0, 0)
1203 layout.setSpacing(0)
1204 frame.setLayout(layout)
1206 parlayout = qw.QGridLayout()
1208 irow = 0
1209 ipar = 0
1210 have_switches = False
1211 have_params = False
1212 for iparam, param in enumerate(params):
1213 if isinstance(param, Param):
1214 if param.minimum <= 0.0:
1215 param_control = LinValControl(
1216 high_is_none=param.high_is_none,
1217 low_is_none=param.low_is_none)
1218 else:
1219 param_control = ValControl(
1220 high_is_none=param.high_is_none,
1221 low_is_none=param.low_is_none,
1222 low_is_zero=param.low_is_zero)
1224 param_control.setup(
1225 param.name,
1226 param.minimum,
1227 param.maximum,
1228 param.default,
1229 iparam)
1231 param_control.set_tracking(param.tracking)
1232 param_control.set_type(param.type)
1233 param_control.valchange.connect(
1234 self.modified_snuffling_panel)
1236 self._param_controls[param.ident] = param_control
1237 for iw, w in enumerate(param_control.widgets()):
1238 parlayout.addWidget(w, ipar, iw)
1240 ipar += 1
1241 have_params = True
1243 elif isinstance(param, Choice):
1244 param_widget = ChoiceControl(
1245 param.ident, param.default, param.choices, param.name)
1246 param_widget.choosen.connect(
1247 self.choose_on_snuffling_panel)
1249 self._param_controls[param.ident] = param_widget
1250 parlayout.addWidget(param_widget, ipar, 0, 1, 3)
1251 ipar += 1
1252 have_params = True
1254 elif isinstance(param, Switch):
1255 have_switches = True
1257 if have_params:
1258 parframe = qw.QFrame(sarea)
1259 parframe.setSizePolicy(qw.QSizePolicy(
1260 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1261 parframe.setLayout(parlayout)
1262 layout.addWidget(parframe, irow, 0)
1263 irow += 1
1265 if have_switches:
1266 swlayout = qw.QGridLayout()
1267 isw = 0
1268 for iparam, param in enumerate(params):
1269 if isinstance(param, Switch):
1270 param_widget = SwitchControl(
1271 param.ident, param.default, param.name)
1272 param_widget.sw_toggled.connect(
1273 self.switch_on_snuffling_panel)
1275 self._param_controls[param.ident] = param_widget
1276 swlayout.addWidget(param_widget, isw/10, isw % 10)
1277 isw += 1
1279 swframe = qw.QFrame(sarea)
1280 swframe.setSizePolicy(qw.QSizePolicy(
1281 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1282 swframe.setLayout(swlayout)
1283 layout.addWidget(swframe, irow, 0)
1284 irow += 1
1286 butframe = qw.QFrame(sarea)
1287 butframe.setSizePolicy(qw.QSizePolicy(
1288 qw.QSizePolicy.Expanding, qw.QSizePolicy.Minimum))
1289 butlayout = qw.QHBoxLayout()
1290 butframe.setLayout(butlayout)
1292 live_update_checkbox = qw.QCheckBox('Auto-Run')
1293 if self._live_update:
1294 live_update_checkbox.setCheckState(qc.Qt.Checked)
1296 butlayout.addWidget(live_update_checkbox)
1297 live_update_checkbox.toggled.connect(
1298 self.live_update_toggled)
1300 help_button = qw.QPushButton('Help')
1301 butlayout.addWidget(help_button)
1302 help_button.clicked.connect(
1303 self.help_button_triggered)
1305 clear_button = qw.QPushButton('Clear')
1306 butlayout.addWidget(clear_button)
1307 clear_button.clicked.connect(
1308 self.clear_button_triggered)
1310 call_button = qw.QPushButton('Run')
1311 butlayout.addWidget(call_button)
1312 call_button.clicked.connect(
1313 self.call_button_triggered)
1315 for name, method in self._triggers:
1316 but = qw.QPushButton(name)
1318 def call_and_update(method):
1319 def f():
1320 try:
1321 method()
1322 except SnufflingError as e:
1323 if not isinstance(e, SnufflingCallFailed):
1324 # those have logged within error()
1325 logger.error('%s: %s' % (self._name, e))
1326 logger.error(
1327 '%s: Snuffling action failed' % self._name)
1329 self.get_viewer().update()
1330 return f
1332 but.clicked.connect(
1333 call_and_update(method))
1335 butlayout.addWidget(but)
1337 layout.addWidget(butframe, irow, 0)
1339 irow += 1
1340 spacer = qw.QSpacerItem(
1341 0, 0, qw.QSizePolicy.Expanding, qw.QSizePolicy.Expanding)
1343 layout.addItem(spacer, irow, 0)
1345 return sarea
1347 else:
1348 return None
1350 def make_helpmenuitem(self, parent):
1351 '''
1352 Create the help menu item for the snuffling.
1353 '''
1355 item = qw.QAction(self.get_name(), None)
1357 item.triggered.connect(
1358 self.help_button_triggered)
1360 return item
1362 def make_menuitem(self, parent):
1363 '''
1364 Create the menu item for the snuffling.
1366 This method may be overloaded in subclass and return ``None``, if no
1367 menu entry is wanted.
1368 '''
1370 item = qw.QAction(self.get_name(), None)
1371 item.setCheckable(
1372 self._have_pre_process_hook or self._have_post_process_hook)
1374 item.triggered.connect(
1375 self.menuitem_triggered)
1377 return item
1379 def output_filename(
1380 self,
1381 caption='Save File',
1382 dir='',
1383 filter='',
1384 selected_filter=None):
1386 '''
1387 Query user for an output filename.
1389 This is currently a wrapper to :py:func:`QFileDialog.getSaveFileName`.
1390 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1391 dialog.
1392 '''
1394 if not dir and self._previous_output_filename:
1395 dir = self._previous_output_filename
1397 fn = getSaveFileName(
1398 self.get_viewer(), caption, dir, filter, selected_filter)
1399 if not fn:
1400 raise UserCancelled()
1402 self._previous_output_filename = fn
1403 return str(fn)
1405 def input_directory(self, caption='Open Directory', dir=''):
1406 '''
1407 Query user for an input directory.
1409 This is a wrapper to :py:func:`QFileDialog.getExistingDirectory`.
1410 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1411 dialog.
1412 '''
1414 if not dir and self._previous_input_directory:
1415 dir = self._previous_input_directory
1417 dn = qw.QFileDialog.getExistingDirectory(
1418 None, caption, dir, qw.QFileDialog.ShowDirsOnly)
1420 if not dn:
1421 raise UserCancelled()
1423 self._previous_input_directory = dn
1424 return str(dn)
1426 def input_filename(self, caption='Open File', dir='', filter='',
1427 selected_filter=None):
1428 '''
1429 Query user for an input filename.
1431 This is currently a wrapper to :py:func:`QFileDialog.getOpenFileName`.
1432 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1433 dialog.
1434 '''
1436 if not dir and self._previous_input_filename:
1437 dir = self._previous_input_filename
1439 fn, _ = fnpatch(qw.QFileDialog.getOpenFileName(
1440 self.get_viewer(),
1441 caption,
1442 dir,
1443 filter)) # selected_filter)
1445 if not fn:
1446 raise UserCancelled()
1448 self._previous_input_filename = fn
1449 return str(fn)
1451 def input_dialog(self, caption='', request='', directory=False):
1452 '''
1453 Query user for a text input.
1455 This is currently a wrapper to :py:func:`QInputDialog.getText`.
1456 A :py:exc:`UserCancelled` exception is raised if the user cancels the
1457 dialog.
1458 '''
1460 inp, ok = qw.QInputDialog.getText(self.get_viewer(), 'Input', caption)
1462 if not ok:
1463 raise UserCancelled()
1465 return inp
1467 def modified_snuffling_panel(self, value, iparam):
1468 '''
1469 Called when the user has played with an adjustable parameter.
1471 The default implementation sets the parameter, calls the snuffling's
1472 :py:meth:`call` method and finally triggers an update on the viewer
1473 widget.
1474 '''
1476 param = self.get_parameters()[iparam]
1477 self._set_parameter_value(param.ident, value)
1478 if self._live_update:
1479 self.check_call()
1480 self.get_viewer().update()
1482 def switch_on_snuffling_panel(self, ident, state):
1483 '''
1484 Called when the user has toggled a switchable parameter.
1485 '''
1487 self._set_parameter_value(ident, state)
1488 if self._live_update:
1489 self.check_call()
1490 self.get_viewer().update()
1492 def choose_on_snuffling_panel(self, ident, state):
1493 '''
1494 Called when the user has made a choice about a choosable parameter.
1495 '''
1497 self._set_parameter_value(ident, state)
1498 if self._live_update:
1499 self.check_call()
1500 self.get_viewer().update()
1502 def menuitem_triggered(self, arg):
1503 '''
1504 Called when the user has triggered the snuffling's menu.
1506 The default implementation calls the snuffling's :py:meth:`call` method
1507 and triggers an update on the viewer widget.
1508 '''
1510 self.check_call()
1512 if self._have_pre_process_hook:
1513 self._pre_process_hook_enabled = arg
1515 if self._have_post_process_hook:
1516 self._post_process_hook_enabled = arg
1518 if self._have_pre_process_hook or self._have_post_process_hook:
1519 self.get_viewer().clean_update()
1520 else:
1521 self.get_viewer().update()
1523 def call_button_triggered(self):
1524 '''
1525 Called when the user has clicked the snuffling's call button.
1527 The default implementation calls the snuffling's :py:meth:`call` method
1528 and triggers an update on the viewer widget.
1529 '''
1531 self.check_call()
1532 self.get_viewer().update()
1534 def clear_button_triggered(self):
1535 '''
1536 Called when the user has clicked the snuffling's clear button.
1538 This calls the :py:meth:`cleanup` method and triggers an update on the
1539 viewer widget.
1540 '''
1542 self.cleanup()
1543 self.get_viewer().update()
1545 def help_button_triggered(self):
1546 '''
1547 Creates a :py:class:`QLabel` which contains the documentation as
1548 given in the snufflings' __doc__ string.
1549 '''
1551 if self.__doc__:
1552 if self.__doc__.strip().startswith('<html>'):
1553 doc = qw.QLabel(self.__doc__)
1554 else:
1555 try:
1556 import markdown
1557 doc = qw.QLabel(markdown.markdown(self.__doc__))
1559 except ImportError:
1560 logger.error(
1561 'Install Python module "markdown" for pretty help '
1562 'formatting.')
1564 doc = qw.QLabel(self.__doc__)
1565 else:
1566 doc = qw.QLabel('This snuffling does not provide any online help.')
1568 labels = [doc]
1570 if self._filename:
1571 from html import escape
1573 code = open(self._filename, 'r').read()
1575 doc_src = qw.QLabel(
1576 '''<html><body>
1577<hr />
1578<center><em>May the source be with you, young Skywalker!</em><br /><br />
1579<a href="file://%s"><code>%s</code></a></center>
1580<br />
1581<p style="margin-left: 2em; margin-right: 2em; background-color:#eed;">
1582<pre style="white-space: pre-wrap"><code>%s
1583</code></pre></p></body></html>'''
1584 % (
1585 quote(self._filename),
1586 escape(self._filename),
1587 escape(code)))
1589 labels.append(doc_src)
1591 for h in labels:
1592 h.setAlignment(qc.Qt.AlignTop | qc.Qt.AlignLeft)
1593 h.setWordWrap(True)
1595 self._viewer.show_doc('Help: %s' % self._name, labels, target='panel')
1597 def live_update_toggled(self, on):
1598 '''
1599 Called when the checkbox for live-updates has been toggled.
1600 '''
1602 self.set_live_update(on)
1604 def add_traces(self, traces):
1605 '''
1606 Add traces to the viewer.
1608 :param traces: list of objects of type :py:class:`pyrocko.trace.Trace`
1610 The traces are put into a :py:class:`pyrocko.pile.MemTracesFile` and
1611 added to the viewer's internal pile for display. Note, that unlike with
1612 the traces from the files given on the command line, these traces are
1613 kept in memory and so may quickly occupy a lot of ram if a lot of
1614 traces are added.
1616 This method should be preferred over modifying the viewer's internal
1617 pile directly, because this way, the snuffling has a chance to
1618 automatically remove its private traces again (see :py:meth:`cleanup`
1619 method).
1620 '''
1622 ticket = self.get_viewer().add_traces(traces)
1623 self._tickets.append(ticket)
1624 return ticket
1626 def add_trace(self, tr):
1627 '''
1628 Add a trace to the viewer.
1630 See :py:meth:`add_traces`.
1631 '''
1633 self.add_traces([tr])
1635 def add_markers(self, markers):
1636 '''
1637 Add some markers to the display.
1639 Takes a list of objects of type :py:class:`pyrocko.gui.util.Marker` and
1640 adds these to the viewer.
1641 '''
1643 self.get_viewer().add_markers(markers)
1644 self._markers.extend(markers)
1646 def add_marker(self, marker):
1647 '''
1648 Add a marker to the display.
1650 See :py:meth:`add_markers`.
1651 '''
1653 self.add_markers([marker])
1655 def cleanup(self):
1656 '''
1657 Remove all traces and markers which have been added so far by the
1658 snuffling.
1659 '''
1661 try:
1662 viewer = self.get_viewer()
1663 viewer.release_data(self._tickets)
1664 viewer.remove_markers(self._markers)
1666 except NoViewerSet:
1667 pass
1669 self._tickets = []
1670 self._markers = []
1672 def check_call(self):
1674 if self._call_in_progress:
1675 self.show_message('error', 'Previous action still in progress.')
1676 return
1678 try:
1679 self._call_in_progress = True
1680 self.call()
1681 return 0
1683 except SnufflingError as e:
1684 if not isinstance(e, SnufflingCallFailed):
1685 # those have logged within error()
1686 logger.error('%s: %s' % (self._name, e))
1687 logger.error('%s: Snuffling action failed' % self._name)
1688 return 1
1690 except Exception as e:
1691 message = '%s: Snuffling action raised an exception: %s' % (
1692 self._name, str(e))
1694 logger.exception(message)
1695 self.show_message('error', message)
1697 finally:
1698 self._call_in_progress = False
1700 def call(self):
1701 '''
1702 Main work routine of the snuffling.
1704 This method is called when the snuffling's menu item has been triggered
1705 or when the user has played with the panel controls. To be overloaded
1706 in subclass. The default implementation does nothing useful.
1707 '''
1709 pass
1711 def pre_process_hook(self, traces):
1712 return traces
1714 def post_process_hook(self, traces):
1715 return traces
1717 def get_tpad(self):
1718 '''
1719 Return current amount of extra padding needed by live processing hooks.
1720 '''
1722 return 0.0
1724 def pre_destroy(self):
1725 '''
1726 Called when the snuffling instance is about to be deleted.
1728 Can be overloaded to do user-defined cleanup actions. The
1729 default implementation calls :py:meth:`cleanup` and deletes
1730 the snuffling`s tempory directory, if needed.
1731 '''
1733 self.cleanup()
1734 if self._tempdir is not None:
1735 import shutil
1736 shutil.rmtree(self._tempdir)
1739class SnufflingError(Exception):
1740 pass
1743class NoViewerSet(SnufflingError):
1744 '''
1745 This exception is raised, when no viewer has been set on a Snuffling.
1746 '''
1748 def __str__(self):
1749 return 'No GUI available. ' \
1750 'Maybe this Snuffling cannot be run in command line mode?'
1753class MissingStationInformation(SnufflingError):
1754 '''
1755 Raised when station information is missing.
1756 '''
1759class NoTracesSelected(SnufflingError):
1760 '''
1761 This exception is raised, when no traces have been selected in the viewer
1762 and we cannot fallback to using the current view.
1763 '''
1765 def __str__(self):
1766 return 'No traces have been selected / are available.'
1769class UserCancelled(SnufflingError):
1770 '''
1771 This exception is raised, when the user has cancelled a snuffling dialog.
1772 '''
1774 def __str__(self):
1775 return 'The user has cancelled a dialog.'
1778class SnufflingCallFailed(SnufflingError):
1779 '''
1780 This exception is raised, when :py:meth:`Snuffling.fail` is called from
1781 :py:meth:`Snuffling.call`.
1782 '''
1785class InvalidSnufflingFilename(Exception):
1786 pass
1789class SnufflingModule(object):
1790 '''
1791 Utility class to load/reload snufflings from a file.
1793 The snufflings are created by user modules which have the special function
1794 :py:func:`__snufflings__` which return the snuffling instances to be
1795 exported. The snuffling module is attached to a handler class, which makes
1796 use of the snufflings (e.g. :py:class:`pyrocko.pile_viewer.PileOverwiew`
1797 from ``pile_viewer.py``). The handler class must implement the methods
1798 ``add_snuffling()`` and ``remove_snuffling()`` which are used as callbacks.
1799 The callbacks are utilized from the methods :py:meth:`load_if_needed` and
1800 :py:meth:`remove_snufflings` which may be called from the handler class,
1801 when needed.
1802 '''
1804 mtimes = {}
1806 def __init__(self, path, name, handler):
1807 self._path = path
1808 self._name = name
1809 self._mtime = None
1810 self._module = None
1811 self._snufflings = []
1812 self._handler = handler
1814 def load_if_needed(self):
1815 filename = os.path.join(self._path, self._name+'.py')
1817 try:
1818 mtime = os.stat(filename)[8]
1819 except OSError as e:
1820 if e.errno == 2:
1821 logger.error(e)
1822 raise BrokenSnufflingModule(filename)
1824 if self._module is None:
1825 sys.path[0:0] = [self._path]
1826 try:
1827 logger.debug('Loading snuffling module %s' % filename)
1828 if self._name in sys.modules:
1829 raise InvalidSnufflingFilename(self._name)
1831 self._module = __import__(self._name)
1832 del sys.modules[self._name]
1834 for snuffling in self._module.__snufflings__():
1835 snuffling._filename = filename
1836 self.add_snuffling(snuffling)
1838 except Exception:
1839 logger.error(traceback.format_exc())
1840 raise BrokenSnufflingModule(filename)
1842 finally:
1843 sys.path[0:1] = []
1845 elif self._mtime != mtime:
1846 logger.warning('Reloading snuffling module %s' % filename)
1847 settings = self.remove_snufflings()
1848 sys.path[0:0] = [self._path]
1849 try:
1851 sys.modules[self._name] = self._module
1853 reload(self._module)
1854 del sys.modules[self._name]
1856 for snuffling in self._module.__snufflings__():
1857 snuffling._filename = filename
1858 self.add_snuffling(snuffling, reloaded=True)
1860 if len(self._snufflings) == len(settings):
1861 for sett, snuf in zip(settings, self._snufflings):
1862 snuf.set_settings(sett)
1864 except Exception:
1865 logger.error(traceback.format_exc())
1866 raise BrokenSnufflingModule(filename)
1868 finally:
1869 sys.path[0:1] = []
1871 self._mtime = mtime
1873 def add_snuffling(self, snuffling, reloaded=False):
1874 snuffling._path = self._path
1875 snuffling.setup()
1876 self._snufflings.append(snuffling)
1877 self._handler.add_snuffling(snuffling, reloaded=reloaded)
1879 def remove_snufflings(self):
1880 settings = []
1881 for snuffling in self._snufflings:
1882 settings.append(snuffling.get_settings())
1883 self._handler.remove_snuffling(snuffling)
1885 self._snufflings = []
1886 return settings
1889class BrokenSnufflingModule(Exception):
1890 pass
1893class MyScrollArea(qw.QScrollArea):
1895 def sizeHint(self):
1896 s = qc.QSize()
1897 s.setWidth(self.widget().sizeHint().width())
1898 s.setHeight(self.widget().sizeHint().height())
1899 return s
1902class SwitchControl(qw.QCheckBox):
1903 sw_toggled = qc.pyqtSignal(object, bool)
1905 def __init__(self, ident, default, *args):
1906 qw.QCheckBox.__init__(self, *args)
1907 self.ident = ident
1908 self.setChecked(default)
1909 self.toggled.connect(self._sw_toggled)
1911 def _sw_toggled(self, state):
1912 self.sw_toggled.emit(self.ident, state)
1914 def set_value(self, state):
1915 self.blockSignals(True)
1916 self.setChecked(state)
1917 self.blockSignals(False)
1920class ChoiceControl(qw.QFrame):
1921 choosen = qc.pyqtSignal(object, object)
1923 def __init__(self, ident, default, choices, name, *args):
1924 qw.QFrame.__init__(self, *args)
1925 self.label = qw.QLabel(name, self)
1926 self.label.setMinimumWidth(120)
1927 self.cbox = qw.QComboBox(self)
1928 self.layout = qw.QHBoxLayout(self)
1929 self.layout.addWidget(self.label)
1930 self.layout.addWidget(self.cbox)
1931 self.layout.setContentsMargins(0, 0, 0, 0)
1932 self.layout.setSpacing(0)
1933 self.ident = ident
1934 self.choices = choices
1935 for ichoice, choice in enumerate(choices):
1936 self.cbox.addItem(choice)
1938 self.set_value(default)
1939 self.cbox.activated.connect(self.emit_choosen)
1941 def set_choices(self, choices):
1942 icur = self.cbox.currentIndex()
1943 if icur != -1:
1944 selected_choice = choices[icur]
1945 else:
1946 selected_choice = None
1948 self.choices = choices
1949 self.cbox.clear()
1950 for ichoice, choice in enumerate(choices):
1951 self.cbox.addItem(qc.QString(choice))
1953 if selected_choice is not None and selected_choice in choices:
1954 self.set_value(selected_choice)
1955 return selected_choice
1956 else:
1957 self.set_value(choices[0])
1958 return choices[0]
1960 def emit_choosen(self, i):
1961 self.choosen.emit(
1962 self.ident,
1963 self.choices[i])
1965 def set_value(self, v):
1966 self.cbox.blockSignals(True)
1967 for i, choice in enumerate(self.choices):
1968 if choice == v:
1969 self.cbox.setCurrentIndex(i)
1970 self.cbox.blockSignals(False)