forked from pyrocko/pyrocko
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
857 lines
25 KiB
Python
857 lines
25 KiB
Python
# http://pyrocko.org - GPLv3
|
|
#
|
|
# The Pyrocko Developers, 21st Century
|
|
# ---|P------/S----------~Lg----------
|
|
'''
|
|
Effective seismological trace viewer.
|
|
'''
|
|
from __future__ import absolute_import
|
|
|
|
import os
|
|
import sys
|
|
import signal
|
|
import logging
|
|
import time
|
|
import re
|
|
import zlib
|
|
import struct
|
|
import pickle
|
|
|
|
|
|
from pyrocko.streaming import serial_hamster
|
|
from pyrocko.streaming import slink
|
|
from pyrocko.streaming import edl
|
|
|
|
from pyrocko import pile # noqa
|
|
from pyrocko import util # noqa
|
|
from pyrocko import model # noqa
|
|
from pyrocko import config # noqa
|
|
from pyrocko import io # noqa
|
|
|
|
from . import pile_viewer # noqa
|
|
|
|
from .qt_compat import qc, qg, qw, qn
|
|
|
|
logger = logging.getLogger('pyrocko.gui.snuffler_app')
|
|
|
|
|
|
class AcquisitionThread(qc.QThread):
|
|
def __init__(self, post_process_sleep=0.0):
|
|
qc.QThread.__init__(self)
|
|
self.mutex = qc.QMutex()
|
|
self.queue = []
|
|
self.post_process_sleep = post_process_sleep
|
|
self._sun_is_shining = True
|
|
|
|
def run(self):
|
|
while True:
|
|
try:
|
|
self.acquisition_start()
|
|
while self._sun_is_shining:
|
|
t0 = time.time()
|
|
self.process()
|
|
t1 = time.time()
|
|
if self.post_process_sleep != 0.0:
|
|
time.sleep(max(0, self.post_process_sleep-(t1-t0)))
|
|
|
|
self.acquisition_stop()
|
|
break
|
|
|
|
except (
|
|
edl.ReadError,
|
|
serial_hamster.SerialHamsterError,
|
|
slink.SlowSlinkError) as e:
|
|
|
|
logger.error(str(e))
|
|
logger.error('Acquistion terminated, restart in 5 s')
|
|
self.acquisition_stop()
|
|
time.sleep(5)
|
|
if not self._sun_is_shining:
|
|
break
|
|
|
|
def stop(self):
|
|
self._sun_is_shining = False
|
|
|
|
logger.debug("Waiting for thread to terminate...")
|
|
self.wait()
|
|
logger.debug("Thread has terminated.")
|
|
|
|
def got_trace(self, tr):
|
|
self.mutex.lock()
|
|
self.queue.append(tr)
|
|
self.mutex.unlock()
|
|
|
|
def poll(self):
|
|
self.mutex.lock()
|
|
items = self.queue[:]
|
|
self.queue[:] = []
|
|
self.mutex.unlock()
|
|
return items
|
|
|
|
|
|
class SlinkAcquisition(
|
|
slink.SlowSlink, AcquisitionThread):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
slink.SlowSlink.__init__(self, *args, **kwargs)
|
|
AcquisitionThread.__init__(self)
|
|
|
|
def got_trace(self, tr):
|
|
AcquisitionThread.got_trace(self, tr)
|
|
|
|
|
|
class CamAcquisition(
|
|
serial_hamster.CamSerialHamster, AcquisitionThread):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
serial_hamster.CamSerialHamster.__init__(self, *args, **kwargs)
|
|
AcquisitionThread.__init__(self, post_process_sleep=0.1)
|
|
|
|
def got_trace(self, tr):
|
|
AcquisitionThread.got_trace(self, tr)
|
|
|
|
|
|
class USBHB628Acquisition(
|
|
serial_hamster.USBHB628Hamster, AcquisitionThread):
|
|
|
|
def __init__(self, deltat=0.02, *args, **kwargs):
|
|
serial_hamster.USBHB628Hamster.__init__(
|
|
self, deltat=deltat, *args, **kwargs)
|
|
AcquisitionThread.__init__(self)
|
|
|
|
def got_trace(self, tr):
|
|
AcquisitionThread.got_trace(self, tr)
|
|
|
|
|
|
class SchoolSeismometerAcquisition(
|
|
serial_hamster.SerialHamster, AcquisitionThread):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
serial_hamster.SerialHamster.__init__(self, *args, **kwargs)
|
|
AcquisitionThread.__init__(self, post_process_sleep=0.01)
|
|
|
|
def got_trace(self, tr):
|
|
AcquisitionThread.got_trace(self, tr)
|
|
|
|
|
|
class EDLAcquisition(
|
|
edl.EDLHamster, AcquisitionThread):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
edl.EDLHamster.__init__(self, *args, **kwargs)
|
|
AcquisitionThread.__init__(self)
|
|
|
|
def got_trace(self, tr):
|
|
AcquisitionThread.got_trace(self, tr)
|
|
|
|
|
|
def setup_acquisition_sources(args):
|
|
|
|
sources = []
|
|
iarg = 0
|
|
while iarg < len(args):
|
|
arg = args[iarg]
|
|
|
|
msl = re.match(r'seedlink://([a-zA-Z0-9.-]+)(:(\d+))?(/(.*))?', arg)
|
|
mca = re.match(r'cam://([^:]+)', arg)
|
|
mus = re.match(r'hb628://([^:?]+)(\?([^?]+))?', arg)
|
|
msc = re.match(r'school://([^:]+)', arg)
|
|
med = re.match(r'edl://([^:]+)', arg)
|
|
if msl:
|
|
host = msl.group(1)
|
|
port = msl.group(3)
|
|
if not port:
|
|
port = '18000'
|
|
|
|
sl = SlinkAcquisition(host=host, port=port)
|
|
if msl.group(5):
|
|
stream_patterns = msl.group(5).split(',')
|
|
|
|
if '_' not in msl.group(5):
|
|
try:
|
|
streams = sl.query_streams()
|
|
except slink.SlowSlinkError as e:
|
|
logger.fatal(str(e))
|
|
sys.exit(1)
|
|
|
|
streams = list(set(
|
|
util.match_nslcs(stream_patterns, streams)))
|
|
|
|
for stream in streams:
|
|
sl.add_stream(*stream)
|
|
else:
|
|
for stream in stream_patterns:
|
|
sl.add_raw_stream_selector(stream)
|
|
|
|
sources.append(sl)
|
|
elif mca:
|
|
port = mca.group(1)
|
|
cam = CamAcquisition(port=port, deltat=0.0314504)
|
|
sources.append(cam)
|
|
elif mus:
|
|
port = mus.group(1)
|
|
try:
|
|
d = {}
|
|
if mus.group(3):
|
|
d = dict(urlparse.parse_qsl(mus.group(3))) # noqa
|
|
|
|
deltat = 1.0/float(d.get('rate', '50'))
|
|
channels = [(int(c), c) for c in d.get('channels', '01234567')]
|
|
hb628 = USBHB628Acquisition(
|
|
port=port,
|
|
deltat=deltat,
|
|
channels=channels,
|
|
buffersize=16,
|
|
lookback=50)
|
|
|
|
sources.append(hb628)
|
|
except Exception:
|
|
raise
|
|
sys.exit('invalid acquisition source: %s' % arg)
|
|
|
|
elif msc:
|
|
port = msc.group(1)
|
|
sco = SchoolSeismometerAcquisition(port=port)
|
|
sources.append(sco)
|
|
elif med:
|
|
port = med.group(1)
|
|
edl = EDLAcquisition(port=port)
|
|
sources.append(edl)
|
|
|
|
if msl or mca or mus or msc or med:
|
|
args.pop(iarg)
|
|
else:
|
|
iarg += 1
|
|
|
|
return sources
|
|
|
|
|
|
class PollInjector(qc.QObject):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
qc.QObject.__init__(self)
|
|
self._injector = pile.Injector(*args, **kwargs)
|
|
self._sources = []
|
|
self.startTimer(1000.)
|
|
|
|
def add_source(self, source):
|
|
self._sources.append(source)
|
|
|
|
def remove_source(self, source):
|
|
self._sources.remove(source)
|
|
|
|
def timerEvent(self, ev):
|
|
for source in self._sources:
|
|
trs = source.poll()
|
|
for tr in trs:
|
|
self._injector.inject(tr)
|
|
|
|
# following methods needed because mulitple inheritance does not seem
|
|
# to work anymore with QObject in Python3 or PyQt5
|
|
|
|
def set_fixation_length(self, length):
|
|
return self._injector.set_fixation_length(length)
|
|
|
|
def set_save_path(
|
|
self,
|
|
path='dump_%(network)s.%(station)s.%(location)s.%(channel)s_'
|
|
'%(tmin)s_%(tmax)s.mseed'):
|
|
|
|
return self._injector.set_save_path(path)
|
|
|
|
def fixate_all(self):
|
|
return self._injector.fixate_all()
|
|
|
|
def free(self):
|
|
return self._injector.free()
|
|
|
|
|
|
class Connection(qc.QObject):
|
|
|
|
received = qc.pyqtSignal(object, object)
|
|
disconnected = qc.pyqtSignal(object)
|
|
|
|
def __init__(self, parent, sock):
|
|
qc.QObject.__init__(self, parent)
|
|
self.socket = sock
|
|
self.readyRead.connect(
|
|
self.handle_read)
|
|
self.disconnected.connect(
|
|
self.handle_disconnected)
|
|
self.nwanted = 8
|
|
self.reading_size = True
|
|
self.handler = None
|
|
self.nbytes_received = 0
|
|
self.nbytes_sent = 0
|
|
self.compressor = zlib.compressobj()
|
|
self.decompressor = zlib.decompressobj()
|
|
|
|
def handle_read(self):
|
|
while True:
|
|
navail = self.socket.bytesAvailable()
|
|
if navail < self.nwanted:
|
|
return
|
|
|
|
data = self.socket.read(self.nwanted)
|
|
self.nbytes_received += len(data)
|
|
if self.reading_size:
|
|
self.nwanted = struct.unpack('>Q', data)[0]
|
|
self.reading_size = False
|
|
else:
|
|
obj = pickle.loads(self.decompressor.decompress(data))
|
|
if obj is None:
|
|
self.socket.disconnectFromHost()
|
|
else:
|
|
self.handle_received(obj)
|
|
self.nwanted = 8
|
|
self.reading_size = True
|
|
|
|
def handle_received(self, obj):
|
|
self.received.emit(self, obj)
|
|
|
|
def ship(self, obj):
|
|
data = self.compressor.compress(pickle.dumps(obj))
|
|
data_end = self.compressor.flush(zlib.Z_FULL_FLUSH)
|
|
self.socket.write(struct.pack('>Q', len(data)+len(data_end)))
|
|
self.socket.write(data)
|
|
self.socket.write(data_end)
|
|
self.nbytes_sent += len(data)+len(data_end) + 8
|
|
|
|
def handle_disconnected(self):
|
|
self.disconnected.emit(self)
|
|
|
|
def close(self):
|
|
self.socket.close()
|
|
|
|
|
|
class ConnectionHandler(qc.QObject):
|
|
def __init__(self, parent):
|
|
qc.QObject.__init__(self, parent)
|
|
self.queue = []
|
|
self.connection = None
|
|
|
|
def connected(self):
|
|
return self.connection is None
|
|
|
|
def set_connection(self, connection):
|
|
self.connection = connection
|
|
connection.received.connect(
|
|
self._handle_received)
|
|
|
|
connection.connect(
|
|
self.handle_disconnected)
|
|
|
|
for obj in self.queue:
|
|
self.connection.ship(obj)
|
|
|
|
self.queue = []
|
|
|
|
def _handle_received(self, conn, obj):
|
|
self.handle_received(obj)
|
|
|
|
def handle_received(self, obj):
|
|
pass
|
|
|
|
def handle_disconnected(self):
|
|
self.connection = None
|
|
|
|
def ship(self, obj):
|
|
if self.connection:
|
|
self.connection.ship(obj)
|
|
else:
|
|
self.queue.append(obj)
|
|
|
|
|
|
class SimpleConnectionHandler(ConnectionHandler):
|
|
def __init__(self, parent, **mapping):
|
|
ConnectionHandler.__init__(self, parent)
|
|
self.mapping = mapping
|
|
|
|
def handle_received(self, obj):
|
|
command = obj[0]
|
|
args = obj[1:]
|
|
self.mapping[command](*args)
|
|
|
|
|
|
class MyMainWindow(qw.QMainWindow):
|
|
|
|
def __init__(self, app, *args):
|
|
qg.QMainWindow.__init__(self, *args)
|
|
self.app = app
|
|
|
|
def keyPressEvent(self, ev):
|
|
self.app.pile_viewer.get_view().keyPressEvent(ev)
|
|
|
|
|
|
class SnufflerTabs(qw.QTabWidget):
|
|
def __init__(self, parent):
|
|
qw.QTabWidget.__init__(self, parent)
|
|
if hasattr(self, 'setTabsClosable'):
|
|
self.setTabsClosable(True)
|
|
|
|
self.tabCloseRequested.connect(
|
|
self.removeTab)
|
|
|
|
if hasattr(self, 'setDocumentMode'):
|
|
self.setDocumentMode(True)
|
|
|
|
def hide_close_button_on_first_tab(self):
|
|
tbar = self.tabBar()
|
|
if hasattr(tbar, 'setTabButton'):
|
|
tbar.setTabButton(0, qw.QTabBar.LeftSide, None)
|
|
tbar.setTabButton(0, qw.QTabBar.RightSide, None)
|
|
|
|
def append_tab(self, widget, name):
|
|
widget.setParent(self)
|
|
self.insertTab(self.count(), widget, name)
|
|
self.setCurrentIndex(self.count()-1)
|
|
|
|
def remove_tab(self, widget):
|
|
self.removeTab(self.indexOf(widget))
|
|
|
|
def tabInserted(self, index):
|
|
if index == 0:
|
|
self.hide_close_button_on_first_tab()
|
|
|
|
self.tabbar_visibility()
|
|
self.setFocus()
|
|
|
|
def removeTab(self, index):
|
|
w = self.widget(index)
|
|
w.close()
|
|
qw.QTabWidget.removeTab(self, index)
|
|
|
|
def tabRemoved(self, index):
|
|
self.tabbar_visibility()
|
|
|
|
def tabbar_visibility(self):
|
|
if self.count() <= 1:
|
|
self.tabBar().hide()
|
|
elif self.count() > 1:
|
|
self.tabBar().show()
|
|
|
|
def keyPressEvent(self, event):
|
|
if event.text() == 'd':
|
|
i = self.currentIndex()
|
|
if i != 0:
|
|
self.tabCloseRequested.emit(i)
|
|
else:
|
|
self.parent().keyPressEvent(event)
|
|
|
|
|
|
class SnufflerStartWizard(qw.QWizard):
|
|
|
|
def __init__(self, parent):
|
|
qw.QWizard.__init__(self, parent)
|
|
|
|
self.setOption(self.NoBackButtonOnStartPage)
|
|
self.setOption(self.NoBackButtonOnLastPage)
|
|
self.setOption(self.NoCancelButton)
|
|
self.addPageSurvey()
|
|
self.addPageHelp()
|
|
self.setWindowTitle('Welcome to Pyrocko')
|
|
|
|
def getSystemInfo(self):
|
|
import numpy
|
|
import scipy
|
|
import pyrocko
|
|
import platform
|
|
import uuid
|
|
data = {
|
|
'node-uuid': uuid.getnode(),
|
|
'platform.architecture': platform.architecture(),
|
|
'platform.system': platform.system(),
|
|
'platform.release': platform.release(),
|
|
'python': platform.python_version(),
|
|
'pyrocko': pyrocko.__version__,
|
|
'numpy': numpy.__version__,
|
|
'scipy': scipy.__version__,
|
|
'qt': qc.PYQT_VERSION_STR,
|
|
}
|
|
return data
|
|
|
|
def addPageSurvey(self):
|
|
import pprint
|
|
webtk = 'DSFGK234ADF4ASDF'
|
|
sys_info = self.getSystemInfo()
|
|
|
|
p = qw.QWizardPage()
|
|
p.setCommitPage(True)
|
|
p.setTitle('Thank you for installing Pyrocko!')
|
|
|
|
lyt = qw.QVBoxLayout()
|
|
lyt.addWidget(qw.QLabel(
|
|
'<p>Your feedback is important for'
|
|
' the development and improvement of Pyrocko.</p>'
|
|
'<p>Do you want to send this system information anon'
|
|
'ymously to <a href="https://pyrocko.org">'
|
|
'https://pyrocko.org</a>?</p>'))
|
|
|
|
text_data = qw.QLabel(
|
|
'<code style="font-size: small;">%s</code>' %
|
|
pprint.pformat(
|
|
sys_info,
|
|
indent=1).replace('\n', '<br>')
|
|
)
|
|
text_data.setStyleSheet('padding: 10px;')
|
|
lyt.addWidget(text_data)
|
|
|
|
lyt.addWidget(qw.QLabel(
|
|
'This message won\'t be shown again.\n\n'
|
|
'We appreciate your contribution!\n- The Pyrocko Developers'
|
|
))
|
|
|
|
p.setLayout(lyt)
|
|
p.setButtonText(self.CommitButton, 'No')
|
|
|
|
yes_btn = qw.QPushButton(p)
|
|
yes_btn.setText('Yes')
|
|
|
|
@qc.pyqtSlot()
|
|
def send_data():
|
|
import requests
|
|
import json
|
|
try:
|
|
requests.post('https://pyrocko.org/%s' % webtk,
|
|
data=json.dumps(sys_info))
|
|
except Exception as e:
|
|
print(e)
|
|
self.button(self.NextButton).clicked.emit(True)
|
|
|
|
self.customButtonClicked.connect(send_data)
|
|
|
|
self.setButton(self.CustomButton1, yes_btn)
|
|
self.setOption(self.HaveCustomButton1, True)
|
|
|
|
self.addPage(p)
|
|
return p
|
|
|
|
def addPageHelp(self):
|
|
p = qw.QWizardPage()
|
|
p.setTitle('Welcome to Snuffler!')
|
|
|
|
text = qw.QLabel('''<html>
|
|
<h3>- <i>The Seismogram browser and workbench.</i></h3>
|
|
<p>Looks like you are starting the Snuffler for the first time.<br>
|
|
It allows you to browse and process large archives of waveform data.</p>
|
|
<p>Basic processing is complemented by Snufflings (<i>Plugins</i>):</p>
|
|
<ul>
|
|
<li><b>Download seismograms</b> from Geofon, IRIS and others</li>
|
|
<li><b>Earthquake catalog</b> access to Geofon, GobalCMT, USGS...</li>
|
|
<li><b>Cake</b>, Calculate synthetic arrival times</li>
|
|
<li><b>Seismosizer</b>, generate synthetic seismograms on-the-fly</li>
|
|
<li>
|
|
<b>Map</b>, swiftly inspect stations and events on interactive maps
|
|
</li>
|
|
</ul>
|
|
<p>And more, see <a href="https://pyrocko.org/">https://pyrocko.org/</a></p>
|
|
<p><b>NOTE:</b><br>If you installed snufflings from the
|
|
<a href="https://github.com/pyrocko/contrib-snufflings">user contributed
|
|
snufflings repository</a><br>you also have to pull an update from there.
|
|
</p>
|
|
<p style="width: 100%; background-color: #e9b96e; margin: 5px; padding: 50;"
|
|
align="center">
|
|
<b>You can always press <code>?</code> for help!</b>
|
|
</p>
|
|
</html>''')
|
|
|
|
lyt = qw.QVBoxLayout()
|
|
lyt.addWidget(text)
|
|
|
|
def remove_custom_button():
|
|
self.setOption(self.HaveCustomButton1, False)
|
|
|
|
p.initializePage = remove_custom_button
|
|
|
|
p.setLayout(lyt)
|
|
self.addPage(p)
|
|
return p
|
|
|
|
|
|
class SnufflerWindow(qw.QMainWindow):
|
|
|
|
def __init__(
|
|
self, pile, stations=None, events=None, markers=None, ntracks=12,
|
|
marker_editor_sortable=True, follow=None, controls=True,
|
|
opengl=False, instant_close=False):
|
|
|
|
qw.QMainWindow.__init__(self)
|
|
|
|
self.instant_close = instant_close
|
|
|
|
self.dockwidget_to_toggler = {}
|
|
self.dockwidgets = []
|
|
|
|
self.setWindowTitle("Snuffler")
|
|
|
|
self.pile_viewer = pile_viewer.PileViewer(
|
|
pile, ntracks_shown_max=ntracks, use_opengl=opengl,
|
|
marker_editor_sortable=marker_editor_sortable,
|
|
panel_parent=self)
|
|
|
|
self.marker_editor = self.pile_viewer.marker_editor()
|
|
self.add_panel(
|
|
'Markers', self.marker_editor, visible=False,
|
|
where=qc.Qt.RightDockWidgetArea)
|
|
if stations:
|
|
self.get_view().add_stations(stations)
|
|
|
|
if events:
|
|
self.get_view().add_events(events)
|
|
|
|
if len(events) == 1:
|
|
self.get_view().set_active_event(events[0])
|
|
|
|
if markers:
|
|
self.get_view().add_markers(markers)
|
|
self.get_view().associate_phases_to_events()
|
|
|
|
self.tabs = SnufflerTabs(self)
|
|
self.setCentralWidget(self.tabs)
|
|
self.add_tab('Main', self.pile_viewer)
|
|
|
|
self.pile_viewer.setup_snufflings()
|
|
|
|
self.main_controls = self.pile_viewer.controls()
|
|
self.add_panel('Main Controls', self.main_controls, visible=controls)
|
|
self.show()
|
|
|
|
self.get_view().setFocus(qc.Qt.OtherFocusReason)
|
|
|
|
sb = self.statusBar()
|
|
sb.clearMessage()
|
|
sb.showMessage('Welcome to Snuffler! Press <?> for help.')
|
|
|
|
snuffler_config = self.pile_viewer.viewer.config
|
|
|
|
if snuffler_config.first_start:
|
|
wizard = SnufflerStartWizard(self)
|
|
|
|
@qc.pyqtSlot()
|
|
def wizard_finished(result):
|
|
if result == wizard.Accepted:
|
|
snuffler_config.first_start = False
|
|
config.write_config(snuffler_config, 'snuffler')
|
|
|
|
wizard.finished.connect(wizard_finished)
|
|
|
|
wizard.show()
|
|
|
|
if follow:
|
|
self.get_view().follow(float(follow))
|
|
|
|
self.closing = False
|
|
|
|
def sizeHint(self):
|
|
return qc.QSize(1024, 768)
|
|
# return qc.QSize(800, 600) # used for screen shots in tutorial
|
|
|
|
def keyPressEvent(self, ev):
|
|
self.get_view().keyPressEvent(ev)
|
|
|
|
def get_view(self):
|
|
return self.pile_viewer.get_view()
|
|
|
|
def get_panel_parent_widget(self):
|
|
return self
|
|
|
|
def add_tab(self, name, widget):
|
|
self.tabs.append_tab(widget, name)
|
|
|
|
def remove_tab(self, widget):
|
|
self.tabs.remove_tab(widget)
|
|
|
|
def add_panel(self, name, panel, visible=False, volatile=False,
|
|
where=qc.Qt.BottomDockWidgetArea):
|
|
|
|
if not self.dockwidgets:
|
|
self.dockwidgets = []
|
|
|
|
dws = [x for x in self.dockwidgets if self.dockWidgetArea(x) == where]
|
|
|
|
dockwidget = qw.QDockWidget(name, self)
|
|
self.dockwidgets.append(dockwidget)
|
|
dockwidget.setWidget(panel)
|
|
panel.setParent(dockwidget)
|
|
self.addDockWidget(where, dockwidget)
|
|
|
|
if dws:
|
|
self.tabifyDockWidget(dws[-1], dockwidget)
|
|
|
|
self.toggle_panel(dockwidget, visible)
|
|
|
|
mitem = qw.QAction(name, None)
|
|
|
|
def toggle_panel(checked):
|
|
self.toggle_panel(dockwidget, True)
|
|
|
|
mitem.triggered.connect(toggle_panel)
|
|
|
|
if volatile:
|
|
def visibility(visible):
|
|
if not visible:
|
|
self.remove_panel(panel)
|
|
|
|
dockwidget.visibilityChanged.connect(
|
|
visibility)
|
|
|
|
self.get_view().add_panel_toggler(mitem)
|
|
self.dockwidget_to_toggler[dockwidget] = mitem
|
|
|
|
def toggle_panel(self, dockwidget, visible):
|
|
if visible is None:
|
|
visible = not dockwidget.isVisible()
|
|
|
|
dockwidget.setVisible(visible)
|
|
if visible:
|
|
w = dockwidget.widget()
|
|
minsize = w.minimumSize()
|
|
w.setMinimumHeight(w.sizeHint().height() + 5)
|
|
|
|
def reset_minimum_size():
|
|
import sip
|
|
if not sip.isdeleted(w):
|
|
w.setMinimumSize(minsize)
|
|
|
|
qc.QTimer.singleShot(200, reset_minimum_size)
|
|
|
|
dockwidget.setFocus()
|
|
dockwidget.raise_()
|
|
|
|
def toggle_marker_editor(self):
|
|
self.toggle_panel(self.marker_editor.parent(), None)
|
|
|
|
def toggle_main_controls(self):
|
|
self.toggle_panel(self.main_controls.parent(), None)
|
|
|
|
def remove_panel(self, panel):
|
|
dockwidget = panel.parent()
|
|
self.removeDockWidget(dockwidget)
|
|
dockwidget.setParent(None)
|
|
mitem = self.dockwidget_to_toggler[dockwidget]
|
|
self.get_view().remove_panel_toggler(mitem)
|
|
|
|
def return_tag(self):
|
|
return self.get_view().return_tag
|
|
|
|
def confirm_close(self):
|
|
ret = qw.QMessageBox.question(
|
|
self,
|
|
'Snuffler',
|
|
'Close Snuffler window?',
|
|
qw.QMessageBox.Cancel | qw.QMessageBox.Ok,
|
|
qw.QMessageBox.Ok)
|
|
|
|
return ret == qw.QMessageBox.Ok
|
|
|
|
def closeEvent(self, event):
|
|
if self.instant_close or self.confirm_close():
|
|
self.closing = True
|
|
self.pile_viewer.cleanup()
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
|
|
def is_closing(self):
|
|
return self.closing
|
|
|
|
|
|
class Snuffler(qw.QApplication):
|
|
|
|
def __init__(self):
|
|
qw.QApplication.__init__(self, sys.argv)
|
|
self.lastWindowClosed.connect(self.myQuit)
|
|
self.server = None
|
|
self.loader = None
|
|
|
|
def install_sigint_handler(self):
|
|
self._old_signal_handler = signal.signal(
|
|
signal.SIGINT,
|
|
self.myCloseAllWindows)
|
|
|
|
def uninstall_sigint_handler(self):
|
|
signal.signal(signal.SIGINT, self._old_signal_handler)
|
|
|
|
def start_server(self):
|
|
self.connections = []
|
|
s = qn.QTcpServer(self)
|
|
s.listen(qn.QHostAddress.LocalHost)
|
|
s.newConnection.connect(
|
|
self.handle_accept)
|
|
self.server = s
|
|
|
|
def start_loader(self):
|
|
self.loader = SimpleConnectionHandler(
|
|
self,
|
|
add_files=self.add_files,
|
|
update_progress=self.update_progress)
|
|
ticket = os.urandom(32)
|
|
self.forker.spawn('loader', self.server.serverPort(), ticket)
|
|
self.connection_handlers[ticket] = self.loader
|
|
|
|
def handle_accept(self):
|
|
sock = self.server.nextPendingConnection()
|
|
con = Connection(self, sock)
|
|
self.connections.append(con)
|
|
|
|
con.disconnected.connect(
|
|
self.handle_disconnected)
|
|
|
|
con.received.connect(
|
|
self.handle_received_ticket)
|
|
|
|
def handle_disconnected(self, connection):
|
|
self.connections.remove(connection)
|
|
connection.close()
|
|
del connection
|
|
|
|
def handle_received_ticket(self, connection, object):
|
|
if not isinstance(object, str):
|
|
self.handle_disconnected(connection)
|
|
|
|
ticket = object
|
|
if ticket in self.connection_handlers:
|
|
h = self.connection_handlers[ticket]
|
|
connection.received.disconnect(
|
|
self.handle_received_ticket)
|
|
|
|
h.set_connection(connection)
|
|
else:
|
|
self.handle_disconnected(connection)
|
|
|
|
def snuffler_windows(self):
|
|
return [w for w in self.topLevelWidgets()
|
|
if isinstance(w, SnufflerWindow) and not w.is_closing()]
|
|
|
|
def event(self, e):
|
|
if isinstance(e, qg.QFileOpenEvent):
|
|
paths = [str(e.file())]
|
|
wins = self.snuffler_windows()
|
|
if wins:
|
|
wins[0].get_view().load_soon(paths)
|
|
|
|
return True
|
|
else:
|
|
return qw.QApplication.event(self, e)
|
|
|
|
def load(self, pathes, cachedirname, pattern, format):
|
|
if not self.loader:
|
|
self.start_loader()
|
|
|
|
self.loader.ship(
|
|
('load', pathes, cachedirname, pattern, format))
|
|
|
|
def add_files(self, files):
|
|
p = self.pile_viewer.get_pile()
|
|
p.add_files(files)
|
|
self.pile_viewer.update_contents()
|
|
|
|
def update_progress(self, task, percent):
|
|
self.pile_viewer.progressbars.set_status(task, percent)
|
|
|
|
def myCloseAllWindows(self, *args):
|
|
self.closeAllWindows()
|
|
|
|
def myQuit(self, *args):
|
|
self.quit()
|