issue126-backup #198
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import os
|
||||||
sys.path.append(str(pathlib.Path(__file__).absolute().parent.parent / 'src'))
|
sys.path.append(str(pathlib.Path(__file__).absolute().parent.parent / 'src'))
|
||||||
|
|
||||||
from nmreval.configs import check_for_config
|
from nmreval.configs import check_for_config
|
||||||
@ -16,12 +17,45 @@ from nmreval.lib.logger import handle_exception
|
|||||||
sys.excepthook = handle_exception
|
sys.excepthook = handle_exception
|
||||||
|
|
||||||
from gui_qt import App
|
from gui_qt import App
|
||||||
|
from gui_qt.Qt import QtCore
|
||||||
|
|
||||||
app = App(['Team Rocket FTW!'])
|
app = App(['NMReval'])
|
||||||
|
|
||||||
from gui_qt.main.mainwindow import NMRMainWindow
|
from gui_qt.main.mainwindow import NMRMainWindow
|
||||||
|
from gui_qt.lib.backup import BackupManager
|
||||||
|
|
||||||
|
|
||||||
|
def do_autosave():
|
||||||
|
# autosave and update timestamp in db
|
||||||
|
success = mplQt.autosave()
|
||||||
|
if success:
|
||||||
|
backuping.update_last_save()
|
||||||
|
|
||||||
|
# autosave stuff: keep track of instance and their backup files
|
||||||
|
backuping = BackupManager()
|
||||||
|
|
||||||
|
# look for autosaves in DB without running programs
|
||||||
|
files = backuping.search_unsaved()
|
||||||
|
|
||||||
|
# tell everyone what autosave files belongs to this process
|
||||||
|
pid = os.getpid()
|
||||||
|
bck_name = backuping.create_entry(pid)
|
||||||
|
mplQt = NMRMainWindow(bck_file=bck_name)
|
||||||
|
|
||||||
|
# one manual autosave to create the file
|
||||||
|
do_autosave()
|
||||||
|
|
||||||
|
# load all selected autosaves to program
|
||||||
|
for f in files:
|
||||||
|
mplQt.management.load_files(f)
|
||||||
|
f.unlink()
|
||||||
|
|
||||||
|
timer = QtCore.QTimer()
|
||||||
|
timer.timeout.connect(do_autosave)
|
||||||
|
timer.start(3 * 60 * 1000)
|
||||||
|
|
||||||
|
app.aboutToQuit.connect(backuping.close)
|
||||||
|
|
||||||
mplQt = NMRMainWindow()
|
|
||||||
mplQt.show()
|
mplQt.show()
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui'
|
# Form implementation generated from reading ui file 'resources/_ui/basewindow.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt5 UI code generator 5.15.9
|
# Created by: PyQt5 UI code generator 5.15.10
|
||||||
#
|
#
|
||||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||||
# run again. Do not edit this file unless you know what you are doing.
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
|
207
src/gui_qt/lib/backup.py
Normal file
207
src/gui_qt/lib/backup.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from nmreval.configs import config_paths
|
||||||
|
from ..Qt import QtCore, QtWidgets
|
||||||
|
|
||||||
|
DB_FILE = '/tmp/nmreval.db'
|
||||||
|
|
||||||
|
|
||||||
|
class BackupManager(QtCore.QObject):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.create_table()
|
||||||
|
self._pid = None
|
||||||
|
|
||||||
|
def create_table(self):
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
con.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS sessions "
|
||||||
|
"(pid INTEGER NOT NULL, backup_file TEXT NOT NULL, last_save TEXT);"
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_entry(self, pid: int):
|
||||||
|
backup_path = config_paths() / f'autosave_{datetime.now().strftime("%Y-%m-%d_%H%M%S")}_{pid}.nmr'
|
||||||
|
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
con.execute('INSERT INTO sessions VALUES(?, ?, ?);',
|
||||||
|
(pid, str(backup_path), None))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
self._pid = pid
|
||||||
|
|
||||||
|
return backup_path
|
||||||
|
|
||||||
|
def update_last_save(self):
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
con.execute(
|
||||||
|
'UPDATE sessions SET last_save = ? WHERE pid = ?',
|
||||||
|
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self._pid)
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def search_unsaved(self):
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = con.cursor()
|
||||||
|
res = cursor.execute('SELECT sessions.* FROM sessions;')
|
||||||
|
con.commit()
|
||||||
|
data = res.fetchall()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
missing_processes = []
|
||||||
|
|
||||||
|
for pid, fname, save_date in data:
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
if Path(fname).exists():
|
||||||
|
missing_processes.append((pid, fname, save_date))
|
||||||
|
else:
|
||||||
|
# remove entries without valid file
|
||||||
|
self.remove_row(pid)
|
||||||
|
|
||||||
|
if missing_processes:
|
||||||
|
msg = QLonelyBackupWindow()
|
||||||
|
msg.add_files(missing_processes)
|
||||||
|
msg.exec()
|
||||||
|
toberead = msg.recover
|
||||||
|
|
||||||
|
for pid in toberead[0]:
|
||||||
|
self.remove_file(pid)
|
||||||
|
|
||||||
|
for pid in toberead[1]:
|
||||||
|
self.remove_row(pid)
|
||||||
|
|
||||||
|
return list(toberead[1].values())
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def remove_row(self, pid):
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
con.execute('DELETE FROM sessions WHERE pid = ?', (pid,))
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def remove_file(self, pid: int = None):
|
||||||
|
if pid is None:
|
||||||
|
pid = self._pid
|
||||||
|
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = con.cursor()
|
||||||
|
|
||||||
|
# remove backup file
|
||||||
|
res = cursor.execute('SELECT sessions.backup_file FROM sessions WHERE pid = ?', (pid,))
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
fname = Path(res.fetchone()[0])
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
fname.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
# because autosave backups in a *.nmr.0 file and then moves it to *.nmr we look also for this one.
|
||||||
|
fname.with_suffix('.nmr.0').unlink(missing_ok=True)
|
||||||
|
|
||||||
|
# after removal of file also remove entry
|
||||||
|
self.remove_row(pid)
|
||||||
|
|
||||||
|
def delete_db_if_empty(self):
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = con.cursor()
|
||||||
|
res = cursor.execute('SELECT COUNT(sessions.pid) FROM sessions GROUP BY sessions.pid;')
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
remaining_processes = res.fetchone()
|
||||||
|
con.close()
|
||||||
|
if remaining_processes is None:
|
||||||
|
Path(DB_FILE).unlink()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.remove_file()
|
||||||
|
self.delete_db_if_empty()
|
||||||
|
|
||||||
|
|
||||||
|
class QLonelyBackupWindow(QtWidgets.QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.recover = [[], {}] # list of pid to delete, dict of files to read {pid: file}
|
||||||
|
|
||||||
|
self.setWindowTitle('Adopt a file!')
|
||||||
|
self.resize(720, 320)
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
self.label = QtWidgets.QLabel(self)
|
||||||
|
self.label.setText('Abandoned backup file(s) looking for a loving home!\n'
|
||||||
|
'(Files will all be loaded to same instance)')
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
self.table = QtWidgets.QTableWidget(self)
|
||||||
|
self.table.setColumnCount(4)
|
||||||
|
self.table.setHorizontalHeaderLabels(['File name', 'Last saved', 'File size', 'Action'])
|
||||||
|
self.table.setGridStyle(QtCore.Qt.PenStyle.DashLine)
|
||||||
|
# self.table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.table.verticalHeader().setVisible(False)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
self.buttons = QtWidgets.QDialogButtonBox(self)
|
||||||
|
self.buttons.setOrientation(QtCore.Qt.Orientation.Horizontal)
|
||||||
|
self.buttons.setStandardButtons(QtWidgets.QDialogButtonBox.Ok)
|
||||||
|
layout.addWidget(self.buttons)
|
||||||
|
|
||||||
|
self.buttons.accepted.connect(self.accept)
|
||||||
|
|
||||||
|
def add_files(self, entries):
|
||||||
|
self.table.setRowCount(len(entries))
|
||||||
|
for i, (pid, path, date) in enumerate(entries):
|
||||||
|
path = Path(path)
|
||||||
|
item1 = QtWidgets.QTableWidgetItem(path.name)
|
||||||
|
item1.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
item1.setData(QtCore.Qt.ItemDataRole.UserRole, pid)
|
||||||
|
item1.setData(QtCore.Qt.ItemDataRole.UserRole+1, path)
|
||||||
|
self.table.setItem(i, 0, item1)
|
||||||
|
|
||||||
|
item2 = QtWidgets.QTableWidgetItem(date)
|
||||||
|
item2.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
self.table.setItem(i, 1, item2)
|
||||||
|
|
||||||
|
size = path.stat().st_size
|
||||||
|
size_cnt = 0
|
||||||
|
while size > 1024:
|
||||||
|
# make file size human-readable
|
||||||
|
size /= 1024
|
||||||
|
size_cnt += 1
|
||||||
|
if size_cnt == 5:
|
||||||
|
break
|
||||||
|
|
||||||
|
byte = ['bytes', 'kB', 'MiB', 'GiB', 'TiB', 'PiB'][size_cnt]
|
||||||
|
|
||||||
|
item3 = QtWidgets.QTableWidgetItem(f'{size:.2f} {byte}')
|
||||||
|
item3.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||||
|
self.table.setItem(i, 2, item3)
|
||||||
|
|
||||||
|
cw = QtWidgets.QComboBox(self)
|
||||||
|
cw.addItems(['Load', 'Delete', 'Keep for later'])
|
||||||
|
self.table.setCellWidget(i, 3, cw)
|
||||||
|
|
||||||
|
self.table.resizeColumnsToContents()
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
for i in range(self.table.rowCount()):
|
||||||
|
decision = self.table.cellWidget(i, 3).currentIndex()
|
||||||
|
item = self.table.item(i, 0)
|
||||||
|
pid = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||||
|
if decision == 0:
|
||||||
|
# load file
|
||||||
|
self.recover[1][pid] = item.data(QtCore.Qt.ItemDataRole.UserRole+1)
|
||||||
|
elif decision == 1:
|
||||||
|
# delete
|
||||||
|
self.recover[0].append(pid)
|
||||||
|
else:
|
||||||
|
# do nothing
|
||||||
|
pass
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
@ -1,4 +1,3 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .codeeditor import _make_textformats
|
from .codeeditor import _make_textformats
|
||||||
@ -6,12 +5,14 @@ from ..Qt import QtWidgets, QtCore, QtGui
|
|||||||
from nmreval.configs import config_paths
|
from nmreval.configs import config_paths
|
||||||
|
|
||||||
|
|
||||||
STYLES = {'INFO': _make_textformats('black'),
|
STYLES = {
|
||||||
'WARNING': _make_textformats('blue'),
|
'INFO': _make_textformats('black'),
|
||||||
'ERROR': _make_textformats('red', 'bold'),
|
'WARNING': _make_textformats('blue'),
|
||||||
'DEBUG': _make_textformats('black', 'italic'),
|
'ERROR': _make_textformats('red', 'bold'),
|
||||||
'file': _make_textformats('red', 'italic'),
|
'DEBUG': _make_textformats('black', 'italic'),
|
||||||
'PyError': _make_textformats('red', 'bold-italic')}
|
'file': _make_textformats('red', 'italic'),
|
||||||
|
'PyError': _make_textformats('red', 'bold-italic'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class LogHighlighter(QtGui.QSyntaxHighlighter):
|
class LogHighlighter(QtGui.QSyntaxHighlighter):
|
||||||
|
288
src/gui_qt/lib/update.py
Normal file
288
src/gui_qt/lib/update.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from os import getenv, stat
|
||||||
|
from os.path import exists
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
from PyQt5 import QtWidgets, QtCore
|
||||||
|
|
||||||
|
from nmreval.lib.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDialog(QtWidgets.QDialog):
|
||||||
|
startDownload = QtCore.pyqtSignal(tuple)
|
||||||
|
restartSignal = QtCore.pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, filename: str = None, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
filename = getenv('APPIMAGE')
|
||||||
|
self._appfile = filename
|
||||||
|
|
||||||
|
self.updater = Updater()
|
||||||
|
|
||||||
|
self.thread = QtCore.QThread(self)
|
||||||
|
self.thread.start()
|
||||||
|
self.helper = Downloader()
|
||||||
|
self.startDownload.connect(self.helper.run_download)
|
||||||
|
self.helper.progressChanged.connect(self.status.setText)
|
||||||
|
self.helper.finished.connect(self.finish_update)
|
||||||
|
self.helper.started.connect(self.status.show)
|
||||||
|
self.helper.moveToThread(self.thread)
|
||||||
|
|
||||||
|
self.look_for_updates(self._appfile)
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
self.setWindowTitle('Updates')
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
|
||||||
|
self.label = QtWidgets.QLabel()
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
self.status = QtWidgets.QLabel()
|
||||||
|
self.status.hide()
|
||||||
|
layout.addWidget(self.status)
|
||||||
|
layout.addSpacing(10)
|
||||||
|
|
||||||
|
self.dialog_button = QtWidgets.QDialogButtonBox()
|
||||||
|
self.dialog_button.accepted.connect(self.update_appimage)
|
||||||
|
self.dialog_button.rejected.connect(self.close)
|
||||||
|
layout.addWidget(self.dialog_button)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def look_for_updates(self, filename=None):
|
||||||
|
logger.info(f'Looking for updates, compare to file {filename}')
|
||||||
|
# Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage
|
||||||
|
is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename)
|
||||||
|
|
||||||
|
label_text = ''
|
||||||
|
|
||||||
|
if is_updateble is None:
|
||||||
|
label_text += '<p>Could not determine if this version is newer, please update manually (if necessary).</p>'
|
||||||
|
dialog_bttns = QtWidgets.QDialogButtonBox.Close
|
||||||
|
elif is_updateble:
|
||||||
|
label_text += '<p>Different version available. Press Ok to download this version, Cancel to ignore.</p>'
|
||||||
|
dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
|
||||||
|
else:
|
||||||
|
label_text += '<p>Version may be already up-to-date.</p>'
|
||||||
|
dialog_bttns = QtWidgets.QDialogButtonBox.Close
|
||||||
|
|
||||||
|
if m_time_zsync is None:
|
||||||
|
label_text += '<p>Creation date of remote version is unknown.</p>'
|
||||||
|
else:
|
||||||
|
label_text += f'<p>Date of most recent AppImage: {m_time_zsync.strftime("%d %B %Y %H:%M")}</p>'
|
||||||
|
|
||||||
|
if m_time_file is None:
|
||||||
|
label_text += 'No AppImage file found, press Ok to download latest version.'
|
||||||
|
dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Close
|
||||||
|
else:
|
||||||
|
label_text += f'<p>Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}</p>'
|
||||||
|
|
||||||
|
self.label.setText(label_text)
|
||||||
|
self.dialog_button.setStandardButtons(dialog_bttns)
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def update_appimage(self):
|
||||||
|
if self._appfile is None:
|
||||||
|
args = (self.updater.zsync_url,)
|
||||||
|
else:
|
||||||
|
# this breaks the download for some reason
|
||||||
|
args = (self.updater.zsync_url, self._appfile)
|
||||||
|
|
||||||
|
self.dialog_button.setEnabled(False)
|
||||||
|
|
||||||
|
self.startDownload.emit(args)
|
||||||
|
self.status.show()
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(int, str)
|
||||||
|
def finish_update(self, retcode: int, file_loc: str):
|
||||||
|
restart = QRestartWindow(state=retcode, file_loc=file_loc, parent=self)
|
||||||
|
res = restart.exec()
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
if res == QtWidgets.QMessageBox.Ok:
|
||||||
|
self.restartSignal.emit()
|
||||||
|
|
||||||
|
def closeEvent(self, evt):
|
||||||
|
self.thread.quit()
|
||||||
|
self.thread.wait()
|
||||||
|
|
||||||
|
super().closeEvent(evt)
|
||||||
|
|
||||||
|
|
||||||
|
class QRestartWindow(QtWidgets.QMessageBox):
|
||||||
|
def __init__(self, state: int, file_loc: str, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self._appfile = file_loc
|
||||||
|
|
||||||
|
if state:
|
||||||
|
self.setText('Download failed')
|
||||||
|
self.setDetailedText(f'Status code of failure is {state}')
|
||||||
|
self.setStandardButtons(QtWidgets.QMessageBox.Close)
|
||||||
|
self.setIcon(QtWidgets.QMessageBox.Warning)
|
||||||
|
else:
|
||||||
|
self.setText('Download completed!')
|
||||||
|
self.setInformativeText("Press Restart to use new AppImage")
|
||||||
|
self.setDetailedText(f'Location of AppImage: {file_loc}')
|
||||||
|
|
||||||
|
self.setIcon(QtWidgets.QMessageBox.Information)
|
||||||
|
|
||||||
|
self.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Close)
|
||||||
|
restart_button = self.button(QtWidgets.QMessageBox.Ok)
|
||||||
|
restart_button.setText('Restart')
|
||||||
|
|
||||||
|
self.buttonClicked.connect(self.maybe_close)
|
||||||
|
|
||||||
|
def maybe_close(self):
|
||||||
|
if self.clickedButton() == self.button(QtWidgets.QMessageBox.Ok):
|
||||||
|
app = QtWidgets.QApplication.instance()
|
||||||
|
app.quit()
|
||||||
|
subprocess.Popen(self._appfile)
|
||||||
|
|
||||||
|
|
||||||
|
class Downloader(QtCore.QObject):
|
||||||
|
started = QtCore.pyqtSignal()
|
||||||
|
finished = QtCore.pyqtSignal(int, str)
|
||||||
|
progressChanged = QtCore.pyqtSignal(str)
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(tuple)
|
||||||
|
def run_download(self, args: tuple[str]):
|
||||||
|
status = 0
|
||||||
|
appimage_location = args[0][:-6]
|
||||||
|
logger.info(f'Download {appimage_location}')
|
||||||
|
if len(args) == 2:
|
||||||
|
new_file = Path(args[1])
|
||||||
|
else:
|
||||||
|
new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage'
|
||||||
|
|
||||||
|
if new_file.exists():
|
||||||
|
os.rename(new_file, new_file.with_suffix('.AppImage.old'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(appimage_location) as response:
|
||||||
|
with new_file.open('wb') as f:
|
||||||
|
f.write(response.read())
|
||||||
|
|
||||||
|
new_file.chmod(0o755)
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.exception(f'Download failed with {e}')
|
||||||
|
status = 3
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'Download failed with {e.args}')
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
if status != 0:
|
||||||
|
logger.warning('Download failed, restore previous AppImage')
|
||||||
|
try:
|
||||||
|
os.rename(new_file.with_suffix('.AppImage.old'), new_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
# zsync does not support https
|
||||||
|
|
||||||
|
self.finished.emit(status, str(new_file))
|
||||||
|
|
||||||
|
|
||||||
|
class Updater:
|
||||||
|
host = 'gitea.pkm.physik.tu-darmstadt.de/api/packages/IPKM/generic/NMReval/latest/'
|
||||||
|
version = 'NMReval-latest-x86_64'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zsync_url(self):
|
||||||
|
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_zsync():
|
||||||
|
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
||||||
|
m_time_zsync = None
|
||||||
|
checksum_zsync = None
|
||||||
|
zsync_file = None
|
||||||
|
filename = None
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url_zsync) as response:
|
||||||
|
zsync_file = response.read()
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.error(f'Request for zsync returned code {e}')
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'Download of zsync failed with exception {e.args}')
|
||||||
|
|
||||||
|
if zsync_file is not None:
|
||||||
|
for line in zsync_file.split(b'\n'):
|
||||||
|
try:
|
||||||
|
kw, val = line.split(b': ')
|
||||||
|
time_string = str(val, encoding='utf-8')
|
||||||
|
time_format = '%a, %d %b %Y %H:%M:%S %z'
|
||||||
|
if kw == b'MTime':
|
||||||
|
try:
|
||||||
|
m_time_zsync = datetime.strptime(time_string, time_format).astimezone(None)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f'zsync time "{time_string}" does not match "{time_format}"')
|
||||||
|
elif kw == b'SHA-1':
|
||||||
|
checksum_zsync = str(val, encoding='utf-8')
|
||||||
|
elif kw == b'Filename':
|
||||||
|
filename = str(val, encoding='utf-8')
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# stop when empty line is reached
|
||||||
|
break
|
||||||
|
|
||||||
|
return m_time_zsync, checksum_zsync, filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_appimage_info(filename: str):
|
||||||
|
if filename is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if not exists(filename):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
stat_mtime = stat(filename).st_mtime
|
||||||
|
m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0)
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
checksum_file = hashlib.sha1(f.read()).hexdigest()
|
||||||
|
if checksum_file is None:
|
||||||
|
logger.warning('No checksum for AppImage calculated')
|
||||||
|
|
||||||
|
return m_time_file, checksum_file
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]:
|
||||||
|
m_time_zsync, checksum_zsync, appname = Updater.get_zsync()
|
||||||
|
m_time_file, checksum_file = Updater.get_appimage_info(filename)
|
||||||
|
|
||||||
|
logger.debug(f'zsync information {m_time_zsync}, {checksum_zsync}, {appname}')
|
||||||
|
logger.debug(f'file information {m_time_file}, {checksum_file}')
|
||||||
|
|
||||||
|
if not ((checksum_file is not None) and (checksum_zsync is not None)):
|
||||||
|
return None, m_time_file, m_time_zsync
|
||||||
|
else:
|
||||||
|
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
from gui_qt import App
|
||||||
|
|
||||||
|
app = App(['Team Rocket FTW!'])
|
||||||
|
|
||||||
|
updater = UpdateDialog()
|
||||||
|
updater.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,20 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import urllib.request
|
|
||||||
from os import getenv, stat
|
|
||||||
from os.path import exists
|
|
||||||
import hashlib
|
|
||||||
import subprocess
|
|
||||||
from datetime import datetime
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
from numpy import linspace
|
from numpy import linspace
|
||||||
from scipy.interpolate import interp1d
|
from scipy.interpolate import interp1d
|
||||||
|
|
||||||
from nmreval.lib.logger import logger
|
|
||||||
|
|
||||||
from ..Qt import QtGui, QtWidgets, QtCore
|
from ..Qt import QtGui, QtWidgets, QtCore
|
||||||
|
|
||||||
|
|
||||||
@ -63,225 +52,3 @@ class RdBuCMap:
|
|||||||
return col
|
return col
|
||||||
|
|
||||||
|
|
||||||
class UpdateDialog(QtWidgets.QDialog):
|
|
||||||
startDownload = QtCore.pyqtSignal(tuple)
|
|
||||||
|
|
||||||
def __init__(self, filename: str = None, parent=None):
|
|
||||||
super().__init__(parent=parent)
|
|
||||||
self._init_ui()
|
|
||||||
|
|
||||||
if filename is None:
|
|
||||||
filename = getenv('APPIMAGE')
|
|
||||||
self._appfile = filename
|
|
||||||
|
|
||||||
self.updater = Updater()
|
|
||||||
|
|
||||||
self.thread = QtCore.QThread(self)
|
|
||||||
self.thread.start()
|
|
||||||
self.helper = Downloader()
|
|
||||||
self.startDownload.connect(self.helper.run_download)
|
|
||||||
self.helper.progressChanged.connect(self.status.setText)
|
|
||||||
self.helper.finished.connect(self.finish_update)
|
|
||||||
self.helper.started.connect(self.status.show)
|
|
||||||
self.helper.moveToThread(self.thread)
|
|
||||||
|
|
||||||
self.look_for_updates(self._appfile)
|
|
||||||
|
|
||||||
def _init_ui(self):
|
|
||||||
self.setWindowTitle('Updates')
|
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
|
|
||||||
self.label = QtWidgets.QLabel()
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
|
|
||||||
layout.addSpacing(10)
|
|
||||||
|
|
||||||
self.status = QtWidgets.QLabel()
|
|
||||||
self.status.hide()
|
|
||||||
layout.addWidget(self.status)
|
|
||||||
layout.addSpacing(10)
|
|
||||||
|
|
||||||
self.dialog_button = QtWidgets.QDialogButtonBox()
|
|
||||||
self.dialog_button.accepted.connect(self.update_appimage)
|
|
||||||
self.dialog_button.rejected.connect(self.close)
|
|
||||||
layout.addWidget(self.dialog_button)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def look_for_updates(self, filename=None):
|
|
||||||
logger.info(f'Looking for updates, compare to file {filename}')
|
|
||||||
# Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage
|
|
||||||
is_updateble, m_time_file, m_time_zsync = self.updater.get_update_information(filename)
|
|
||||||
|
|
||||||
label_text = ''
|
|
||||||
|
|
||||||
if is_updateble is None:
|
|
||||||
label_text += '<p>Could not determine if this version is newer, please update manually (if necessary).</p>'
|
|
||||||
dialog_bttns = QtWidgets.QDialogButtonBox.Close
|
|
||||||
elif is_updateble:
|
|
||||||
label_text += '<p>Newer version available. Press Ok to download new version, Cancel to ignore.</p>'
|
|
||||||
dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
|
|
||||||
else:
|
|
||||||
label_text += '<p>Version may be already up-to-date.</p>'
|
|
||||||
dialog_bttns = QtWidgets.QDialogButtonBox.Close
|
|
||||||
|
|
||||||
if m_time_zsync is None:
|
|
||||||
label_text += '<p>Creation date of remote version is unknown.</p>'
|
|
||||||
else:
|
|
||||||
label_text += f'<p>Date of most recent AppImage: {m_time_zsync.strftime("%d %B %Y %H:%M")}</p>'
|
|
||||||
|
|
||||||
if m_time_file is None:
|
|
||||||
label_text += 'No AppImage file found, press Ok to download latest version.'
|
|
||||||
dialog_bttns = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Close
|
|
||||||
else:
|
|
||||||
label_text += f'<p>Date of used AppImage: {m_time_file.strftime("%d %B %Y %H:%M")}</p>'
|
|
||||||
|
|
||||||
self.label.setText(label_text)
|
|
||||||
self.dialog_button.setStandardButtons(dialog_bttns)
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot()
|
|
||||||
def update_appimage(self):
|
|
||||||
if self._appfile is None:
|
|
||||||
args = (self.updater.zsync_url,)
|
|
||||||
else:
|
|
||||||
# this breaks the download for some reason
|
|
||||||
args = (self.updater.zsync_url, self._appfile)
|
|
||||||
|
|
||||||
self.dialog_button.setEnabled(False)
|
|
||||||
|
|
||||||
self.startDownload.emit(args)
|
|
||||||
self.status.show()
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(int, str)
|
|
||||||
def finish_update(self, retcode: int, file_loc: str):
|
|
||||||
if retcode == 0:
|
|
||||||
self.status.setText(f'Download complete.New AppImage lies in <p><em>{file_loc}</em>.</p>')
|
|
||||||
else:
|
|
||||||
self.status.setText(f'Download failed :( with return code {retcode}.')
|
|
||||||
self.dialog_button.setStandardButtons(QtWidgets.QDialogButtonBox.Close)
|
|
||||||
self.dialog_button.setEnabled(True)
|
|
||||||
|
|
||||||
def closeEvent(self, evt):
|
|
||||||
self.thread.quit()
|
|
||||||
self.thread.wait()
|
|
||||||
|
|
||||||
super().closeEvent(evt)
|
|
||||||
|
|
||||||
|
|
||||||
class Downloader(QtCore.QObject):
|
|
||||||
started = QtCore.pyqtSignal()
|
|
||||||
finished = QtCore.pyqtSignal(int, str)
|
|
||||||
progressChanged = QtCore.pyqtSignal(str)
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(tuple)
|
|
||||||
def run_download(self, args: tuple[str]):
|
|
||||||
status = 0
|
|
||||||
appimage_location = args[0][:-6]
|
|
||||||
logger.info(f'Download {appimage_location}')
|
|
||||||
if len(args) == 2:
|
|
||||||
new_file = Path(args[1])
|
|
||||||
else:
|
|
||||||
new_file = Path.home() / 'Downloads' / 'NMReval-latest-x86_64.AppImage'
|
|
||||||
|
|
||||||
if new_file.exists():
|
|
||||||
os.rename(new_file, new_file.with_suffix('.AppImage.old'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(appimage_location) as response:
|
|
||||||
with new_file.open('wb') as f:
|
|
||||||
f.write(response.read())
|
|
||||||
|
|
||||||
new_file.chmod(0o755)
|
|
||||||
except HTTPError as e:
|
|
||||||
logger.exception(f'Download failed with {e}')
|
|
||||||
status = 3
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f'Download failed with {e.args}')
|
|
||||||
status = 1
|
|
||||||
|
|
||||||
if status != 0:
|
|
||||||
logger.warning('Download failed, restore previous AppImage')
|
|
||||||
try:
|
|
||||||
os.rename(new_file.with_suffix('.AppImage.old'), new_file)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
# zsync does not support https
|
|
||||||
|
|
||||||
self.finished.emit(status, str(new_file))
|
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
|
||||||
host = 'gitea.pkm.physik.tu-darmstadt.de/api/packages/IPKM/generic/NMReval/latest/'
|
|
||||||
version = 'NMReval-latest-x86_64'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def zsync_url(self):
|
|
||||||
return f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_zsync():
|
|
||||||
url_zsync = f'https://{Updater.host}/{Updater.version}.AppImage.zsync'
|
|
||||||
m_time_zsync = None
|
|
||||||
checksum_zsync = None
|
|
||||||
zsync_file = None
|
|
||||||
filename = None
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(url_zsync) as response:
|
|
||||||
zsync_file = response.read()
|
|
||||||
except HTTPError as e:
|
|
||||||
logger.error(f'Request for zsync returned code {e}')
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f'Download of zsync failed with exception {e.args}')
|
|
||||||
|
|
||||||
if zsync_file is not None:
|
|
||||||
for line in zsync_file.split(b'\n'):
|
|
||||||
try:
|
|
||||||
kw, val = line.split(b': ')
|
|
||||||
time_string = str(val, encoding='utf-8')
|
|
||||||
time_format = '%a, %d %b %Y %H:%M:%S %z'
|
|
||||||
if kw == b'MTime':
|
|
||||||
try:
|
|
||||||
m_time_zsync = datetime.strptime(time_string, time_format).astimezone(None)
|
|
||||||
except ValueError:
|
|
||||||
logger.warning(f'zsync time "{time_string}" does not match "{time_format}"')
|
|
||||||
elif kw == b'SHA-1':
|
|
||||||
checksum_zsync = str(val, encoding='utf-8')
|
|
||||||
elif kw == b'Filename':
|
|
||||||
filename = str(val, encoding='utf-8')
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
# stop when empty line is reached
|
|
||||||
break
|
|
||||||
|
|
||||||
return m_time_zsync, checksum_zsync, filename
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_appimage_info(filename: str):
|
|
||||||
if filename is None:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
if not exists(filename):
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
stat_mtime = stat(filename).st_mtime
|
|
||||||
m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0)
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
checksum_file = hashlib.sha1(f.read()).hexdigest()
|
|
||||||
if checksum_file is None:
|
|
||||||
logger.warning('No checksum for AppImage calculated')
|
|
||||||
|
|
||||||
return m_time_file, checksum_file
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_update_information(filename: str) -> tuple[(bool | None), datetime, datetime]:
|
|
||||||
m_time_zsync, checksum_zsync, appname = Updater.get_zsync()
|
|
||||||
m_time_file, checksum_file = Updater.get_appimage_info(filename)
|
|
||||||
|
|
||||||
logger.debug(f'zsync information {m_time_zsync}, {checksum_zsync}, {appname}')
|
|
||||||
logger.debug(f'file information {m_time_file}, {checksum_file}')
|
|
||||||
|
|
||||||
if not ((checksum_file is not None) and (checksum_zsync is not None)):
|
|
||||||
return None, m_time_file, m_time_zsync
|
|
||||||
else:
|
|
||||||
return checksum_file != checksum_zsync, m_time_file, m_time_zsync
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from numpy import geomspace, linspace
|
from numpy import geomspace, linspace
|
||||||
@ -33,7 +33,7 @@ from ..math.smooth import QSmooth
|
|||||||
from ..nmr.coupling_calc import QCoupCalcDialog
|
from ..nmr.coupling_calc import QCoupCalcDialog
|
||||||
from ..nmr.t1_from_tau import QRelaxCalc
|
from ..nmr.t1_from_tau import QRelaxCalc
|
||||||
from .._py.basewindow import Ui_BaseWindow
|
from .._py.basewindow import Ui_BaseWindow
|
||||||
from ..lib.utils import UpdateDialog, Updater
|
from ..lib.update import UpdateDialog
|
||||||
|
|
||||||
|
|
||||||
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
||||||
@ -42,7 +42,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
save_ses_sig = QtCore.pyqtSignal(str)
|
save_ses_sig = QtCore.pyqtSignal(str)
|
||||||
rest_ses_sig = QtCore.pyqtSignal(str)
|
rest_ses_sig = QtCore.pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parents=None, path=None):
|
def __init__(self, parents=None, path=None, bck_file=None):
|
||||||
super().__init__(parent=parents)
|
super().__init__(parent=parents)
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
@ -75,21 +75,16 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self._init_gui()
|
self._init_gui()
|
||||||
self._init_signals()
|
self._init_signals()
|
||||||
|
|
||||||
if os.getenv('APPIMAGE') is not None:
|
|
||||||
if Updater.get_update_information(os.getenv('APPIMAGE'))[0]:
|
|
||||||
self.look_for_update()
|
|
||||||
|
|
||||||
self.check_for_backup()
|
|
||||||
|
|
||||||
self.__timer = QtCore.QTimer()
|
|
||||||
self.__backup_path = config_paths() / f'{datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S")}.nmr'
|
|
||||||
self.__timer.start(3*60*1000) # every three minutes
|
|
||||||
self.__timer.timeout.connect(self._autosave)
|
|
||||||
|
|
||||||
self.fit_timer = QtCore.QTimer()
|
self.fit_timer = QtCore.QTimer()
|
||||||
self.fit_timer.setInterval(500)
|
self.fit_timer.setInterval(500)
|
||||||
self.fit_timer.timeout.connect(
|
self.fit_timer.timeout.connect(
|
||||||
lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)'))
|
lambda: self.status.setText(f'Fit running... ({self.management.fitter.step} evaluations)')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__backup_path = pathlib.Path(bck_file)
|
||||||
|
if os.getenv('APPIMAGE'):
|
||||||
|
# ignore AppImages if not running from AppImage
|
||||||
|
self.look_for_update()
|
||||||
|
|
||||||
def _init_gui(self):
|
def _init_gui(self):
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -108,8 +103,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self.fitlim_button.setIcon(get_icon('fit_region'))
|
self.fitlim_button.setIcon(get_icon('fit_region'))
|
||||||
self.toolBar_fit.addWidget(self.fitlim_button)
|
self.toolBar_fit.addWidget(self.fitlim_button)
|
||||||
|
|
||||||
# self.area.dragEnterEvent = self.dragEnterEvent
|
|
||||||
|
|
||||||
while self.tabWidget.count() > 2:
|
while self.tabWidget.count() > 2:
|
||||||
self.tabWidget.removeTab(self.tabWidget.count()-1)
|
self.tabWidget.removeTab(self.tabWidget.count()-1)
|
||||||
|
|
||||||
@ -130,12 +123,15 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self.fitregion = RegionItem()
|
self.fitregion = RegionItem()
|
||||||
self._fit_plot_id = None
|
self._fit_plot_id = None
|
||||||
|
|
||||||
self.setGeometry(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter,
|
self.setGeometry(QtWidgets.QStyle.alignedRect(
|
||||||
self.size(), QtWidgets.qApp.desktop().availableGeometry()))
|
QtCore.Qt.LayoutDirection.LeftToRight,
|
||||||
|
QtCore.Qt.AlignmentFlag.AlignCenter,
|
||||||
|
self.size(),
|
||||||
|
QtWidgets.qApp.desktop().availableGeometry()),
|
||||||
|
)
|
||||||
|
|
||||||
self.datawidget.management = self.management
|
self.datawidget.management = self.management
|
||||||
self.integralwidget.management = self.management
|
self.integralwidget.management = self.management
|
||||||
# self.drawingswidget.graphs = self.management.graphs
|
|
||||||
|
|
||||||
self.ac_group = QtWidgets.QActionGroup(self)
|
self.ac_group = QtWidgets.QActionGroup(self)
|
||||||
self.ac_group.addAction(self.action_lm_fit)
|
self.ac_group.addAction(self.action_lm_fit)
|
||||||
@ -161,6 +157,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self.menuData.insertAction(self.actionRedo, self.actionUndo)
|
self.menuData.insertAction(self.actionRedo, self.actionUndo)
|
||||||
|
|
||||||
self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter)
|
self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.ac_group2.triggered.connect(self.change_fit_limits)
|
self.ac_group2.triggered.connect(self.change_fit_limits)
|
||||||
|
|
||||||
self.t1action.triggered.connect(lambda: self._show_tab('t1_temp'))
|
self.t1action.triggered.connect(lambda: self._show_tab('t1_temp'))
|
||||||
@ -235,8 +232,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
self.actionNext_window.triggered.connect(lambda: self.area.activateNextSubWindow())
|
self.actionNext_window.triggered.connect(lambda: self.area.activateNextSubWindow())
|
||||||
self.actionPrevious.triggered.connect(lambda: self.area.activatePreviousSubWindow())
|
self.actionPrevious.triggered.connect(lambda: self.area.activatePreviousSubWindow())
|
||||||
|
|
||||||
self.closeSignal.connect(self.close)
|
|
||||||
|
|
||||||
self.action_norm_max.triggered.connect(lambda: self.management.apply('norm', ('max',)))
|
self.action_norm_max.triggered.connect(lambda: self.management.apply('norm', ('max',)))
|
||||||
self.action_norm_max_abs.triggered.connect(lambda: self.management.apply('norm', ('maxabs',)))
|
self.action_norm_max_abs.triggered.connect(lambda: self.management.apply('norm', ('maxabs',)))
|
||||||
self.action_norm_first.triggered.connect(lambda: self.management.apply('norm', ('first',)))
|
self.action_norm_first.triggered.connect(lambda: self.management.apply('norm', ('first',)))
|
||||||
@ -864,10 +859,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
def item_from_graph(self, item, graph_id):
|
def item_from_graph(self, item, graph_id):
|
||||||
self.management.graphs[graph_id].remove_external(item)
|
self.management.graphs[graph_id].remove_external(item)
|
||||||
|
|
||||||
def closeEvent(self, evt):
|
|
||||||
# self._write_settings()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(int)
|
@QtCore.pyqtSlot(int)
|
||||||
def request_data(self, idx):
|
def request_data(self, idx):
|
||||||
idd = self.datawidget.get_indexes(idx=idx-1)
|
idd = self.datawidget.get_indexes(idx=idx-1)
|
||||||
@ -995,7 +986,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
|
|
||||||
self.editor = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self)
|
self.editor = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self)
|
||||||
self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions())
|
self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions())
|
||||||
self.editor.setWindowModality(QtCore.Qt.ApplicationModal)
|
self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
||||||
self.editor.show()
|
self.editor.show()
|
||||||
|
|
||||||
@QtCore.pyqtSlot(list)
|
@QtCore.pyqtSlot(list)
|
||||||
@ -1029,9 +1020,10 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@QtCore.pyqtSlot(name='on_actionDocumentation_triggered')
|
@QtCore.pyqtSlot(name='on_actionDocumentation_triggered')
|
||||||
def open_doc():
|
def open_doc():
|
||||||
docpath = '/autohome/dominik/auswerteprogramm3/doc/_build/html/index.html'
|
pass
|
||||||
import webbrowser
|
# docpath = '/autohome/dominik/auswerteprogramm3/doc/_build/html/index.html'
|
||||||
webbrowser.open(docpath)
|
# import webbrowser
|
||||||
|
# webbrowser.open(docpath)
|
||||||
|
|
||||||
def dropEvent(self, evt):
|
def dropEvent(self, evt):
|
||||||
if evt.mimeData().hasUrls():
|
if evt.mimeData().hasUrls():
|
||||||
@ -1060,13 +1052,13 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
if self.sender() == self.actionLife:
|
if self.sender() == self.actionLife:
|
||||||
from ..lib.gol import QGameOfLife
|
from ..lib.gol import QGameOfLife
|
||||||
game = QGameOfLife(parent=self)
|
game = QGameOfLife(parent=self)
|
||||||
game.setWindowModality(QtCore.Qt.NonModal)
|
game.setWindowModality(QtCore.Qt.WindowModality.NonModal)
|
||||||
game.show()
|
game.show()
|
||||||
|
|
||||||
elif self.sender() == self.actionMine:
|
elif self.sender() == self.actionMine:
|
||||||
from ..lib.stuff import QMines
|
from ..lib.stuff import QMines
|
||||||
game = QMines(parent=self)
|
game = QMines(parent=self)
|
||||||
game.setWindowModality(QtCore.Qt.NonModal)
|
game.setWindowModality(QtCore.Qt.WindowModality.NonModal)
|
||||||
game.show()
|
game.show()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -1094,9 +1086,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
def close(self):
|
def close(self):
|
||||||
write_state({'History': {'recent path': str(self.path)}})
|
write_state({'History': {'recent path': str(self.path)}})
|
||||||
|
|
||||||
# remove backup file when closing
|
|
||||||
self.__backup_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
super().close()
|
super().close()
|
||||||
|
|
||||||
def read_state(self):
|
def read_state(self):
|
||||||
@ -1120,40 +1109,16 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
|
|||||||
|
|
||||||
QLog(parent=self).show()
|
QLog(parent=self).show()
|
||||||
|
|
||||||
def _autosave(self):
|
def autosave(self) -> bool:
|
||||||
# TODO better separate thread may it takes some time to save
|
|
||||||
self.status.setText('Autosave...')
|
self.status.setText('Autosave...')
|
||||||
success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0'))
|
success = NMRWriter(self.management.graphs, self.management.data).export(self.__backup_path.with_suffix('.nmr.0'))
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path)
|
self.__backup_path.with_suffix('.nmr.0').rename(self.__backup_path)
|
||||||
|
|
||||||
self.status.setText('')
|
self.status.setText('')
|
||||||
|
|
||||||
def check_for_backup(self):
|
return success
|
||||||
backups = []
|
|
||||||
for filename in config_paths().glob('*.nmr'):
|
|
||||||
try:
|
|
||||||
backups.append((filename, datetime.datetime.strptime(filename.stem, "%Y-%m-%d_%H%M%S")))
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if backups:
|
|
||||||
backup_by_date = sorted(backups, key=lambda x: x[1])
|
|
||||||
msg = QtWidgets.QMessageBox.information(
|
|
||||||
self, 'Backup found',
|
|
||||||
f'{len(backups)} backup files in directory {backup_by_date[-1][0].parent} found.\n\n'
|
|
||||||
f'Latest backup date: {backup_by_date[-1][1]}\n\n'
|
|
||||||
f'Press Ok to load, Cancel to delete backup, Close to do nothing.',
|
|
||||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Close
|
|
||||||
)
|
|
||||||
|
|
||||||
if msg == QtWidgets.QMessageBox.Ok:
|
|
||||||
self.management.load_files([str(backup_by_date[-1][0])])
|
|
||||||
backup_by_date[-1][0].unlink()
|
|
||||||
elif msg == QtWidgets.QMessageBox.Cancel:
|
|
||||||
backup_by_date[-1][0].unlink()
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
|
@QtCore.pyqtSlot(name='on_actionCreate_starter_triggered')
|
||||||
def create_starter(self):
|
def create_starter(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user