1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Effective seismological trace viewer.
8'''
10import sys
11import signal
12import logging
13import time
14import re
15import zlib
16import struct
17import pickle
18try:
19 from urlparse import parse_qsl
20except ImportError:
21 from urllib.parse import parse_qsl
23from pyrocko.streaming import serial_hamster
24from pyrocko.streaming import slink
25from pyrocko.streaming import edl
26from pyrocko.streaming import datacube
28from pyrocko import pile # noqa
29from pyrocko import util # noqa
30from pyrocko import model # noqa
31from pyrocko import config # noqa
32from pyrocko import io # noqa
34from . import pile_viewer # noqa
36from ..qt_compat import qc, qg, qw
38logger = logging.getLogger('pyrocko.gui.snuffler.snuffler_app')
41class _Getch:
42 '''
43 Gets a single character from standard input.
45 Does not echo to the screen.
47 https://stackoverflow.com/questions/510357/how-to-read-a-single-character-from-the-user
48 '''
49 def __init__(self):
50 try:
51 self.impl = _GetchWindows()
52 except ImportError:
53 self.impl = _GetchUnix()
55 def __call__(self): return self.impl()
58class _GetchUnix:
59 def __init__(self):
60 import tty, sys # noqa
62 def __call__(self):
63 import sys
64 import tty
65 import termios
67 fd = sys.stdin.fileno()
68 old_settings = termios.tcgetattr(fd)
69 try:
70 tty.setraw(fd)
71 ch = sys.stdin.read(1)
72 finally:
73 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
75 return ch
78class _GetchWindows:
79 def __init__(self):
80 import msvcrt # noqa
82 def __call__(self):
83 import msvcrt
84 return msvcrt.getch()
87getch = _Getch()
90class AcquisitionThread(qc.QThread):
91 def __init__(self, post_process_sleep=0.0):
92 qc.QThread.__init__(self)
93 self.mutex = qc.QMutex()
94 self.queue = []
95 self.post_process_sleep = post_process_sleep
96 self._sun_is_shining = True
98 def get_wanted_poll_interval(self):
99 return 1000.
101 def run(self):
102 while True:
103 try:
104 self.acquisition_start()
105 while self._sun_is_shining:
106 t0 = time.time()
107 self.process()
108 t1 = time.time()
109 if self.post_process_sleep != 0.0:
110 time.sleep(max(0, self.post_process_sleep-(t1-t0)))
112 self.acquisition_stop()
113 break
115 except (
116 edl.ReadError,
117 serial_hamster.SerialHamsterError,
118 slink.SlowSlinkError) as e:
120 logger.error(str(e))
121 logger.error('Acquistion terminated, restart in 5 s')
122 self.acquisition_stop()
123 time.sleep(5)
124 if not self._sun_is_shining:
125 break
127 def stop(self):
128 self._sun_is_shining = False
130 logger.debug("Waiting for thread to terminate...")
131 self.wait()
132 logger.debug("Thread has terminated.")
134 def got_trace(self, tr):
135 self.mutex.lock()
136 self.queue.append(tr)
137 self.mutex.unlock()
139 def poll(self):
140 self.mutex.lock()
141 items = self.queue[:]
142 self.queue[:] = []
143 self.mutex.unlock()
144 return items
147class SlinkAcquisition(
148 slink.SlowSlink, AcquisitionThread):
150 def __init__(self, *args, **kwargs):
151 slink.SlowSlink.__init__(self, *args, **kwargs)
152 AcquisitionThread.__init__(self)
154 def got_trace(self, tr):
155 AcquisitionThread.got_trace(self, tr)
158class CamAcquisition(
159 serial_hamster.CamSerialHamster, AcquisitionThread):
161 def __init__(self, *args, **kwargs):
162 serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
163 AcquisitionThread.__init__(self, post_process_sleep=0.1)
165 def got_trace(self, tr):
166 AcquisitionThread.got_trace(self, tr)
169class USBHB628Acquisition(
170 serial_hamster.USBHB628Hamster, AcquisitionThread):
172 def __init__(self, deltat=0.02, *args, **kwargs):
173 serial_hamster.USBHB628Hamster.__init__(
174 self, deltat=deltat, *args, **kwargs)
175 AcquisitionThread.__init__(self)
177 def got_trace(self, tr):
178 AcquisitionThread.got_trace(self, tr)
181class SchoolSeismometerAcquisition(
182 serial_hamster.SerialHamster, AcquisitionThread):
184 def __init__(self, *args, **kwargs):
185 serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
186 AcquisitionThread.__init__(self, post_process_sleep=0.0)
188 def got_trace(self, tr):
189 AcquisitionThread.got_trace(self, tr)
191 def get_wanted_poll_interval(self):
192 return 100.
195class EDLAcquisition(
196 edl.EDLHamster, AcquisitionThread):
198 def __init__(self, *args, **kwargs):
199 edl.EDLHamster.__init__(self, *args, **kwargs)
200 AcquisitionThread.__init__(self)
202 def got_trace(self, tr):
203 AcquisitionThread.got_trace(self, tr)
206class CubeAcquisition(
207 datacube.SerialCube, AcquisitionThread):
209 def __init__(self, *args, **kwargs):
210 datacube.SerialCube.__init__(self, *args, **kwargs)
211 AcquisitionThread.__init__(self)
213 def got_trace(self, tr):
214 AcquisitionThread.got_trace(self, tr)
217def setup_acquisition_sources(args):
219 sources = []
220 iarg = 0
221 while iarg < len(args):
222 arg = args[iarg]
224 msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
225 mca = re.match(r'cam://([^:]+)', arg)
226 mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
227 msc = re.match(r'school://([^:?]+)(\?([^?]+))?', arg)
228 med = re.match(r'edl://([^:]+)', arg)
229 mcu = re.match(r'cube://([^:]+)', arg)
231 if msl:
232 host = msl.group(1)
233 port = msl.group(3)
234 if not port:
235 port = '18000'
237 sl = SlinkAcquisition(host=host, port=port)
238 if msl.group(5):
239 stream_patterns = msl.group(5).split(',')
241 if '_' not in msl.group(5):
242 try:
243 streams = sl.query_streams()
244 except slink.SlowSlinkError as e:
245 logger.fatal(str(e))
246 sys.exit(1)
248 streams = list(set(
249 util.match_nslcs(stream_patterns, streams)))
251 for stream in streams:
252 sl.add_stream(*stream)
253 else:
254 for stream in stream_patterns:
255 sl.add_raw_stream_selector(stream)
257 sources.append(sl)
259 elif mca:
260 port = mca.group(1)
261 cam = CamAcquisition(port=port, deltat=0.0314504)
262 sources.append(cam)
264 elif mus:
265 port = mus.group(1)
266 try:
267 d = {}
268 if mus.group(3):
269 d = dict(parse_qsl(mus.group(3)))
271 deltat = 1.0/float(d.get('rate', '50'))
272 channels = [(int(c), c) for c in d.get('channels', '01234567')]
273 hb628 = USBHB628Acquisition(
274 port=port,
275 deltat=deltat,
276 channels=channels,
277 buffersize=16,
278 lookback=50)
280 sources.append(hb628)
281 except Exception:
282 sys.exit('invalid acquisition source: %s' % arg)
284 elif msc:
285 port = msc.group(1)
287 d = {}
288 if msc.group(3):
289 d = dict(parse_qsl(msc.group(3)))
291 d_rate = {
292 '20': ('a', 20.032),
293 '40': ('b', 39.860),
294 '80': ('c', 79.719)}
296 s_rate = d.get('rate', '80')
297 station = d.get('station', 'TEST')
299 if s_rate not in d_rate:
300 raise Exception(
301 'Unsupported rate: %s (expected "20", "40" or "80")'
302 % s_rate)
304 s_gain = d.get('gain', '4')
306 if s_gain not in ('1', '2', '4'):
307 raise Exception(
308 'Unsupported gain: %s (expected "1", "2" or "4")'
309 % s_gain)
311 start_string = s_gain + d_rate[s_rate][0]
312 deltat = 1.0 / d_rate[s_rate][1]
314 logger.info(
315 'School seismometer: trying to use device %s with gain=%s and '
316 'rate=%g.' % (port, s_gain, 1.0/deltat))
318 sco = SchoolSeismometerAcquisition(
319 port=port,
320 deltat=deltat,
321 start_string=start_string,
322 min_detection_size=50,
323 disallow_uneven_sampling_rates=False,
324 station=station)
326 sources.append(sco)
328 elif med:
329 port = med.group(1)
330 edl = EDLAcquisition(port=port)
331 sources.append(edl)
332 elif mcu:
333 device = mcu.group(1)
334 cube = CubeAcquisition(device=device)
335 sources.append(cube)
337 if msl or mca or mus or msc or med or mcu:
338 args.pop(iarg)
339 else:
340 iarg += 1
342 return sources
345class PollInjector(qc.QObject):
347 def __init__(self, *args, **kwargs):
348 interval = kwargs.pop('interval', 1000.)
349 qc.QObject.__init__(self)
350 self._injector = pile.Injector(*args, **kwargs)
351 self._sources = []
352 self.startTimer(int(interval))
354 def add_source(self, source):
355 self._sources.append(source)
357 def remove_source(self, source):
358 self._sources.remove(source)
360 def timerEvent(self, ev):
361 for source in self._sources:
362 trs = source.poll()
363 for tr in trs:
364 self._injector.inject(tr)
366 # following methods needed because mulitple inheritance does not seem
367 # to work anymore with QObject in Python3 or PyQt5
369 def set_fixation_length(self, length):
370 return self._injector.set_fixation_length(length)
372 def set_save_path(
373 self,
374 path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
375 '%(tmin)s_%(tmax)s.mseed'):
377 return self._injector.set_save_path(path)
379 def fixate_all(self):
380 return self._injector.fixate_all()
382 def free(self):
383 return self._injector.free()
386class Connection(qc.QObject):
388 received = qc.pyqtSignal(object, object)
389 disconnected = qc.pyqtSignal(object)
391 def __init__(self, parent, sock):
392 qc.QObject.__init__(self, parent)
393 self.socket = sock
394 self.readyRead.connect(
395 self.handle_read)
396 self.disconnected.connect(
397 self.handle_disconnected)
398 self.nwanted = 8
399 self.reading_size = True
400 self.handler = None
401 self.nbytes_received = 0
402 self.nbytes_sent = 0
403 self.compressor = zlib.compressobj()
404 self.decompressor = zlib.decompressobj()
406 def handle_read(self):
407 while True:
408 navail = self.socket.bytesAvailable()
409 if navail < self.nwanted:
410 return
412 data = self.socket.read(self.nwanted)
413 self.nbytes_received += len(data)
414 if self.reading_size:
415 self.nwanted = struct.unpack('>Q', data)[0]
416 self.reading_size = False
417 else:
418 obj = pickle.loads(self.decompressor.decompress(data))
419 if obj is None:
420 self.socket.disconnectFromHost()
421 else:
422 self.handle_received(obj)
423 self.nwanted = 8
424 self.reading_size = True
426 def handle_received(self, obj):
427 self.received.emit(self, obj)
429 def ship(self, obj):
430 data = self.compressor.compress(pickle.dumps(obj))
431 data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
432 self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
433 self.socket.write(data)
434 self.socket.write(data_end)
435 self.nbytes_sent += len(data)+len(data_end) + 8
437 def handle_disconnected(self):
438 self.disconnected.emit(self)
440 def close(self):
441 self.socket.close()
444class ConnectionHandler(qc.QObject):
445 def __init__(self, parent):
446 qc.QObject.__init__(self, parent)
447 self.queue = []
448 self.connection = None
450 def connected(self):
451 return self.connection is None
453 def set_connection(self, connection):
454 self.connection = connection
455 connection.received.connect(
456 self._handle_received)
458 connection.connect(
459 self.handle_disconnected)
461 for obj in self.queue:
462 self.connection.ship(obj)
464 self.queue = []
466 def _handle_received(self, conn, obj):
467 self.handle_received(obj)
469 def handle_received(self, obj):
470 pass
472 def handle_disconnected(self):
473 self.connection = None
475 def ship(self, obj):
476 if self.connection:
477 self.connection.ship(obj)
478 else:
479 self.queue.append(obj)
482class SimpleConnectionHandler(ConnectionHandler):
483 def __init__(self, parent, **mapping):
484 ConnectionHandler.__init__(self, parent)
485 self.mapping = mapping
487 def handle_received(self, obj):
488 command = obj[0]
489 args = obj[1:]
490 self.mapping[command](*args)
493class MyMainWindow(qw.QMainWindow):
495 def __init__(self, app, *args):
496 qg.QMainWindow.__init__(self, *args)
497 self.app = app
499 def keyPressEvent(self, ev):
500 self.app.pile_viewer.get_view().keyPressEvent(ev)
503class SnufflerTabs(qw.QTabWidget):
504 def __init__(self, parent):
505 qw.QTabWidget.__init__(self, parent)
506 if hasattr(self, 'setTabsClosable'):
507 self.setTabsClosable(True)
509 self.tabCloseRequested.connect(
510 self.removeTab)
512 if hasattr(self, 'setDocumentMode'):
513 self.setDocumentMode(True)
515 def hide_close_button_on_first_tab(self):
516 tbar = self.tabBar()
517 if hasattr(tbar, 'setTabButton'):
518 tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
519 tbar.setTabButton(0, qw.QTabBar.RightSide, None)
521 def append_tab(self, widget, name):
522 widget.setParent(self)
523 self.insertTab(self.count(), widget, name)
524 self.setCurrentIndex(self.count()-1)
526 def remove_tab(self, widget):
527 self.removeTab(self.indexOf(widget))
529 def tabInserted(self, index):
530 if index == 0:
531 self.hide_close_button_on_first_tab()
533 self.tabbar_visibility()
534 self.setFocus()
536 def removeTab(self, index):
537 w = self.widget(index)
538 w.close()
539 qw.QTabWidget.removeTab(self, index)
541 def tabRemoved(self, index):
542 self.tabbar_visibility()
544 def tabbar_visibility(self):
545 if self.count() <= 1:
546 self.tabBar().hide()
547 elif self.count() > 1:
548 self.tabBar().show()
550 def keyPressEvent(self, event):
551 if event.text() == 'd':
552 i = self.currentIndex()
553 if i != 0:
554 self.tabCloseRequested.emit(i)
555 else:
556 self.parent().keyPressEvent(event)
559class SnufflerStartWizard(qw.QWizard):
561 def __init__(self, parent):
562 qw.QWizard.__init__(self, parent)
564 self.setOption(self.NoBackButtonOnStartPage)
565 self.setOption(self.NoBackButtonOnLastPage)
566 self.setOption(self.NoCancelButton)
567 self.addPageSurvey()
568 self.addPageHelp()
569 self.setWindowTitle('Welcome to Pyrocko')
571 def getSystemInfo(self):
572 import numpy
573 import scipy
574 import pyrocko
575 import platform
576 import uuid
577 data = {
578 'node-uuid': uuid.getnode(),
579 'platform.architecture': platform.architecture(),
580 'platform.system': platform.system(),
581 'platform.release': platform.release(),
582 'python': platform.python_version(),
583 'pyrocko': pyrocko.__version__,
584 'numpy': numpy.__version__,
585 'scipy': scipy.__version__,
586 'qt': qc.PYQT_VERSION_STR,
587 }
588 return data
590 def addPageSurvey(self):
591 import pprint
592 webtk = 'DSFGK234ADF4ASDF'
593 sys_info = self.getSystemInfo()
595 p = qw.QWizardPage()
596 p.setCommitPage(True)
597 p.setTitle('Thank you for installing Pyrocko!')
599 lyt = qw.QVBoxLayout()
600 lyt.addWidget(qw.QLabel(
601 '<p>Your feedback is important for'
602 ' the development and improvement of Pyrocko.</p>'
603 '<p>Do you want to send this system information anon'
604 'ymously to <a href="https://pyrocko.org">'
605 'https://pyrocko.org</a>?</p>'))
607 text_data = qw.QLabel(
608 '<code style="font-size: small;">%s</code>' %
609 pprint.pformat(
610 sys_info,
611 indent=1).replace('\n', '<br>')
612 )
613 text_data.setStyleSheet('padding: 10px;')
614 lyt.addWidget(text_data)
616 lyt.addWidget(qw.QLabel(
617 'This message won\'t be shown again.\n\n'
618 'We appreciate your contribution!\n- The Pyrocko Developers'
619 ))
621 p.setLayout(lyt)
622 p.setButtonText(self.CommitButton, 'No')
624 yes_btn = qw.QPushButton(p)
625 yes_btn.setText('Yes')
627 @qc.pyqtSlot()
628 def send_data():
629 import requests
630 import json
631 try:
632 requests.post('https://pyrocko.org/%s' % webtk,
633 data=json.dumps(sys_info))
634 except Exception as e:
635 print(e)
636 self.button(self.NextButton).clicked.emit(True)
638 self.customButtonClicked.connect(send_data)
640 self.setButton(self.CustomButton1, yes_btn)
641 self.setOption(self.HaveCustomButton1, True)
643 self.addPage(p)
644 return p
646 def addPageHelp(self):
647 p = qw.QWizardPage()
648 p.setTitle('Welcome to Snuffler!')
650 text = qw.QLabel('''<html>
651<h3>- <i>The Seismogram browser and workbench.</i></h3>
652<p>Looks like you are starting the Snuffler for the first time.<br>
653It allows you to browse and process large archives of waveform data.</p>
654<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
655<ul>
656 <li><b>Download seismograms</b> from Geofon, IRIS and others</li>
657 <li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
658 <li><b>Cake</b>, Calculate synthetic arrival times</li>
659 <li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
660 <li>
661 <b>Map</b>, swiftly inspect stations and events on interactive maps
662 </li>
663</ul>
664<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
665<p><b>NOTE:</b><br>If you installed snufflings from the
666<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
667snufflings repository</a><br>you also have to pull an update from there.
668</p>
669<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
670 align="center">
671 <b>You can always press <code>?</code> for help!</b>
672</p>
673</html>''')
675 lyt = qw.QVBoxLayout()
676 lyt.addWidget(text)
678 def remove_custom_button():
679 self.setOption(self.HaveCustomButton1, False)
681 p.initializePage = remove_custom_button
683 p.setLayout(lyt)
684 self.addPage(p)
685 return p
688class SnufflerWindow(qw.QMainWindow):
690 def __init__(
691 self, pile, stations=None, events=None, markers=None, ntracks=12,
692 marker_editor_sortable=True, follow=None, controls=True,
693 opengl=None, instant_close=False):
695 qw.QMainWindow.__init__(self)
697 self.instant_close = instant_close
699 self.dockwidget_to_toggler = {}
700 self.dockwidgets = []
702 self.setWindowTitle("Snuffler")
704 self.pile_viewer = pile_viewer.PileViewer(
705 pile, ntracks_shown_max=ntracks, use_opengl=opengl,
706 marker_editor_sortable=marker_editor_sortable,
707 panel_parent=self)
709 self.marker_editor = self.pile_viewer.marker_editor()
710 self.add_panel(
711 'Markers', self.marker_editor, visible=False,
712 where=qc.Qt.RightDockWidgetArea)
713 if stations:
714 self.get_view().add_stations(stations)
716 if events:
717 self.get_view().add_events(events)
719 if len(events) == 1:
720 self.get_view().set_active_event(events[0])
722 if markers:
723 self.get_view().add_markers(markers)
724 self.get_view().associate_phases_to_events()
726 self.tabs = SnufflerTabs(self)
727 self.setCentralWidget(self.tabs)
728 self.add_tab('Main', self.pile_viewer)
730 self.pile_viewer.setup_snufflings()
731 self.setMenuBar(self.pile_viewer.menu)
733 self.main_controls = self.pile_viewer.controls()
734 self.add_panel('Main Controls', self.main_controls, visible=controls)
735 self.show()
737 self.get_view().setFocus(qc.Qt.OtherFocusReason)
739 sb = self.statusBar()
740 sb.clearMessage()
741 sb.showMessage('Welcome to Snuffler! Press <?> for help.')
743 snuffler_config = self.pile_viewer.viewer.config
745 if snuffler_config.first_start:
746 wizard = SnufflerStartWizard(self)
748 @qc.pyqtSlot()
749 def wizard_finished(result):
750 if result == wizard.Accepted:
751 snuffler_config.first_start = False
752 config.write_config(snuffler_config, 'snuffler')
754 wizard.finished.connect(wizard_finished)
756 wizard.show()
758 if follow:
759 self.get_view().follow(float(follow))
761 self.closing = False
763 def sizeHint(self):
764 return qc.QSize(1024, 768)
765 # return qc.QSize(800, 600) # used for screen shots in tutorial
767 def keyPressEvent(self, ev):
768 self.get_view().keyPressEvent(ev)
770 def get_view(self):
771 return self.pile_viewer.get_view()
773 def get_panel_parent_widget(self):
774 return self
776 def add_tab(self, name, widget):
777 self.tabs.append_tab(widget, name)
779 def remove_tab(self, widget):
780 self.tabs.remove_tab(widget)
782 def add_panel(self, name, panel, visible=False, volatile=False,
783 where=qc.Qt.BottomDockWidgetArea):
785 if not self.dockwidgets:
786 self.dockwidgets = []
788 dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
790 dockwidget = qw.QDockWidget(name, self)
791 self.dockwidgets.append(dockwidget)
792 dockwidget.setWidget(panel)
793 panel.setParent(dockwidget)
794 self.addDockWidget(where, dockwidget)
796 if dws:
797 self.tabifyDockWidget(dws[-1], dockwidget)
799 self.toggle_panel(dockwidget, visible)
801 mitem = qw.QAction(name, None)
803 def toggle_panel(checked):
804 self.toggle_panel(dockwidget, True)
806 mitem.triggered.connect(toggle_panel)
808 if volatile:
809 def visibility(visible):
810 if not visible:
811 self.remove_panel(panel)
813 dockwidget.visibilityChanged.connect(
814 visibility)
816 self.get_view().add_panel_toggler(mitem)
817 self.dockwidget_to_toggler[dockwidget] = mitem
819 if pile_viewer.is_macos:
820 tabbars = self.findChildren(qw.QTabBar)
821 for tabbar in tabbars:
822 tabbar.setShape(qw.QTabBar.TriangularNorth)
823 tabbar.setDocumentMode(True)
825 def toggle_panel(self, dockwidget, visible):
826 if visible is None:
827 visible = not dockwidget.isVisible()
829 dockwidget.setVisible(visible)
830 if visible:
831 w = dockwidget.widget()
832 minsize = w.minimumSize()
833 w.setMinimumHeight(w.sizeHint().height() + 5)
835 def reset_minimum_size():
836 import sip
837 if not sip.isdeleted(w):
838 w.setMinimumSize(minsize)
840 qc.QTimer.singleShot(200, reset_minimum_size)
842 dockwidget.setFocus()
843 dockwidget.raise_()
845 def toggle_marker_editor(self):
846 self.toggle_panel(self.marker_editor.parent(), None)
848 def toggle_main_controls(self):
849 self.toggle_panel(self.main_controls.parent(), None)
851 def remove_panel(self, panel):
852 dockwidget = panel.parent()
853 self.removeDockWidget(dockwidget)
854 dockwidget.setParent(None)
855 mitem = self.dockwidget_to_toggler[dockwidget]
856 self.get_view().remove_panel_toggler(mitem)
858 def return_tag(self):
859 return self.get_view().return_tag
861 def confirm_close(self):
862 ret = qw.QMessageBox.question(
863 self,
864 'Snuffler',
865 'Close Snuffler window?',
866 qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
867 qw.QMessageBox.Ok)
869 return ret == qw.QMessageBox.Ok
871 def closeEvent(self, event):
872 if self.instant_close or self.confirm_close():
873 self.closing = True
874 self.pile_viewer.cleanup()
875 event.accept()
876 else:
877 event.ignore()
879 def is_closing(self):
880 return self.closing
883class Snuffler(qw.QApplication):
885 def __init__(self):
886 qw.QApplication.__init__(self, [])
887 self.setApplicationName('Snuffler')
888 self.setApplicationDisplayName('Snuffler')
889 self.lastWindowClosed.connect(self.myQuit)
890 self.server = None
891 self.loader = None
893 def install_sigint_handler(self):
894 self._old_signal_handler = signal.signal(
895 signal.SIGINT,
896 self.myCloseAllWindows)
898 def uninstall_sigint_handler(self):
899 signal.signal(signal.SIGINT, self._old_signal_handler)
901 def snuffler_windows(self):
902 return [w for w in self.topLevelWidgets()
903 if isinstance(w, SnufflerWindow) and not w.is_closing()]
905 def event(self, e):
906 if isinstance(e, qg.QFileOpenEvent):
907 path = str(e.file())
908 if path != sys.argv[0]:
909 wins = self.snuffler_windows()
910 if wins:
911 wins[0].get_view().load_soon([path])
913 return True
914 else:
915 return qw.QApplication.event(self, e)
917 def load(self, pathes, cachedirname, pattern, format):
918 if not self.loader:
919 self.start_loader()
921 self.loader.ship(
922 ('load', pathes, cachedirname, pattern, format))
924 def update_progress(self, task, percent):
925 self.pile_viewer.progressbars.set_status(task, percent)
927 def myCloseAllWindows(self, *args):
929 def confirm():
930 try:
931 print('\nQuit Snuffler? [y/n]', file=sys.stderr)
932 confirmed = getch() == 'y'
933 if not confirmed:
934 print('Continuing.', file=sys.stderr)
935 else:
936 print('Quitting Snuffler.', file=sys.stderr)
938 return confirmed
940 except Exception:
941 return False
943 if confirm():
944 for win in self.snuffler_windows():
945 win.instant_close = True
947 self.closeAllWindows()
949 def myQuit(self, *args):
950 self.quit()