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 os
11import sys
12import signal
13import logging
14import time
15import re
16import zlib
17import struct
18import pickle
21from pyrocko.streaming import serial_hamster
22from pyrocko.streaming import slink
23from pyrocko.streaming import edl
24from pyrocko.streaming import datacube
26from pyrocko import pile # noqa
27from pyrocko import util # noqa
28from pyrocko import model # noqa
29from pyrocko import config # noqa
30from pyrocko import io # noqa
32from . import pile_viewer # noqa
34from .qt_compat import qc, qg, qw, qn
36logger = logging.getLogger('pyrocko.gui.snuffler_app')
39class _Getch:
40 '''
41 Gets a single character from standard input.
43 Does not echo to the screen.
45 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
46 '''
47 def __init__(self):
48 try:
49 self.impl = _GetchWindows()
50 except ImportError:
51 self.impl = _GetchUnix()
53 def __call__(self): return self.impl()
56class _GetchUnix:
57 def __init__(self):
58 import tty, sys # noqa
60 def __call__(self):
61 import sys
62 import tty
63 import termios
65 fd = sys.stdin.fileno()
66 old_settings = termios.tcgetattr(fd)
67 try:
68 tty.setraw(fd)
69 ch = sys.stdin.read(1)
70 finally:
71 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
73 return ch
76class _GetchWindows:
77 def __init__(self):
78 import msvcrt # noqa
80 def __call__(self):
81 import msvcrt
82 return msvcrt.getch()
85getch = _Getch()
88class AcquisitionThread(qc.QThread):
89 def __init__(self, post_process_sleep=0.0):
90 qc.QThread.__init__(self)
91 self.mutex = qc.QMutex()
92 self.queue = []
93 self.post_process_sleep = post_process_sleep
94 self._sun_is_shining = True
96 def run(self):
97 while True:
98 try:
99 self.acquisition_start()
100 while self._sun_is_shining:
101 t0 = time.time()
102 self.process()
103 t1 = time.time()
104 if self.post_process_sleep != 0.0:
105 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
107 self.acquisition_stop()
108 break
110 except (
111 edl.ReadError,
112 serial_hamster.SerialHamsterError,
113 slink.SlowSlinkError) as e:
115 logger.error(str(e))
116 logger.error('Acquistion terminated, restart in 5 s')
117 self.acquisition_stop()
118 time.sleep(5)
119 if not self._sun_is_shining:
120 break
122 def stop(self):
123 self._sun_is_shining = False
125 logger.debug("Waiting for thread to terminate...")
126 self.wait()
127 logger.debug("Thread has terminated.")
129 def got_trace(self, tr):
130 self.mutex.lock()
131 self.queue.append(tr)
132 self.mutex.unlock()
134 def poll(self):
135 self.mutex.lock()
136 items = self.queue[:]
137 self.queue[:] = []
138 self.mutex.unlock()
139 return items
142class SlinkAcquisition(
143 slink.SlowSlink, AcquisitionThread):
145 def __init__(self, *args, **kwargs):
146 slink.SlowSlink.__init__(self, *args, **kwargs)
147 AcquisitionThread.__init__(self)
149 def got_trace(self, tr):
150 AcquisitionThread.got_trace(self, tr)
153class CamAcquisition(
154 serial_hamster.CamSerialHamster, AcquisitionThread):
156 def __init__(self, *args, **kwargs):
157 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
158 AcquisitionThread.__init__(self, post_process_sleep=0.1)
160 def got_trace(self, tr):
161 AcquisitionThread.got_trace(self, tr)
164class USBHB628Acquisition(
165 serial_hamster.USBHB628Hamster, AcquisitionThread):
167 def __init__(self, deltat=0.02, *args, **kwargs):
168 serial_hamster.USBHB628Hamster.__init__(
169 self, deltat=deltat, *args, **kwargs)
170 AcquisitionThread.__init__(self)
172 def got_trace(self, tr):
173 AcquisitionThread.got_trace(self, tr)
176class SchoolSeismometerAcquisition(
177 serial_hamster.SerialHamster, AcquisitionThread):
179 def __init__(self, *args, **kwargs):
180 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
181 AcquisitionThread.__init__(self, post_process_sleep=0.01)
183 def got_trace(self, tr):
184 AcquisitionThread.got_trace(self, tr)
187class EDLAcquisition(
188 edl.EDLHamster, AcquisitionThread):
190 def __init__(self, *args, **kwargs):
191 edl.EDLHamster.__init__(self, *args, **kwargs)
192 AcquisitionThread.__init__(self)
194 def got_trace(self, tr):
195 AcquisitionThread.got_trace(self, tr)
198class CubeAcquisition(
199 datacube.SerialCube, AcquisitionThread):
201 def __init__(self, *args, **kwargs):
202 datacube.SerialCube.__init__(self, *args, **kwargs)
203 AcquisitionThread.__init__(self)
205 def got_trace(self, tr):
206 AcquisitionThread.got_trace(self, tr)
209def setup_acquisition_sources(args):
211 sources = []
212 iarg = 0
213 while iarg < len(args):
214 arg = args[iarg]
216 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
217 mca = re.match(r'cam://([^:]+)', arg)
218 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
219 msc = re.match(r'school://([^:]+)', arg)
220 med = re.match(r'edl://([^:]+)', arg)
221 mcu = re.match(r'cube://([^:]+)', arg)
223 if msl:
224 host = msl.group(1)
225 port = msl.group(3)
226 if not port:
227 port = '18000'
229 sl = SlinkAcquisition(host=host, port=port)
230 if msl.group(5):
231 stream_patterns = msl.group(5).split(',')
233 if '_' not in msl.group(5):
234 try:
235 streams = sl.query_streams()
236 except slink.SlowSlinkError as e:
237 logger.fatal(str(e))
238 sys.exit(1)
240 streams = list(set(
241 util.match_nslcs(stream_patterns, streams)))
243 for stream in streams:
244 sl.add_stream(*stream)
245 else:
246 for stream in stream_patterns:
247 sl.add_raw_stream_selector(stream)
249 sources.append(sl)
250 elif mca:
251 port = mca.group(1)
252 cam = CamAcquisition(port=port, deltat=0.0314504)
253 sources.append(cam)
254 elif mus:
255 port = mus.group(1)
256 try:
257 d = {}
258 if mus.group(3):
259 d = dict(urlparse.parse_qsl(mus.group(3))) # noqa
261 deltat = 1.0/float(d.get('rate', '50'))
262 channels = [(int(c), c) for c in d.get('channels', '01234567')]
263 hb628 = USBHB628Acquisition(
264 port=port,
265 deltat=deltat,
266 channels=channels,
267 buffersize=16,
268 lookback=50)
270 sources.append(hb628)
271 except Exception:
272 raise
273 sys.exit('invalid acquisition source: %s' % arg)
275 elif msc:
276 port = msc.group(1)
277 sco = SchoolSeismometerAcquisition(port=port)
278 sources.append(sco)
279 elif med:
280 port = med.group(1)
281 edl = EDLAcquisition(port=port)
282 sources.append(edl)
283 elif mcu:
284 device = mcu.group(1)
285 cube = CubeAcquisition(device=device)
286 sources.append(cube)
288 if msl or mca or mus or msc or med or mcu:
289 args.pop(iarg)
290 else:
291 iarg += 1
293 return sources
296class PollInjector(qc.QObject):
298 def __init__(self, *args, **kwargs):
299 qc.QObject.__init__(self)
300 self._injector = pile.Injector(*args, **kwargs)
301 self._sources = []
302 self.startTimer(1000)
304 def add_source(self, source):
305 self._sources.append(source)
307 def remove_source(self, source):
308 self._sources.remove(source)
310 def timerEvent(self, ev):
311 for source in self._sources:
312 trs = source.poll()
313 for tr in trs:
314 self._injector.inject(tr)
316 # following methods needed because mulitple inheritance does not seem
317 # to work anymore with QObject in Python3 or PyQt5
319 def set_fixation_length(self, length):
320 return self._injector.set_fixation_length(length)
322 def set_save_path(
323 self,
324 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
325 '%(tmin)s_%(tmax)s.mseed'):
327 return self._injector.set_save_path(path)
329 def fixate_all(self):
330 return self._injector.fixate_all()
332 def free(self):
333 return self._injector.free()
336class Connection(qc.QObject):
338 received = qc.pyqtSignal(object, object)
339 disconnected = qc.pyqtSignal(object)
341 def __init__(self, parent, sock):
342 qc.QObject.__init__(self, parent)
343 self.socket = sock
344 self.readyRead.connect(
345 self.handle_read)
346 self.disconnected.connect(
347 self.handle_disconnected)
348 self.nwanted = 8
349 self.reading_size = True
350 self.handler = None
351 self.nbytes_received = 0
352 self.nbytes_sent = 0
353 self.compressor = zlib.compressobj()
354 self.decompressor = zlib.decompressobj()
356 def handle_read(self):
357 while True:
358 navail = self.socket.bytesAvailable()
359 if navail < self.nwanted:
360 return
362 data = self.socket.read(self.nwanted)
363 self.nbytes_received += len(data)
364 if self.reading_size:
365 self.nwanted = struct.unpack('>Q', data)[0]
366 self.reading_size = False
367 else:
368 obj = pickle.loads(self.decompressor.decompress(data))
369 if obj is None:
370 self.socket.disconnectFromHost()
371 else:
372 self.handle_received(obj)
373 self.nwanted = 8
374 self.reading_size = True
376 def handle_received(self, obj):
377 self.received.emit(self, obj)
379 def ship(self, obj):
380 data = self.compressor.compress(pickle.dumps(obj))
381 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
382 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
383 self.socket.write(data)
384 self.socket.write(data_end)
385 self.nbytes_sent += len(data)+len(data_end) + 8
387 def handle_disconnected(self):
388 self.disconnected.emit(self)
390 def close(self):
391 self.socket.close()
394class ConnectionHandler(qc.QObject):
395 def __init__(self, parent):
396 qc.QObject.__init__(self, parent)
397 self.queue = []
398 self.connection = None
400 def connected(self):
401 return self.connection is None
403 def set_connection(self, connection):
404 self.connection = connection
405 connection.received.connect(
406 self._handle_received)
408 connection.connect(
409 self.handle_disconnected)
411 for obj in self.queue:
412 self.connection.ship(obj)
414 self.queue = []
416 def _handle_received(self, conn, obj):
417 self.handle_received(obj)
419 def handle_received(self, obj):
420 pass
422 def handle_disconnected(self):
423 self.connection = None
425 def ship(self, obj):
426 if self.connection:
427 self.connection.ship(obj)
428 else:
429 self.queue.append(obj)
432class SimpleConnectionHandler(ConnectionHandler):
433 def __init__(self, parent, **mapping):
434 ConnectionHandler.__init__(self, parent)
435 self.mapping = mapping
437 def handle_received(self, obj):
438 command = obj[0]
439 args = obj[1:]
440 self.mapping[command](*args)
443class MyMainWindow(qw.QMainWindow):
445 def __init__(self, app, *args):
446 qg.QMainWindow.__init__(self, *args)
447 self.app = app
449 def keyPressEvent(self, ev):
450 self.app.pile_viewer.get_view().keyPressEvent(ev)
453class SnufflerTabs(qw.QTabWidget):
454 def __init__(self, parent):
455 qw.QTabWidget.__init__(self, parent)
456 if hasattr(self, 'setTabsClosable'):
457 self.setTabsClosable(True)
459 self.tabCloseRequested.connect(
460 self.removeTab)
462 if hasattr(self, 'setDocumentMode'):
463 self.setDocumentMode(True)
465 def hide_close_button_on_first_tab(self):
466 tbar = self.tabBar()
467 if hasattr(tbar, 'setTabButton'):
468 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
469 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
471 def append_tab(self, widget, name):
472 widget.setParent(self)
473 self.insertTab(self.count(), widget, name)
474 self.setCurrentIndex(self.count()-1)
476 def remove_tab(self, widget):
477 self.removeTab(self.indexOf(widget))
479 def tabInserted(self, index):
480 if index == 0:
481 self.hide_close_button_on_first_tab()
483 self.tabbar_visibility()
484 self.setFocus()
486 def removeTab(self, index):
487 w = self.widget(index)
488 w.close()
489 qw.QTabWidget.removeTab(self, index)
491 def tabRemoved(self, index):
492 self.tabbar_visibility()
494 def tabbar_visibility(self):
495 if self.count() <= 1:
496 self.tabBar().hide()
497 elif self.count() > 1:
498 self.tabBar().show()
500 def keyPressEvent(self, event):
501 if event.text() == 'd':
502 i = self.currentIndex()
503 if i != 0:
504 self.tabCloseRequested.emit(i)
505 else:
506 self.parent().keyPressEvent(event)
509class SnufflerStartWizard(qw.QWizard):
511 def __init__(self, parent):
512 qw.QWizard.__init__(self, parent)
514 self.setOption(self.NoBackButtonOnStartPage)
515 self.setOption(self.NoBackButtonOnLastPage)
516 self.setOption(self.NoCancelButton)
517 self.addPageSurvey()
518 self.addPageHelp()
519 self.setWindowTitle('Welcome to Pyrocko')
521 def getSystemInfo(self):
522 import numpy
523 import scipy
524 import pyrocko
525 import platform
526 import uuid
527 data = {
528 'node-uuid': uuid.getnode(),
529 'platform.architecture': platform.architecture(),
530 'platform.system': platform.system(),
531 'platform.release': platform.release(),
532 'python': platform.python_version(),
533 'pyrocko': pyrocko.__version__,
534 'numpy': numpy.__version__,
535 'scipy': scipy.__version__,
536 'qt': qc.PYQT_VERSION_STR,
537 }
538 return data
540 def addPageSurvey(self):
541 import pprint
542 webtk = 'DSFGK234ADF4ASDF'
543 sys_info = self.getSystemInfo()
545 p = qw.QWizardPage()
546 p.setCommitPage(True)
547 p.setTitle('Thank you for installing Pyrocko!')
549 lyt = qw.QVBoxLayout()
550 lyt.addWidget(qw.QLabel(
551 '<p>Your feedback is important for'
552 ' the development and improvement of Pyrocko.</p>'
553 '<p>Do you want to send this system information anon'
554 'ymously to <a href="https://pyrocko.org">'
555 'https://pyrocko.org</a>?</p>'))
557 text_data = qw.QLabel(
558 '<code style="font-size: small;">%s</code>' %
559 pprint.pformat(
560 sys_info,
561 indent=1).replace('\n', '<br>')
562 )
563 text_data.setStyleSheet('padding: 10px;')
564 lyt.addWidget(text_data)
566 lyt.addWidget(qw.QLabel(
567 'This message won\'t be shown again.\n\n'
568 'We appreciate your contribution!\n- The Pyrocko Developers'
569 ))
571 p.setLayout(lyt)
572 p.setButtonText(self.CommitButton, 'No')
574 yes_btn = qw.QPushButton(p)
575 yes_btn.setText('Yes')
577 @qc.pyqtSlot()
578 def send_data():
579 import requests
580 import json
581 try:
582 requests.post('https://pyrocko.org/%s' % webtk,
583 data=json.dumps(sys_info))
584 except Exception as e:
585 print(e)
586 self.button(self.NextButton).clicked.emit(True)
588 self.customButtonClicked.connect(send_data)
590 self.setButton(self.CustomButton1, yes_btn)
591 self.setOption(self.HaveCustomButton1, True)
593 self.addPage(p)
594 return p
596 def addPageHelp(self):
597 p = qw.QWizardPage()
598 p.setTitle('Welcome to Snuffler!')
600 text = qw.QLabel('''<html>
601<h3>- <i>The Seismogram browser and workbench.</i></h3>
602<p>Looks like you are starting the Snuffler for the first time.<br>
603It allows you to browse and process large archives of waveform data.</p>
604<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
605<ul>
606 <li><b>Download seismograms</b> from Geofon, IRIS and others</li>
607 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
608 <li><b>Cake</b>, Calculate synthetic arrival times</li>
609 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
610 <li>
611 <b>Map</b>, swiftly inspect stations and events on interactive maps
612 </li>
613</ul>
614<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
615<p><b>NOTE:</b><br>If you installed snufflings from the
616<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
617snufflings repository</a><br>you also have to pull an update from there.
618</p>
619<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
620 align="center">
621 <b>You can always press <code>?</code> for help!</b>
622</p>
623</html>''')
625 lyt = qw.QVBoxLayout()
626 lyt.addWidget(text)
628 def remove_custom_button():
629 self.setOption(self.HaveCustomButton1, False)
631 p.initializePage = remove_custom_button
633 p.setLayout(lyt)
634 self.addPage(p)
635 return p
638class SnufflerWindow(qw.QMainWindow):
640 def __init__(
641 self, pile, stations=None, events=None, markers=None, ntracks=12,
642 marker_editor_sortable=True, follow=None, controls=True,
643 opengl=None, instant_close=False):
645 qw.QMainWindow.__init__(self)
647 self.instant_close = instant_close
649 self.dockwidget_to_toggler = {}
650 self.dockwidgets = []
652 self.setWindowTitle("Snuffler")
654 self.pile_viewer = pile_viewer.PileViewer(
655 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
656 marker_editor_sortable=marker_editor_sortable,
657 panel_parent=self)
659 self.marker_editor = self.pile_viewer.marker_editor()
660 self.add_panel(
661 'Markers', self.marker_editor, visible=False,
662 where=qc.Qt.RightDockWidgetArea)
663 if stations:
664 self.get_view().add_stations(stations)
666 if events:
667 self.get_view().add_events(events)
669 if len(events) == 1:
670 self.get_view().set_active_event(events[0])
672 if markers:
673 self.get_view().add_markers(markers)
674 self.get_view().associate_phases_to_events()
676 self.tabs = SnufflerTabs(self)
677 self.setCentralWidget(self.tabs)
678 self.add_tab('Main', self.pile_viewer)
680 self.pile_viewer.setup_snufflings()
681 self.setMenuBar(self.pile_viewer.menu)
683 self.main_controls = self.pile_viewer.controls()
684 self.add_panel('Main Controls', self.main_controls, visible=controls)
685 self.show()
687 self.get_view().setFocus(qc.Qt.OtherFocusReason)
689 sb = self.statusBar()
690 sb.clearMessage()
691 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
693 snuffler_config = self.pile_viewer.viewer.config
695 if snuffler_config.first_start:
696 wizard = SnufflerStartWizard(self)
698 @qc.pyqtSlot()
699 def wizard_finished(result):
700 if result == wizard.Accepted:
701 snuffler_config.first_start = False
702 config.write_config(snuffler_config, 'snuffler')
704 wizard.finished.connect(wizard_finished)
706 wizard.show()
708 if follow:
709 self.get_view().follow(float(follow))
711 self.closing = False
713 def sizeHint(self):
714 return qc.QSize(1024, 768)
715 # return qc.QSize(800, 600) # used for screen shots in tutorial
717 def keyPressEvent(self, ev):
718 self.get_view().keyPressEvent(ev)
720 def get_view(self):
721 return self.pile_viewer.get_view()
723 def get_panel_parent_widget(self):
724 return self
726 def add_tab(self, name, widget):
727 self.tabs.append_tab(widget, name)
729 def remove_tab(self, widget):
730 self.tabs.remove_tab(widget)
732 def add_panel(self, name, panel, visible=False, volatile=False,
733 where=qc.Qt.BottomDockWidgetArea):
735 if not self.dockwidgets:
736 self.dockwidgets = []
738 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
740 dockwidget = qw.QDockWidget(name, self)
741 self.dockwidgets.append(dockwidget)
742 dockwidget.setWidget(panel)
743 panel.setParent(dockwidget)
744 self.addDockWidget(where, dockwidget)
746 if dws:
747 self.tabifyDockWidget(dws[-1], dockwidget)
749 self.toggle_panel(dockwidget, visible)
751 mitem = qw.QAction(name, None)
753 def toggle_panel(checked):
754 self.toggle_panel(dockwidget, True)
756 mitem.triggered.connect(toggle_panel)
758 if volatile:
759 def visibility(visible):
760 if not visible:
761 self.remove_panel(panel)
763 dockwidget.visibilityChanged.connect(
764 visibility)
766 self.get_view().add_panel_toggler(mitem)
767 self.dockwidget_to_toggler[dockwidget] = mitem
769 def toggle_panel(self, dockwidget, visible):
770 if visible is None:
771 visible = not dockwidget.isVisible()
773 dockwidget.setVisible(visible)
774 if visible:
775 w = dockwidget.widget()
776 minsize = w.minimumSize()
777 w.setMinimumHeight(w.sizeHint().height() + 5)
779 def reset_minimum_size():
780 import sip
781 if not sip.isdeleted(w):
782 w.setMinimumSize(minsize)
784 qc.QTimer.singleShot(200, reset_minimum_size)
786 dockwidget.setFocus()
787 dockwidget.raise_()
789 def toggle_marker_editor(self):
790 self.toggle_panel(self.marker_editor.parent(), None)
792 def toggle_main_controls(self):
793 self.toggle_panel(self.main_controls.parent(), None)
795 def remove_panel(self, panel):
796 dockwidget = panel.parent()
797 self.removeDockWidget(dockwidget)
798 dockwidget.setParent(None)
799 mitem = self.dockwidget_to_toggler[dockwidget]
800 self.get_view().remove_panel_toggler(mitem)
802 def return_tag(self):
803 return self.get_view().return_tag
805 def confirm_close(self):
806 ret = qw.QMessageBox.question(
807 self,
808 'Snuffler',
809 'Close Snuffler window?',
810 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
811 qw.QMessageBox.Ok)
813 return ret == qw.QMessageBox.Ok
815 def closeEvent(self, event):
816 if self.instant_close or self.confirm_close():
817 self.closing = True
818 self.pile_viewer.cleanup()
819 event.accept()
820 else:
821 event.ignore()
823 def is_closing(self):
824 return self.closing
827class Snuffler(qw.QApplication):
829 def __init__(self):
830 qw.QApplication.__init__(self, sys.argv)
831 self.lastWindowClosed.connect(self.myQuit)
832 self.server = None
833 self.loader = None
835 def install_sigint_handler(self):
836 self._old_signal_handler = signal.signal(
837 signal.SIGINT,
838 self.myCloseAllWindows)
840 def uninstall_sigint_handler(self):
841 signal.signal(signal.SIGINT, self._old_signal_handler)
843 def start_server(self):
844 self.connections = []
845 s = qn.QTcpServer(self)
846 s.listen(qn.QHostAddress.LocalHost)
847 s.newConnection.connect(
848 self.handle_accept)
849 self.server = s
851 def start_loader(self):
852 self.loader = SimpleConnectionHandler(
853 self,
854 add_files=self.add_files,
855 update_progress=self.update_progress)
856 ticket = os.urandom(32)
857 self.forker.spawn('loader', self.server.serverPort(), ticket)
858 self.connection_handlers[ticket] = self.loader
860 def handle_accept(self):
861 sock = self.server.nextPendingConnection()
862 con = Connection(self, sock)
863 self.connections.append(con)
865 con.disconnected.connect(
866 self.handle_disconnected)
868 con.received.connect(
869 self.handle_received_ticket)
871 def handle_disconnected(self, connection):
872 self.connections.remove(connection)
873 connection.close()
874 del connection
876 def handle_received_ticket(self, connection, object):
877 if not isinstance(object, str):
878 self.handle_disconnected(connection)
880 ticket = object
881 if ticket in self.connection_handlers:
882 h = self.connection_handlers[ticket]
883 connection.received.disconnect(
884 self.handle_received_ticket)
886 h.set_connection(connection)
887 else:
888 self.handle_disconnected(connection)
890 def snuffler_windows(self):
891 return [w for w in self.topLevelWidgets()
892 if isinstance(w, SnufflerWindow) and not w.is_closing()]
894 def event(self, e):
895 if isinstance(e, qg.QFileOpenEvent):
896 paths = [str(e.file())]
897 wins = self.snuffler_windows()
898 if wins:
899 wins[0].get_view().load_soon(paths)
901 return True
902 else:
903 return qw.QApplication.event(self, e)
905 def load(self, pathes, cachedirname, pattern, format):
906 if not self.loader:
907 self.start_loader()
909 self.loader.ship(
910 ('load', pathes, cachedirname, pattern, format))
912 def add_files(self, files):
913 p = self.pile_viewer.get_pile()
914 p.add_files(files)
915 self.pile_viewer.update_contents()
917 def update_progress(self, task, percent):
918 self.pile_viewer.progressbars.set_status(task, percent)
920 def myCloseAllWindows(self, *args):
922 def confirm():
923 try:
924 print('\nQuit Snuffler? [y/n]', file=sys.stderr)
925 confirmed = getch() == 'y'
926 if not confirmed:
927 print('Continuing.', file=sys.stderr)
928 else:
929 print('Quitting Snuffler.', file=sys.stderr)
931 return confirmed
933 except Exception:
934 return False
936 if confirm():
937 for win in self.snuffler_windows():
938 win.instant_close = True
940 self.closeAllWindows()
942 def myQuit(self, *args):
943 self.quit()