1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
5'''
6Effective seismological trace viewer.
7'''
9import sys
10import signal
11import logging
12import time
13import re
14import zlib
15import struct
16import pickle
19from pyrocko.streaming import serial_hamster
20from pyrocko.streaming import slink
21from pyrocko.streaming import edl
22from pyrocko.streaming import datacube
24from pyrocko import pile # noqa
25from pyrocko import util # noqa
26from pyrocko import model # noqa
27from pyrocko import config # noqa
28from pyrocko import io # noqa
30from . import pile_viewer # noqa
32from .qt_compat import qc, qg, qw
34logger = logging.getLogger('pyrocko.gui.snuffler_app')
37class _Getch:
38 '''
39 Gets a single character from standard input.
41 Does not echo to the screen.
43 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
44 '''
45 def __init__(self):
46 try:
47 self.impl = _GetchWindows()
48 except ImportError:
49 self.impl = _GetchUnix()
51 def __call__(self): return self.impl()
54class _GetchUnix:
55 def __init__(self):
56 import tty, sys # noqa
58 def __call__(self):
59 import sys
60 import tty
61 import termios
63 fd = sys.stdin.fileno()
64 old_settings = termios.tcgetattr(fd)
65 try:
66 tty.setraw(fd)
67 ch = sys.stdin.read(1)
68 finally:
69 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
71 return ch
74class _GetchWindows:
75 def __init__(self):
76 import msvcrt # noqa
78 def __call__(self):
79 import msvcrt
80 return msvcrt.getch()
83getch = _Getch()
86class AcquisitionThread(qc.QThread):
87 def __init__(self, post_process_sleep=0.0):
88 qc.QThread.__init__(self)
89 self.mutex = qc.QMutex()
90 self.queue = []
91 self.post_process_sleep = post_process_sleep
92 self._sun_is_shining = True
94 def run(self):
95 while True:
96 try:
97 self.acquisition_start()
98 while self._sun_is_shining:
99 t0 = time.time()
100 self.process()
101 t1 = time.time()
102 if self.post_process_sleep != 0.0:
103 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
105 self.acquisition_stop()
106 break
108 except (
109 edl.ReadError,
110 serial_hamster.SerialHamsterError,
111 slink.SlowSlinkError) as e:
113 logger.error(str(e))
114 logger.error('Acquistion terminated, restart in 5 s')
115 self.acquisition_stop()
116 time.sleep(5)
117 if not self._sun_is_shining:
118 break
120 def stop(self):
121 self._sun_is_shining = False
123 logger.debug('Waiting for thread to terminate...')
124 self.wait()
125 logger.debug('Thread has terminated.')
127 def got_trace(self, tr):
128 self.mutex.lock()
129 self.queue.append(tr)
130 self.mutex.unlock()
132 def poll(self):
133 self.mutex.lock()
134 items = self.queue[:]
135 self.queue[:] = []
136 self.mutex.unlock()
137 return items
140class SlinkAcquisition(
141 slink.SlowSlink, AcquisitionThread):
143 def __init__(self, *args, **kwargs):
144 slink.SlowSlink.__init__(self, *args, **kwargs)
145 AcquisitionThread.__init__(self)
147 def got_trace(self, tr):
148 AcquisitionThread.got_trace(self, tr)
151class CamAcquisition(
152 serial_hamster.CamSerialHamster, AcquisitionThread):
154 def __init__(self, *args, **kwargs):
155 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
156 AcquisitionThread.__init__(self, post_process_sleep=0.1)
158 def got_trace(self, tr):
159 AcquisitionThread.got_trace(self, tr)
162class USBHB628Acquisition(
163 serial_hamster.USBHB628Hamster, AcquisitionThread):
165 def __init__(self, deltat=0.02, *args, **kwargs):
166 serial_hamster.USBHB628Hamster.__init__(
167 self, deltat=deltat, *args, **kwargs)
168 AcquisitionThread.__init__(self)
170 def got_trace(self, tr):
171 AcquisitionThread.got_trace(self, tr)
174class SchoolSeismometerAcquisition(
175 serial_hamster.SerialHamster, AcquisitionThread):
177 def __init__(self, *args, **kwargs):
178 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
179 AcquisitionThread.__init__(self, post_process_sleep=0.01)
181 def got_trace(self, tr):
182 AcquisitionThread.got_trace(self, tr)
185class EDLAcquisition(
186 edl.EDLHamster, AcquisitionThread):
188 def __init__(self, *args, **kwargs):
189 edl.EDLHamster.__init__(self, *args, **kwargs)
190 AcquisitionThread.__init__(self)
192 def got_trace(self, tr):
193 AcquisitionThread.got_trace(self, tr)
196class CubeAcquisition(
197 datacube.SerialCube, AcquisitionThread):
199 def __init__(self, *args, **kwargs):
200 datacube.SerialCube.__init__(self, *args, **kwargs)
201 AcquisitionThread.__init__(self)
203 def got_trace(self, tr):
204 AcquisitionThread.got_trace(self, tr)
207def setup_acquisition_sources(args):
209 sources = []
210 iarg = 0
211 while iarg < len(args):
212 arg = args[iarg]
214 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
215 mca = re.match(r'cam://([^:]+)', arg)
216 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
217 msc = re.match(r'school://([^:]+)', arg)
218 med = re.match(r'edl://([^:]+)', arg)
219 mcu = re.match(r'cube://([^:]+)', arg)
221 if msl:
222 host = msl.group(1)
223 port = msl.group(3)
224 if not port:
225 port = '18000'
227 sl = SlinkAcquisition(host=host, port=port)
228 if msl.group(5):
229 stream_patterns = msl.group(5).split(',')
231 if '_' not in msl.group(5):
232 try:
233 streams = sl.query_streams()
234 except slink.SlowSlinkError as e:
235 logger.fatal(str(e))
236 sys.exit(1)
238 streams = list(set(
239 util.match_nslcs(stream_patterns, streams)))
241 for stream in streams:
242 sl.add_stream(*stream)
243 else:
244 for stream in stream_patterns:
245 sl.add_raw_stream_selector(stream)
247 sources.append(sl)
248 elif mca:
249 port = mca.group(1)
250 cam = CamAcquisition(port=port, deltat=0.0314504)
251 sources.append(cam)
252 elif mus:
253 port = mus.group(1)
254 try:
255 d = {}
256 if mus.group(3):
257 d = dict(urlparse.parse_qsl(mus.group(3))) # noqa
259 deltat = 1.0/float(d.get('rate', '50'))
260 channels = [(int(c), c) for c in d.get('channels', '01234567')]
261 hb628 = USBHB628Acquisition(
262 port=port,
263 deltat=deltat,
264 channels=channels,
265 buffersize=16,
266 lookback=50)
268 sources.append(hb628)
269 except Exception:
270 raise
271 sys.exit('invalid acquisition source: %s' % arg)
273 elif msc:
274 port = msc.group(1)
275 sco = SchoolSeismometerAcquisition(port=port)
276 sources.append(sco)
277 elif med:
278 port = med.group(1)
279 edl = EDLAcquisition(port=port)
280 sources.append(edl)
281 elif mcu:
282 device = mcu.group(1)
283 cube = CubeAcquisition(device=device)
284 sources.append(cube)
286 if msl or mca or mus or msc or med or mcu:
287 args.pop(iarg)
288 else:
289 iarg += 1
291 return sources
294class PollInjector(qc.QObject):
296 def __init__(self, *args, **kwargs):
297 qc.QObject.__init__(self)
298 self._injector = pile.Injector(*args, **kwargs)
299 self._sources = []
300 self.startTimer(1000)
302 def add_source(self, source):
303 self._sources.append(source)
305 def remove_source(self, source):
306 self._sources.remove(source)
308 def timerEvent(self, ev):
309 for source in self._sources:
310 trs = source.poll()
311 for tr in trs:
312 self._injector.inject(tr)
314 # following methods needed because mulitple inheritance does not seem
315 # to work anymore with QObject in Python3 or PyQt5
317 def set_fixation_length(self, length):
318 return self._injector.set_fixation_length(length)
320 def set_save_path(
321 self,
322 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
323 '%(tmin)s_%(tmax)s.mseed'):
325 return self._injector.set_save_path(path)
327 def fixate_all(self):
328 return self._injector.fixate_all()
330 def free(self):
331 return self._injector.free()
334class Connection(qc.QObject):
336 received = qc.pyqtSignal(object, object)
337 disconnected = qc.pyqtSignal(object)
339 def __init__(self, parent, sock):
340 qc.QObject.__init__(self, parent)
341 self.socket = sock
342 self.readyRead.connect(
343 self.handle_read)
344 self.disconnected.connect(
345 self.handle_disconnected)
346 self.nwanted = 8
347 self.reading_size = True
348 self.handler = None
349 self.nbytes_received = 0
350 self.nbytes_sent = 0
351 self.compressor = zlib.compressobj()
352 self.decompressor = zlib.decompressobj()
354 def handle_read(self):
355 while True:
356 navail = self.socket.bytesAvailable()
357 if navail < self.nwanted:
358 return
360 data = self.socket.read(self.nwanted)
361 self.nbytes_received += len(data)
362 if self.reading_size:
363 self.nwanted = struct.unpack('>Q', data)[0]
364 self.reading_size = False
365 else:
366 obj = pickle.loads(self.decompressor.decompress(data))
367 if obj is None:
368 self.socket.disconnectFromHost()
369 else:
370 self.handle_received(obj)
371 self.nwanted = 8
372 self.reading_size = True
374 def handle_received(self, obj):
375 self.received.emit(self, obj)
377 def ship(self, obj):
378 data = self.compressor.compress(pickle.dumps(obj))
379 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
380 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
381 self.socket.write(data)
382 self.socket.write(data_end)
383 self.nbytes_sent += len(data)+len(data_end) + 8
385 def handle_disconnected(self):
386 self.disconnected.emit(self)
388 def close(self):
389 self.socket.close()
392class ConnectionHandler(qc.QObject):
393 def __init__(self, parent):
394 qc.QObject.__init__(self, parent)
395 self.queue = []
396 self.connection = None
398 def connected(self):
399 return self.connection is None
401 def set_connection(self, connection):
402 self.connection = connection
403 connection.received.connect(
404 self._handle_received)
406 connection.connect(
407 self.handle_disconnected)
409 for obj in self.queue:
410 self.connection.ship(obj)
412 self.queue = []
414 def _handle_received(self, conn, obj):
415 self.handle_received(obj)
417 def handle_received(self, obj):
418 pass
420 def handle_disconnected(self):
421 self.connection = None
423 def ship(self, obj):
424 if self.connection:
425 self.connection.ship(obj)
426 else:
427 self.queue.append(obj)
430class SimpleConnectionHandler(ConnectionHandler):
431 def __init__(self, parent, **mapping):
432 ConnectionHandler.__init__(self, parent)
433 self.mapping = mapping
435 def handle_received(self, obj):
436 command = obj[0]
437 args = obj[1:]
438 self.mapping[command](*args)
441class MyMainWindow(qw.QMainWindow):
443 def __init__(self, app, *args):
444 qg.QMainWindow.__init__(self, *args)
445 self.app = app
447 def keyPressEvent(self, ev):
448 self.app.pile_viewer.get_view().keyPressEvent(ev)
451class SnufflerTabs(qw.QTabWidget):
452 def __init__(self, parent):
453 qw.QTabWidget.__init__(self, parent)
454 if hasattr(self, 'setTabsClosable'):
455 self.setTabsClosable(True)
457 self.tabCloseRequested.connect(
458 self.removeTab)
460 if hasattr(self, 'setDocumentMode'):
461 self.setDocumentMode(True)
463 def hide_close_button_on_first_tab(self):
464 tbar = self.tabBar()
465 if hasattr(tbar, 'setTabButton'):
466 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
467 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
469 def append_tab(self, widget, name):
470 widget.setParent(self)
471 self.insertTab(self.count(), widget, name)
472 self.setCurrentIndex(self.count()-1)
474 def remove_tab(self, widget):
475 self.removeTab(self.indexOf(widget))
477 def tabInserted(self, index):
478 if index == 0:
479 self.hide_close_button_on_first_tab()
481 self.tabbar_visibility()
482 self.setFocus()
484 def removeTab(self, index):
485 w = self.widget(index)
486 w.close()
487 qw.QTabWidget.removeTab(self, index)
489 def tabRemoved(self, index):
490 self.tabbar_visibility()
492 def tabbar_visibility(self):
493 if self.count() <= 1:
494 self.tabBar().hide()
495 elif self.count() > 1:
496 self.tabBar().show()
498 def keyPressEvent(self, event):
499 if event.text() == 'd':
500 i = self.currentIndex()
501 if i != 0:
502 self.tabCloseRequested.emit(i)
503 else:
504 self.parent().keyPressEvent(event)
507class SnufflerStartWizard(qw.QWizard):
509 def __init__(self, parent):
510 qw.QWizard.__init__(self, parent)
512 self.setOption(self.NoBackButtonOnStartPage)
513 self.setOption(self.NoBackButtonOnLastPage)
514 self.setOption(self.NoCancelButton)
515 self.addPageSurvey()
516 self.addPageHelp()
517 self.setWindowTitle('Welcome to Pyrocko')
519 def getSystemInfo(self):
520 import numpy
521 import scipy
522 import pyrocko
523 import platform
524 import uuid
525 data = {
526 'node-uuid': uuid.getnode(),
527 'platform.architecture': platform.architecture(),
528 'platform.system': platform.system(),
529 'platform.release': platform.release(),
530 'python': platform.python_version(),
531 'pyrocko': pyrocko.__version__,
532 'numpy': numpy.__version__,
533 'scipy': scipy.__version__,
534 'qt': qc.PYQT_VERSION_STR,
535 }
536 return data
538 def addPageSurvey(self):
539 import pprint
540 webtk = 'DSFGK234ADF4ASDF'
541 sys_info = self.getSystemInfo()
543 p = qw.QWizardPage()
544 p.setCommitPage(True)
545 p.setTitle('Thank you for installing Pyrocko!')
547 lyt = qw.QVBoxLayout()
548 lyt.addWidget(qw.QLabel(
549 '<p>Your feedback is important for'
550 ' the development and improvement of Pyrocko.</p>'
551 '<p>Do you want to send this system information anon'
552 'ymously to <a href="https://pyrocko.org">'
553 'https://pyrocko.org</a>?</p>'))
555 text_data = qw.QLabel(
556 '<code style="font-size: small;">%s</code>' %
557 pprint.pformat(
558 sys_info,
559 indent=1).replace('\n', '<br>')
560 )
561 text_data.setStyleSheet('padding: 10px;')
562 lyt.addWidget(text_data)
564 lyt.addWidget(qw.QLabel(
565 "This message won't be shown again.\n\n"
566 'We appreciate your contribution!\n- The Pyrocko Developers'
567 ))
569 p.setLayout(lyt)
570 p.setButtonText(self.CommitButton, 'No')
572 yes_btn = qw.QPushButton(p)
573 yes_btn.setText('Yes')
575 @qc.pyqtSlot()
576 def send_data():
577 import requests
578 import json
579 try:
580 requests.post('https://pyrocko.org/%s' % webtk,
581 data=json.dumps(sys_info))
582 except Exception as e:
583 print(e)
584 self.button(self.NextButton).clicked.emit(True)
586 self.customButtonClicked.connect(send_data)
588 self.setButton(self.CustomButton1, yes_btn)
589 self.setOption(self.HaveCustomButton1, True)
591 self.addPage(p)
592 return p
594 def addPageHelp(self):
595 p = qw.QWizardPage()
596 p.setTitle('Welcome to Snuffler!')
598 text = qw.QLabel('''<html>
599<h3>- <i>The Seismogram browser and workbench.</i></h3>
600<p>Looks like you are starting the Snuffler for the first time.<br>
601It allows you to browse and process large archives of waveform data.</p>
602<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
603<ul>
604 <li><b>Download seismograms</b> from Geofon, IRIS and others</li>
605 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
606 <li><b>Cake</b>, Calculate synthetic arrival times</li>
607 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
608 <li>
609 <b>Map</b>, swiftly inspect stations and events on interactive maps
610 </li>
611</ul>
612<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
613<p><b>NOTE:</b><br>If you installed snufflings from the
614<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
615snufflings repository</a><br>you also have to pull an update from there.
616</p>
617<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
618 align="center">
619 <b>You can always press <code>?</code> for help!</b>
620</p>
621</html>''')
623 lyt = qw.QVBoxLayout()
624 lyt.addWidget(text)
626 def remove_custom_button():
627 self.setOption(self.HaveCustomButton1, False)
629 p.initializePage = remove_custom_button
631 p.setLayout(lyt)
632 self.addPage(p)
633 return p
636class SnufflerWindow(qw.QMainWindow):
638 def __init__(
639 self, pile, stations=None, events=None, markers=None, ntracks=12,
640 marker_editor_sortable=True, follow=None, controls=True,
641 opengl=None, instant_close=False):
643 qw.QMainWindow.__init__(self)
645 self.instant_close = instant_close
647 self.dockwidget_to_toggler = {}
648 self.dockwidgets = []
650 self.setWindowTitle('Snuffler')
652 self.pile_viewer = pile_viewer.PileViewer(
653 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
654 marker_editor_sortable=marker_editor_sortable,
655 panel_parent=self)
657 self.marker_editor = self.pile_viewer.marker_editor()
658 self.add_panel(
659 'Markers', self.marker_editor, visible=False,
660 where=qc.Qt.RightDockWidgetArea)
661 if stations:
662 self.get_view().add_stations(stations)
664 if events:
665 self.get_view().add_events(events)
667 if len(events) == 1:
668 self.get_view().set_active_event(events[0])
670 if markers:
671 self.get_view().add_markers(markers)
672 self.get_view().associate_phases_to_events()
674 self.tabs = SnufflerTabs(self)
675 self.setCentralWidget(self.tabs)
676 self.add_tab('Main', self.pile_viewer)
678 self.pile_viewer.setup_snufflings()
679 self.setMenuBar(self.pile_viewer.menu)
681 self.main_controls = self.pile_viewer.controls()
682 self.add_panel('Main Controls', self.main_controls, visible=controls)
683 self.show()
685 self.get_view().setFocus(qc.Qt.OtherFocusReason)
687 sb = self.statusBar()
688 sb.clearMessage()
689 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
691 snuffler_config = self.pile_viewer.viewer.config
693 if snuffler_config.first_start:
694 wizard = SnufflerStartWizard(self)
696 @qc.pyqtSlot()
697 def wizard_finished(result):
698 if result == wizard.Accepted:
699 snuffler_config.first_start = False
700 config.write_config(snuffler_config, 'snuffler')
702 wizard.finished.connect(wizard_finished)
704 wizard.show()
706 if follow:
707 self.get_view().follow(float(follow))
709 self.closing = False
711 def sizeHint(self):
712 return qc.QSize(1024, 768)
713 # return qc.QSize(800, 600) # used for screen shots in tutorial
715 def keyPressEvent(self, ev):
716 self.get_view().keyPressEvent(ev)
718 def get_view(self):
719 return self.pile_viewer.get_view()
721 def get_panel_parent_widget(self):
722 return self
724 def add_tab(self, name, widget):
725 self.tabs.append_tab(widget, name)
727 def remove_tab(self, widget):
728 self.tabs.remove_tab(widget)
730 def add_panel(self, name, panel, visible=False, volatile=False,
731 where=qc.Qt.BottomDockWidgetArea):
733 if not self.dockwidgets:
734 self.dockwidgets = []
736 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
738 dockwidget = qw.QDockWidget(name, self)
739 self.dockwidgets.append(dockwidget)
740 dockwidget.setWidget(panel)
741 panel.setParent(dockwidget)
742 self.addDockWidget(where, dockwidget)
744 if dws:
745 self.tabifyDockWidget(dws[-1], dockwidget)
747 self.toggle_panel(dockwidget, visible)
749 mitem = qw.QAction(name, None)
751 def toggle_panel(checked):
752 self.toggle_panel(dockwidget, True)
754 mitem.triggered.connect(toggle_panel)
756 if volatile:
757 def visibility(visible):
758 if not visible:
759 self.remove_panel(panel)
761 dockwidget.visibilityChanged.connect(
762 visibility)
764 self.get_view().add_panel_toggler(mitem)
765 self.dockwidget_to_toggler[dockwidget] = mitem
767 if pile_viewer.is_macos:
768 tabbars = self.findChildren(qw.QTabBar)
769 for tabbar in tabbars:
770 tabbar.setShape(qw.QTabBar.TriangularNorth)
771 tabbar.setDocumentMode(True)
773 def toggle_panel(self, dockwidget, visible):
774 if visible is None:
775 visible = not dockwidget.isVisible()
777 dockwidget.setVisible(visible)
778 if visible:
779 w = dockwidget.widget()
780 minsize = w.minimumSize()
781 w.setMinimumHeight(w.sizeHint().height() + 5)
783 def reset_minimum_size():
784 import sip
785 if not sip.isdeleted(w):
786 w.setMinimumSize(minsize)
788 qc.QTimer.singleShot(200, reset_minimum_size)
790 dockwidget.setFocus()
791 dockwidget.raise_()
793 def toggle_marker_editor(self):
794 self.toggle_panel(self.marker_editor.parent(), None)
796 def toggle_main_controls(self):
797 self.toggle_panel(self.main_controls.parent(), None)
799 def remove_panel(self, panel):
800 dockwidget = panel.parent()
801 self.removeDockWidget(dockwidget)
802 dockwidget.setParent(None)
803 mitem = self.dockwidget_to_toggler[dockwidget]
804 self.get_view().remove_panel_toggler(mitem)
806 def return_tag(self):
807 return self.get_view().return_tag
809 def confirm_close(self):
810 ret = qw.QMessageBox.question(
811 self,
812 'Snuffler',
813 'Close Snuffler window?',
814 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
815 qw.QMessageBox.Ok)
817 return ret == qw.QMessageBox.Ok
819 def closeEvent(self, event):
820 if self.instant_close or self.confirm_close():
821 self.closing = True
822 self.pile_viewer.cleanup()
823 event.accept()
824 else:
825 event.ignore()
827 def is_closing(self):
828 return self.closing
831class Snuffler(qw.QApplication):
833 def __init__(self):
834 qw.QApplication.__init__(self, [])
835 self.setApplicationName('Snuffler')
836 self.setApplicationDisplayName('Snuffler')
837 self.lastWindowClosed.connect(self.myQuit)
838 self.server = None
839 self.loader = None
841 def install_sigint_handler(self):
842 self._old_signal_handler = signal.signal(
843 signal.SIGINT,
844 self.myCloseAllWindows)
846 def uninstall_sigint_handler(self):
847 signal.signal(signal.SIGINT, self._old_signal_handler)
849 def snuffler_windows(self):
850 return [w for w in self.topLevelWidgets()
851 if isinstance(w, SnufflerWindow) and not w.is_closing()]
853 def event(self, e):
854 if isinstance(e, qg.QFileOpenEvent):
855 path = str(e.file())
856 if path != sys.argv[0]:
857 wins = self.snuffler_windows()
858 if wins:
859 wins[0].get_view().load_soon([path])
861 return True
862 else:
863 return qw.QApplication.event(self, e)
865 def load(self, pathes, cachedirname, pattern, format):
866 if not self.loader:
867 self.start_loader()
869 self.loader.ship(
870 ('load', pathes, cachedirname, pattern, format))
872 def update_progress(self, task, percent):
873 self.pile_viewer.progressbars.set_status(task, percent)
875 def myCloseAllWindows(self, *args):
877 def confirm():
878 try:
879 print('\nQuit Snuffler? [y/n]', file=sys.stderr)
880 confirmed = getch() == 'y'
881 if not confirmed:
882 print('Continuing.', file=sys.stderr)
883 else:
884 print('Quitting Snuffler.', file=sys.stderr)
886 return confirmed
888 except Exception:
889 return False
891 if confirm():
892 for win in self.snuffler_windows():
893 win.instant_close = True
895 self.closeAllWindows()
897 def myQuit(self, *args):
898 self.quit()