1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Effective seismological trace viewer.
7'''
8from __future__ import absolute_import
10import sys
11import signal
12import logging
13import time
14import re
15import zlib
16import struct
17import pickle
20from pyrocko.streaming import serial_hamster
21from pyrocko.streaming import slink
22from pyrocko.streaming import edl
23from pyrocko.streaming import datacube
25from pyrocko import pile # noqa
26from pyrocko import util # noqa
27from pyrocko import model # noqa
28from pyrocko import config # noqa
29from pyrocko import io # noqa
31from . import pile_viewer # noqa
33from .qt_compat import qc, qg, qw
35logger = logging.getLogger('pyrocko.gui.snuffler_app')
38class _Getch:
39 '''
40 Gets a single character from standard input.
42 Does not echo to the screen.
44 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
45 '''
46 def __init__(self):
47 try:
48 self.impl = _GetchWindows()
49 except ImportError:
50 self.impl = _GetchUnix()
52 def __call__(self): return self.impl()
55class _GetchUnix:
56 def __init__(self):
57 import tty, sys # noqa
59 def __call__(self):
60 import sys
61 import tty
62 import termios
64 fd = sys.stdin.fileno()
65 old_settings = termios.tcgetattr(fd)
66 try:
67 tty.setraw(fd)
68 ch = sys.stdin.read(1)
69 finally:
70 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
72 return ch
75class _GetchWindows:
76 def __init__(self):
77 import msvcrt # noqa
79 def __call__(self):
80 import msvcrt
81 return msvcrt.getch()
84getch = _Getch()
87class AcquisitionThread(qc.QThread):
88 def __init__(self, post_process_sleep=0.0):
89 qc.QThread.__init__(self)
90 self.mutex = qc.QMutex()
91 self.queue = []
92 self.post_process_sleep = post_process_sleep
93 self._sun_is_shining = True
95 def run(self):
96 while True:
97 try:
98 self.acquisition_start()
99 while self._sun_is_shining:
100 t0 = time.time()
101 self.process()
102 t1 = time.time()
103 if self.post_process_sleep != 0.0:
104 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
106 self.acquisition_stop()
107 break
109 except (
110 edl.ReadError,
111 serial_hamster.SerialHamsterError,
112 slink.SlowSlinkError) as e:
114 logger.error(str(e))
115 logger.error('Acquistion terminated, restart in 5 s')
116 self.acquisition_stop()
117 time.sleep(5)
118 if not self._sun_is_shining:
119 break
121 def stop(self):
122 self._sun_is_shining = False
124 logger.debug("Waiting for thread to terminate...")
125 self.wait()
126 logger.debug("Thread has terminated.")
128 def got_trace(self, tr):
129 self.mutex.lock()
130 self.queue.append(tr)
131 self.mutex.unlock()
133 def poll(self):
134 self.mutex.lock()
135 items = self.queue[:]
136 self.queue[:] = []
137 self.mutex.unlock()
138 return items
141class SlinkAcquisition(
142 slink.SlowSlink, AcquisitionThread):
144 def __init__(self, *args, **kwargs):
145 slink.SlowSlink.__init__(self, *args, **kwargs)
146 AcquisitionThread.__init__(self)
148 def got_trace(self, tr):
149 AcquisitionThread.got_trace(self, tr)
152class CamAcquisition(
153 serial_hamster.CamSerialHamster, AcquisitionThread):
155 def __init__(self, *args, **kwargs):
156 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
157 AcquisitionThread.__init__(self, post_process_sleep=0.1)
159 def got_trace(self, tr):
160 AcquisitionThread.got_trace(self, tr)
163class USBHB628Acquisition(
164 serial_hamster.USBHB628Hamster, AcquisitionThread):
166 def __init__(self, deltat=0.02, *args, **kwargs):
167 serial_hamster.USBHB628Hamster.__init__(
168 self, deltat=deltat, *args, **kwargs)
169 AcquisitionThread.__init__(self)
171 def got_trace(self, tr):
172 AcquisitionThread.got_trace(self, tr)
175class SchoolSeismometerAcquisition(
176 serial_hamster.SerialHamster, AcquisitionThread):
178 def __init__(self, *args, **kwargs):
179 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
180 AcquisitionThread.__init__(self, post_process_sleep=0.01)
182 def got_trace(self, tr):
183 AcquisitionThread.got_trace(self, tr)
186class EDLAcquisition(
187 edl.EDLHamster, AcquisitionThread):
189 def __init__(self, *args, **kwargs):
190 edl.EDLHamster.__init__(self, *args, **kwargs)
191 AcquisitionThread.__init__(self)
193 def got_trace(self, tr):
194 AcquisitionThread.got_trace(self, tr)
197class CubeAcquisition(
198 datacube.SerialCube, AcquisitionThread):
200 def __init__(self, *args, **kwargs):
201 datacube.SerialCube.__init__(self, *args, **kwargs)
202 AcquisitionThread.__init__(self)
204 def got_trace(self, tr):
205 AcquisitionThread.got_trace(self, tr)
208def setup_acquisition_sources(args):
210 sources = []
211 iarg = 0
212 while iarg < len(args):
213 arg = args[iarg]
215 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
216 mca = re.match(r'cam://([^:]+)', arg)
217 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
218 msc = re.match(r'school://([^:]+)', arg)
219 med = re.match(r'edl://([^:]+)', arg)
220 mcu = re.match(r'cube://([^:]+)', arg)
222 if msl:
223 host = msl.group(1)
224 port = msl.group(3)
225 if not port:
226 port = '18000'
228 sl = SlinkAcquisition(host=host, port=port)
229 if msl.group(5):
230 stream_patterns = msl.group(5).split(',')
232 if '_' not in msl.group(5):
233 try:
234 streams = sl.query_streams()
235 except slink.SlowSlinkError as e:
236 logger.fatal(str(e))
237 sys.exit(1)
239 streams = list(set(
240 util.match_nslcs(stream_patterns, streams)))
242 for stream in streams:
243 sl.add_stream(*stream)
244 else:
245 for stream in stream_patterns:
246 sl.add_raw_stream_selector(stream)
248 sources.append(sl)
249 elif mca:
250 port = mca.group(1)
251 cam = CamAcquisition(port=port, deltat=0.0314504)
252 sources.append(cam)
253 elif mus:
254 port = mus.group(1)
255 try:
256 d = {}
257 if mus.group(3):
258 d = dict(urlparse.parse_qsl(mus.group(3))) # noqa
260 deltat = 1.0/float(d.get('rate', '50'))
261 channels = [(int(c), c) for c in d.get('channels', '01234567')]
262 hb628 = USBHB628Acquisition(
263 port=port,
264 deltat=deltat,
265 channels=channels,
266 buffersize=16,
267 lookback=50)
269 sources.append(hb628)
270 except Exception:
271 raise
272 sys.exit('invalid acquisition source: %s' % arg)
274 elif msc:
275 port = msc.group(1)
276 sco = SchoolSeismometerAcquisition(port=port)
277 sources.append(sco)
278 elif med:
279 port = med.group(1)
280 edl = EDLAcquisition(port=port)
281 sources.append(edl)
282 elif mcu:
283 device = mcu.group(1)
284 cube = CubeAcquisition(device=device)
285 sources.append(cube)
287 if msl or mca or mus or msc or med or mcu:
288 args.pop(iarg)
289 else:
290 iarg += 1
292 return sources
295class PollInjector(qc.QObject):
297 def __init__(self, *args, **kwargs):
298 qc.QObject.__init__(self)
299 self._injector = pile.Injector(*args, **kwargs)
300 self._sources = []
301 self.startTimer(1000)
303 def add_source(self, source):
304 self._sources.append(source)
306 def remove_source(self, source):
307 self._sources.remove(source)
309 def timerEvent(self, ev):
310 for source in self._sources:
311 trs = source.poll()
312 for tr in trs:
313 self._injector.inject(tr)
315 # following methods needed because mulitple inheritance does not seem
316 # to work anymore with QObject in Python3 or PyQt5
318 def set_fixation_length(self, length):
319 return self._injector.set_fixation_length(length)
321 def set_save_path(
322 self,
323 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
324 '%(tmin)s_%(tmax)s.mseed'):
326 return self._injector.set_save_path(path)
328 def fixate_all(self):
329 return self._injector.fixate_all()
331 def free(self):
332 return self._injector.free()
335class Connection(qc.QObject):
337 received = qc.pyqtSignal(object, object)
338 disconnected = qc.pyqtSignal(object)
340 def __init__(self, parent, sock):
341 qc.QObject.__init__(self, parent)
342 self.socket = sock
343 self.readyRead.connect(
344 self.handle_read)
345 self.disconnected.connect(
346 self.handle_disconnected)
347 self.nwanted = 8
348 self.reading_size = True
349 self.handler = None
350 self.nbytes_received = 0
351 self.nbytes_sent = 0
352 self.compressor = zlib.compressobj()
353 self.decompressor = zlib.decompressobj()
355 def handle_read(self):
356 while True:
357 navail = self.socket.bytesAvailable()
358 if navail < self.nwanted:
359 return
361 data = self.socket.read(self.nwanted)
362 self.nbytes_received += len(data)
363 if self.reading_size:
364 self.nwanted = struct.unpack('>Q', data)[0]
365 self.reading_size = False
366 else:
367 obj = pickle.loads(self.decompressor.decompress(data))
368 if obj is None:
369 self.socket.disconnectFromHost()
370 else:
371 self.handle_received(obj)
372 self.nwanted = 8
373 self.reading_size = True
375 def handle_received(self, obj):
376 self.received.emit(self, obj)
378 def ship(self, obj):
379 data = self.compressor.compress(pickle.dumps(obj))
380 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
381 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
382 self.socket.write(data)
383 self.socket.write(data_end)
384 self.nbytes_sent += len(data)+len(data_end) + 8
386 def handle_disconnected(self):
387 self.disconnected.emit(self)
389 def close(self):
390 self.socket.close()
393class ConnectionHandler(qc.QObject):
394 def __init__(self, parent):
395 qc.QObject.__init__(self, parent)
396 self.queue = []
397 self.connection = None
399 def connected(self):
400 return self.connection is None
402 def set_connection(self, connection):
403 self.connection = connection
404 connection.received.connect(
405 self._handle_received)
407 connection.connect(
408 self.handle_disconnected)
410 for obj in self.queue:
411 self.connection.ship(obj)
413 self.queue = []
415 def _handle_received(self, conn, obj):
416 self.handle_received(obj)
418 def handle_received(self, obj):
419 pass
421 def handle_disconnected(self):
422 self.connection = None
424 def ship(self, obj):
425 if self.connection:
426 self.connection.ship(obj)
427 else:
428 self.queue.append(obj)
431class SimpleConnectionHandler(ConnectionHandler):
432 def __init__(self, parent, **mapping):
433 ConnectionHandler.__init__(self, parent)
434 self.mapping = mapping
436 def handle_received(self, obj):
437 command = obj[0]
438 args = obj[1:]
439 self.mapping[command](*args)
442class MyMainWindow(qw.QMainWindow):
444 def __init__(self, app, *args):
445 qg.QMainWindow.__init__(self, *args)
446 self.app = app
448 def keyPressEvent(self, ev):
449 self.app.pile_viewer.get_view().keyPressEvent(ev)
452class SnufflerTabs(qw.QTabWidget):
453 def __init__(self, parent):
454 qw.QTabWidget.__init__(self, parent)
455 if hasattr(self, 'setTabsClosable'):
456 self.setTabsClosable(True)
458 self.tabCloseRequested.connect(
459 self.removeTab)
461 if hasattr(self, 'setDocumentMode'):
462 self.setDocumentMode(True)
464 def hide_close_button_on_first_tab(self):
465 tbar = self.tabBar()
466 if hasattr(tbar, 'setTabButton'):
467 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
468 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
470 def append_tab(self, widget, name):
471 widget.setParent(self)
472 self.insertTab(self.count(), widget, name)
473 self.setCurrentIndex(self.count()-1)
475 def remove_tab(self, widget):
476 self.removeTab(self.indexOf(widget))
478 def tabInserted(self, index):
479 if index == 0:
480 self.hide_close_button_on_first_tab()
482 self.tabbar_visibility()
483 self.setFocus()
485 def removeTab(self, index):
486 w = self.widget(index)
487 w.close()
488 qw.QTabWidget.removeTab(self, index)
490 def tabRemoved(self, index):
491 self.tabbar_visibility()
493 def tabbar_visibility(self):
494 if self.count() <= 1:
495 self.tabBar().hide()
496 elif self.count() > 1:
497 self.tabBar().show()
499 def keyPressEvent(self, event):
500 if event.text() == 'd':
501 i = self.currentIndex()
502 if i != 0:
503 self.tabCloseRequested.emit(i)
504 else:
505 self.parent().keyPressEvent(event)
508class SnufflerStartWizard(qw.QWizard):
510 def __init__(self, parent):
511 qw.QWizard.__init__(self, parent)
513 self.setOption(self.NoBackButtonOnStartPage)
514 self.setOption(self.NoBackButtonOnLastPage)
515 self.setOption(self.NoCancelButton)
516 self.addPageSurvey()
517 self.addPageHelp()
518 self.setWindowTitle('Welcome to Pyrocko')
520 def getSystemInfo(self):
521 import numpy
522 import scipy
523 import pyrocko
524 import platform
525 import uuid
526 data = {
527 'node-uuid': uuid.getnode(),
528 'platform.architecture': platform.architecture(),
529 'platform.system': platform.system(),
530 'platform.release': platform.release(),
531 'python': platform.python_version(),
532 'pyrocko': pyrocko.__version__,
533 'numpy': numpy.__version__,
534 'scipy': scipy.__version__,
535 'qt': qc.PYQT_VERSION_STR,
536 }
537 return data
539 def addPageSurvey(self):
540 import pprint
541 webtk = 'DSFGK234ADF4ASDF'
542 sys_info = self.getSystemInfo()
544 p = qw.QWizardPage()
545 p.setCommitPage(True)
546 p.setTitle('Thank you for installing Pyrocko!')
548 lyt = qw.QVBoxLayout()
549 lyt.addWidget(qw.QLabel(
550 '<p>Your feedback is important for'
551 ' the development and improvement of Pyrocko.</p>'
552 '<p>Do you want to send this system information anon'
553 'ymously to <a href="https://pyrocko.org">'
554 'https://pyrocko.org</a>?</p>'))
556 text_data = qw.QLabel(
557 '<code style="font-size: small;">%s</code>' %
558 pprint.pformat(
559 sys_info,
560 indent=1).replace('\n', '<br>')
561 )
562 text_data.setStyleSheet('padding: 10px;')
563 lyt.addWidget(text_data)
565 lyt.addWidget(qw.QLabel(
566 'This message won\'t be shown again.\n\n'
567 'We appreciate your contribution!\n- The Pyrocko Developers'
568 ))
570 p.setLayout(lyt)
571 p.setButtonText(self.CommitButton, 'No')
573 yes_btn = qw.QPushButton(p)
574 yes_btn.setText('Yes')
576 @qc.pyqtSlot()
577 def send_data():
578 import requests
579 import json
580 try:
581 requests.post('https://pyrocko.org/%s' % webtk,
582 data=json.dumps(sys_info))
583 except Exception as e:
584 print(e)
585 self.button(self.NextButton).clicked.emit(True)
587 self.customButtonClicked.connect(send_data)
589 self.setButton(self.CustomButton1, yes_btn)
590 self.setOption(self.HaveCustomButton1, True)
592 self.addPage(p)
593 return p
595 def addPageHelp(self):
596 p = qw.QWizardPage()
597 p.setTitle('Welcome to Snuffler!')
599 text = qw.QLabel('''<html>
600<h3>- <i>The Seismogram browser and workbench.</i></h3>
601<p>Looks like you are starting the Snuffler for the first time.<br>
602It allows you to browse and process large archives of waveform data.</p>
603<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
604<ul>
605 <li><b>Download seismograms</b> from Geofon, IRIS and others</li>
606 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
607 <li><b>Cake</b>, Calculate synthetic arrival times</li>
608 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
609 <li>
610 <b>Map</b>, swiftly inspect stations and events on interactive maps
611 </li>
612</ul>
613<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
614<p><b>NOTE:</b><br>If you installed snufflings from the
615<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
616snufflings repository</a><br>you also have to pull an update from there.
617</p>
618<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
619 align="center">
620 <b>You can always press <code>?</code> for help!</b>
621</p>
622</html>''')
624 lyt = qw.QVBoxLayout()
625 lyt.addWidget(text)
627 def remove_custom_button():
628 self.setOption(self.HaveCustomButton1, False)
630 p.initializePage = remove_custom_button
632 p.setLayout(lyt)
633 self.addPage(p)
634 return p
637class SnufflerWindow(qw.QMainWindow):
639 def __init__(
640 self, pile, stations=None, events=None, markers=None, ntracks=12,
641 marker_editor_sortable=True, follow=None, controls=True,
642 opengl=None, instant_close=False):
644 qw.QMainWindow.__init__(self)
646 self.instant_close = instant_close
648 self.dockwidget_to_toggler = {}
649 self.dockwidgets = []
651 self.setWindowTitle("Snuffler")
653 self.pile_viewer = pile_viewer.PileViewer(
654 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
655 marker_editor_sortable=marker_editor_sortable,
656 panel_parent=self)
658 self.marker_editor = self.pile_viewer.marker_editor()
659 self.add_panel(
660 'Markers', self.marker_editor, visible=False,
661 where=qc.Qt.RightDockWidgetArea)
662 if stations:
663 self.get_view().add_stations(stations)
665 if events:
666 self.get_view().add_events(events)
668 if len(events) == 1:
669 self.get_view().set_active_event(events[0])
671 if markers:
672 self.get_view().add_markers(markers)
673 self.get_view().associate_phases_to_events()
675 self.tabs = SnufflerTabs(self)
676 self.setCentralWidget(self.tabs)
677 self.add_tab('Main', self.pile_viewer)
679 self.pile_viewer.setup_snufflings()
680 self.setMenuBar(self.pile_viewer.menu)
682 self.main_controls = self.pile_viewer.controls()
683 self.add_panel('Main Controls', self.main_controls, visible=controls)
684 self.show()
686 self.get_view().setFocus(qc.Qt.OtherFocusReason)
688 sb = self.statusBar()
689 sb.clearMessage()
690 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
692 snuffler_config = self.pile_viewer.viewer.config
694 if snuffler_config.first_start:
695 wizard = SnufflerStartWizard(self)
697 @qc.pyqtSlot()
698 def wizard_finished(result):
699 if result == wizard.Accepted:
700 snuffler_config.first_start = False
701 config.write_config(snuffler_config, 'snuffler')
703 wizard.finished.connect(wizard_finished)
705 wizard.show()
707 if follow:
708 self.get_view().follow(float(follow))
710 self.closing = False
712 def sizeHint(self):
713 return qc.QSize(1024, 768)
714 # return qc.QSize(800, 600) # used for screen shots in tutorial
716 def keyPressEvent(self, ev):
717 self.get_view().keyPressEvent(ev)
719 def get_view(self):
720 return self.pile_viewer.get_view()
722 def get_panel_parent_widget(self):
723 return self
725 def add_tab(self, name, widget):
726 self.tabs.append_tab(widget, name)
728 def remove_tab(self, widget):
729 self.tabs.remove_tab(widget)
731 def add_panel(self, name, panel, visible=False, volatile=False,
732 where=qc.Qt.BottomDockWidgetArea):
734 if not self.dockwidgets:
735 self.dockwidgets = []
737 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
739 dockwidget = qw.QDockWidget(name, self)
740 self.dockwidgets.append(dockwidget)
741 dockwidget.setWidget(panel)
742 panel.setParent(dockwidget)
743 self.addDockWidget(where, dockwidget)
745 if dws:
746 self.tabifyDockWidget(dws[-1], dockwidget)
748 self.toggle_panel(dockwidget, visible)
750 mitem = qw.QAction(name, None)
752 def toggle_panel(checked):
753 self.toggle_panel(dockwidget, True)
755 mitem.triggered.connect(toggle_panel)
757 if volatile:
758 def visibility(visible):
759 if not visible:
760 self.remove_panel(panel)
762 dockwidget.visibilityChanged.connect(
763 visibility)
765 self.get_view().add_panel_toggler(mitem)
766 self.dockwidget_to_toggler[dockwidget] = mitem
768 if pile_viewer.is_macos:
769 tabbars = self.findChildren(qw.QTabBar)
770 for tabbar in tabbars:
771 tabbar.setShape(qw.QTabBar.TriangularNorth)
772 tabbar.setDocumentMode(True)
774 def toggle_panel(self, dockwidget, visible):
775 if visible is None:
776 visible = not dockwidget.isVisible()
778 dockwidget.setVisible(visible)
779 if visible:
780 w = dockwidget.widget()
781 minsize = w.minimumSize()
782 w.setMinimumHeight(w.sizeHint().height() + 5)
784 def reset_minimum_size():
785 import sip
786 if not sip.isdeleted(w):
787 w.setMinimumSize(minsize)
789 qc.QTimer.singleShot(200, reset_minimum_size)
791 dockwidget.setFocus()
792 dockwidget.raise_()
794 def toggle_marker_editor(self):
795 self.toggle_panel(self.marker_editor.parent(), None)
797 def toggle_main_controls(self):
798 self.toggle_panel(self.main_controls.parent(), None)
800 def remove_panel(self, panel):
801 dockwidget = panel.parent()
802 self.removeDockWidget(dockwidget)
803 dockwidget.setParent(None)
804 mitem = self.dockwidget_to_toggler[dockwidget]
805 self.get_view().remove_panel_toggler(mitem)
807 def return_tag(self):
808 return self.get_view().return_tag
810 def confirm_close(self):
811 ret = qw.QMessageBox.question(
812 self,
813 'Snuffler',
814 'Close Snuffler window?',
815 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
816 qw.QMessageBox.Ok)
818 return ret == qw.QMessageBox.Ok
820 def closeEvent(self, event):
821 if self.instant_close or self.confirm_close():
822 self.closing = True
823 self.pile_viewer.cleanup()
824 event.accept()
825 else:
826 event.ignore()
828 def is_closing(self):
829 return self.closing
832class Snuffler(qw.QApplication):
834 def __init__(self):
835 qw.QApplication.__init__(self, [])
836 self.setApplicationName('Snuffler')
837 self.setApplicationDisplayName('Snuffler')
838 self.lastWindowClosed.connect(self.myQuit)
839 self.server = None
840 self.loader = None
842 def install_sigint_handler(self):
843 self._old_signal_handler = signal.signal(
844 signal.SIGINT,
845 self.myCloseAllWindows)
847 def uninstall_sigint_handler(self):
848 signal.signal(signal.SIGINT, self._old_signal_handler)
850 def snuffler_windows(self):
851 return [w for w in self.topLevelWidgets()
852 if isinstance(w, SnufflerWindow) and not w.is_closing()]
854 def event(self, e):
855 if isinstance(e, qg.QFileOpenEvent):
856 path = str(e.file())
857 if path != sys.argv[0]:
858 wins = self.snuffler_windows()
859 if wins:
860 wins[0].get_view().load_soon([path])
862 return True
863 else:
864 return qw.QApplication.event(self, e)
866 def load(self, pathes, cachedirname, pattern, format):
867 if not self.loader:
868 self.start_loader()
870 self.loader.ship(
871 ('load', pathes, cachedirname, pattern, format))
873 def update_progress(self, task, percent):
874 self.pile_viewer.progressbars.set_status(task, percent)
876 def myCloseAllWindows(self, *args):
878 def confirm():
879 try:
880 print('\nQuit Snuffler? [y/n]', file=sys.stderr)
881 confirmed = getch() == 'y'
882 if not confirmed:
883 print('Continuing.', file=sys.stderr)
884 else:
885 print('Quitting Snuffler.', file=sys.stderr)
887 return confirmed
889 except Exception:
890 return False
892 if confirm():
893 for win in self.snuffler_windows():
894 win.instant_close = True
896 self.closeAllWindows()
898 def myQuit(self, *args):
899 self.quit()