crude updater added; write_state uses plain text, not pickle; disallow invalid characters; T246

This commit is contained in:
Dominik Demuth 2023-01-05 15:27:33 +01:00
parent 2ed390ccae
commit 04eb83a19d
11 changed files with 273 additions and 131 deletions

View File

@ -36,18 +36,21 @@ AppDir:
# for /usr/bin/env
- coreutils
- dash
- tango-icon-theme
- zsync
- hicolor-icon-theme
- libatlas3-base
- python3.9-minimal
- python3-pyqt5
- python3-numpy
#- python3-matplotlib
#- python-matplotlib-data
- python3-scipy
# - python3-matplotlib
# - python-matplotlib-data
- python3-bsddb3
- python3-h5py
- python3-pyqt5
- python3-pyqtgraph
- python3-tk
- python3-requests
- python3-urllib3
# - python3-tk
exclude:
- libavahi-client3
- libavahi-common-data

View File

@ -16,9 +16,10 @@ from nmreval.lib.logger import handle_exception
sys.excepthook = handle_exception
from gui_qt import App
from gui_qt.main.mainwindow import NMRMainWindow
app = App([])
app = App(['Team Rocket FTW!'])
from gui_qt.main.mainwindow import NMRMainWindow
mplQt = NMRMainWindow()
mplQt.show()

View File

@ -5,4 +5,4 @@ PyQt5
h5py
pyqtgraph
bsddb3
requests

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file '/autohome/dominik/nmreval/src/resources/_ui/basewindow.ui'
# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
# Created by: PyQt5 UI code generator 5.15.7
#
# 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.
@ -75,7 +75,7 @@ class Ui_BaseWindow(object):
self.horizontalLayout.addWidget(self.splitter)
BaseWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(BaseWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1386, 30))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1386, 20))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
@ -179,8 +179,6 @@ class Ui_BaseWindow(object):
self.toolBar_data.setObjectName("toolBar_data")
BaseWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar_data)
self.action_close = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("window-close")
self.action_close.setIcon(icon)
self.action_close.setObjectName("action_close")
self.actionExportGraphic = QtWidgets.QAction(BaseWindow)
self.actionExportGraphic.setObjectName("actionExportGraphic")
@ -191,20 +189,14 @@ class Ui_BaseWindow(object):
self.action_calc = QtWidgets.QAction(BaseWindow)
self.action_calc.setObjectName("action_calc")
self.action_delete_sets = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("edit-delete")
self.action_delete_sets.setIcon(icon)
self.action_delete_sets.setObjectName("action_delete_sets")
self.action_save_fit_parameter = QtWidgets.QAction(BaseWindow)
self.action_save_fit_parameter.setObjectName("action_save_fit_parameter")
self.action_sort_pts = QtWidgets.QAction(BaseWindow)
self.action_sort_pts.setObjectName("action_sort_pts")
self.action_reset = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("edit-clear")
self.action_reset.setIcon(icon)
self.action_reset.setObjectName("action_reset")
self.actionDocumentation = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("help-about")
self.actionDocumentation.setIcon(icon)
self.actionDocumentation.setObjectName("actionDocumentation")
self.action_FitWidget = QtWidgets.QAction(BaseWindow)
self.action_FitWidget.setObjectName("action_FitWidget")
@ -250,8 +242,6 @@ class Ui_BaseWindow(object):
self.actionConfiguration = QtWidgets.QAction(BaseWindow)
self.actionConfiguration.setObjectName("actionConfiguration")
self.actionRefresh = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("view-refresh")
self.actionRefresh.setIcon(icon)
self.actionRefresh.setObjectName("actionRefresh")
self.actionInterpolation = QtWidgets.QAction(BaseWindow)
self.actionInterpolation.setObjectName("actionInterpolation")
@ -278,8 +268,6 @@ class Ui_BaseWindow(object):
self.actionNew_window = QtWidgets.QAction(BaseWindow)
self.actionNew_window.setObjectName("actionNew_window")
self.actionDelete_window = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("edit-delete")
self.actionDelete_window.setIcon(icon)
self.actionDelete_window.setObjectName("actionDelete_window")
self.actionCascade_windows = QtWidgets.QAction(BaseWindow)
self.actionCascade_windows.setObjectName("actionCascade_windows")
@ -330,8 +318,6 @@ class Ui_BaseWindow(object):
self.actionChange_datatypes = QtWidgets.QAction(BaseWindow)
self.actionChange_datatypes.setObjectName("actionChange_datatypes")
self.actionPrint = QtWidgets.QAction(BaseWindow)
icon = QtGui.QIcon.fromTheme("document-print")
self.actionPrint.setIcon(icon)
self.actionPrint.setObjectName("actionPrint")
self.action_lm_fit = QtWidgets.QAction(BaseWindow)
self.action_lm_fit.setCheckable(True)
@ -494,7 +480,7 @@ class Ui_BaseWindow(object):
self.retranslateUi(BaseWindow)
self.tabWidget.setCurrentIndex(0)
self.action_close.triggered.connect(BaseWindow.close)
self.action_close.triggered.connect(BaseWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(BaseWindow)
def retranslateUi(self, BaseWindow):

View File

@ -8,15 +8,18 @@ from ..Qt import QtWidgets, QtCore
class FileDialog(QtWidgets.QFileDialog):
last_path = None
def __init__(self, directory=None, caption=None, filters='', parent=None):
def __init__(self, directory=None, caption=None, filter='', parent=None):
super().__init__(parent=parent)
self.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
self.setWindowTitle(caption)
if directory is not None:
if directory:
self.setDirectory(str(directory))
self.setNameFilters(filters.split(';;'))
elif self.last_path is not None:
self.setDirectory(str(FileDialog.last_path))
self.setNameFilters(filter.split(';;'))
file_tree = self.findChild(QtWidgets.QTreeView, 'treeView')
file_tree.setSortingEnabled(True)
@ -35,13 +38,30 @@ class FileDialog(QtWidgets.QFileDialog):
def save_file(self) -> pathlib.Path | None:
outfile = self.selectedFiles()
if outfile:
return pathlib.Path(outfile[0])
if self.is_valid(outfile[0]):
return pathlib.Path(outfile[0])
else:
_ = QtWidgets.QMessageBox.warning(self, 'Save file',
'Filename contains one or more invalid character: / * < > \\ | : "')
return
@staticmethod
def is_valid(filename: str):
bad_character = r'/*<>\|:"'
for c in bad_character:
if c in filename:
return False
return True
def close(self):
FileDialog.last_path = self.directory()
super().close()
class OpenFileDialog(FileDialog):
def __init__(self, directory=None, caption=None, filters='', parent=None):
super().__init__(directory=directory, caption=caption, filters=filters, parent=parent)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
@ -81,8 +101,8 @@ class OpenFileDialog(FileDialog):
class SaveDirectoryDialog(FileDialog):
def __init__(self, directory=None, filters='', parent=None):
super().__init__(directory=directory, filters=filters, parent=parent)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False)
self.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
@ -123,4 +143,3 @@ class SaveDirectoryDialog(FileDialog):
self.setWindowTitle('Save')
self.setNameFilters(['All files (*.*)', 'Session file (*.nmr)', 'Text file (*.dat)',
'HDF file (*.h5)', 'Grace files (*.agr)'])

View File

@ -37,7 +37,6 @@ class GraceMsgBox(QtWidgets.QDialog):
agr.parse(fname)
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(13, 13, 13, 13)
self.setLayout(layout)
label = QtWidgets.QLabel('%s already exists. Select one of the options or cancel:' % os.path.split(fname)[1])
@ -100,9 +99,9 @@ class GeneralConfiguration(QtWidgets.QDialog):
for key, value in parser.items(sec):
label = QtWidgets.QLabel(key.capitalize(), self)
layout2.addWidget(label, row, 0)
if (sec, key) in allowed_values:
if (sec, key) in ALLOWED_VALUE:
edit = QtWidgets.QComboBox(self)
edit.addItems(allowed_values[(sec, key)])
edit.addItems(ALLOWED_VALUE[(sec, key)])
edit.setCurrentIndex(edit.findText(value))
else:
edit = QtWidgets.QLineEdit(self)

View File

@ -1,8 +1,15 @@
import sys
from os import getenv, stat
import hashlib
import subprocess
from datetime import datetime
from contextlib import contextmanager
import requests
from numpy import linspace
from scipy.interpolate import interp1d
from ..Qt import QtGui, QtWidgets, QtCore
from gui_qt.Qt import QtGui, QtWidgets, QtCore
@contextmanager
@ -48,3 +55,162 @@ class RdBuCMap:
col = QtGui.QColor.fromRgb(*(float(self.spline[i](val)) for i in range(3)))
return col
class UpdateDialog(QtWidgets.QDialog):
host = 'mirror.infra.pkm'
bucket = 'nmreval'
version = 'NMReval-latest-x86_64'
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._url_zsync = f'http://{self.host}/{self.bucket}/{self.version}.AppImage.zsync'
self.process = QtCore.QProcess(self)
self.look_for_updates()
def _init_ui(self):
self.setWindowTitle('Updates')
layout = QtWidgets.QVBoxLayout()
self.label = QtWidgets.QLabel()
layout.addWidget(self.label)
layout.addSpacing(10)
self.dialog_button = QtWidgets.QDialogButtonBox()
self.dialog_button.accepted.connect(self.accept)
self.dialog_button.rejected.connect(self.reject)
layout.addWidget(self.dialog_button)
self.setLayout(layout)
def look_for_updates(self):
# Download zsync file of latest Appimage, look for SHA-1 hash and compare with hash of AppImage
m_time_zsync, checksum_zsync = self.get_zsync()
m_time_file, checksum_file = self.get_appimage_info()
if m_time_zsync is None:
label_text = '<p>Retrieval of version information failed.</p>' \
'<p>Please try later (or complain to people that it does not work).</p>'
dialog_bttns = QtWidgets.QDialogButtonBox.Close
else:
label_text = f'<p>Found most recent update: {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 downlaod 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>'
if not ((checksum_file is not None) and (checksum_zsync is not None)):
label_text += 'Could not determine if this version is newer, please update manually (if necessary).'
dialog_bttns = QtWidgets.QDialogButtonBox.Close
elif checksum_file != checksum_zsync:
label_text += f'<p>Newer version available. <b>Update?</b></p>'
dialog_bttns = QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel
else:
label_text += f'<p>Version is already the newest</p>'
dialog_bttns = QtWidgets.QDialogButtonBox.Close
self.label.setText(label_text)
self.dialog_button.setStandardButtons(dialog_bttns)
def get_zsync(self):
m_time_zsync = None
checksum_zsync = None
zsync_file = None
try:
response = requests.get(self._url_zsync)
if response.status_code == requests.codes['\o/']:
zsync_file = response.content
except Exception as e:
pass
if zsync_file is not None:
for line in zsync_file.split(b'\n'):
try:
kw, val = line.split(b': ')
if kw == b'MTime':
m_time_zsync = datetime.strptime(str(val, encoding='utf-8'), '%a, %d %b %Y %H:%M:%S %z').astimezone(None)
elif kw == b'SHA-1':
checksum_zsync = str(val, encoding='utf-8')
except ValueError:
# stop when empty line is reached
break
return m_time_zsync, checksum_zsync
def get_appimage_info(self):
if self._appfile is None:
return None, None
stat_mtime = stat(self._appfile).st_mtime
m_time_file = datetime.fromtimestamp(stat_mtime).replace(microsecond=0)
with open(self._appfile, 'rb') as f:
checksum_file = hashlib.sha1(f.read()).hexdigest()
return m_time_file, checksum_file
def update_appimage(self):
if self._appfile is None:
args = [self._url_zsync]
else:
args = ['-i', self._appfile, self._url_zsync]
self.process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput)
self.process.readyReadStandardError.connect(self.onReadyReadStandardOutput)
self.process.start('zsync', args)
if not self.process.waitForFinished():
return False
return True
def onReadyReadStandardOutput(self):
result = self.process.readAllStandardOutput().data().decode()
self.label.setText(result)
def accept(self):
if self.update_appimage():
_ = QtWidgets.QMessageBox.information(self, 'Updates', 'Download finished. Execute AppImage for new version.')
super().accept()
def open_bug_report():
form_entries = {
'description': 'Please state the nature of the medical emergency.',
'title': 'Everything is awesome?',
'assign[0]': 'dominik',
'subscribers[0]': 'dominik',
'tag': 'nmreval',
'priority': 'normal',
'status': 'open',
}
full_url = 'https://chaos3.fkp.physik.tu-darmstadt.de/maniphest/task/edit/?'
for k, v in form_entries.items():
full_url += f'{k}={v}&'
full_url.replace(' ', '+')
import webbrowser
webbrowser.open(full_url)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = UpdateDialog()
w.show()
sys.exit(app.exec())

View File

@ -5,10 +5,9 @@ import re
from pathlib import Path
from numpy import geomspace, linspace
from pyqtgraph import ViewBox, PlotDataItem
from pyqtgraph import ViewBox
from nmreval.configs import *
from nmreval.lib.utils import open_bug_report
from .management import UpperManagement
from ..Qt import QtCore, QtGui, QtPrintSupport, QtWidgets
@ -28,6 +27,7 @@ from ..math.smooth import QSmooth
from ..nmr.coupling_calc import QCoupCalcDialog
from ..nmr.t1_from_tau import QRelaxCalc
from .._py.basewindow import Ui_BaseWindow
from ..lib.utils import UpdateDialog, open_bug_report
class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@ -218,13 +218,13 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@QtCore.pyqtSlot(name='on_action_open_triggered')
def open(self):
filedialog = OpenFileDialog(directory=self.path, caption='Open files',
filters='All files (*.*);;'
'Program session (*.nmr);;'
'HDF files (*.h5);;'
'Text files (*.txt *.dat);;'
'Novocontrol Alpha (*.EPS);;'
'TecMag files (*.tnt);;'
'Grace files (*.agr)')
filter='All files (*.*);;'
'Program session (*.nmr);;'
'HDF files (*.h5);;'
'Text files (*.txt *.dat);;'
'Novocontrol Alpha (*.EPS);;'
'TecMag files (*.tnt);;'
'Grace files (*.agr)')
filedialog.set_graphs(self.management.graphs.list())
@ -250,9 +250,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@QtCore.pyqtSlot(name='on_actionExportData_triggered')
@QtCore.pyqtSlot(name='on_actionSave_triggered')
def save(self):
save_dialog = SaveDirectoryDialog(
directory=str(self.path), parent=self,
)
save_dialog = SaveDirectoryDialog(directory=str(self.path), parent=self)
mode = save_dialog.exec()
if mode == QtWidgets.QDialog.Accepted:
@ -274,12 +272,14 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@QtCore.pyqtSlot()
@QtCore.pyqtSlot(list)
def save_fit_parameter(self, fit_sets: list[str] = None):
fname, _ = QtWidgets.QFileDialog.getSaveFileName(self, 'Save fit parameter', directory=str(self.path),
filter='All files(*, *);;Text files(*.dat *.txt)',
options=QtWidgets.QFileDialog.DontConfirmOverwrite)
save_dialog = SaveDirectoryDialog(parent=self, caption='Save fit parameter', directory=str(self.path),
filter='All files(*, *);;Text files(*.dat *.txt)')
if fname:
self.management.save_fit_parameter(fname, fit_sets=fit_sets)
mode = save_dialog.exec()
if mode == QtWidgets.QDialog.Accepted:
savefile = save_dialog.save_file()
if savefile:
self.management.save_fit_parameter(savefile, fit_sets=fit_sets)
@QtCore.pyqtSlot(name='on_actionExportGraphic_triggered')
def export_graphic(self):
@ -988,14 +988,19 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
dialog.show()
def close(self):
write_state({'recent_path': str(self.path)})
write_state({'History': {'recent path': str(self.path)}})
super().close()
def read_state(self):
opts = read_state()
self.path = pathlib.Path(opts.get('recent_path', Path.home()))
self.path = pathlib.Path(opts['History'].get('recent path', Path.home()))
@QtCore.pyqtSlot(name='on_actionBugs_triggered')
def report_bug(self):
open_bug_report()
@QtCore.pyqtSlot(name='on_actionUpdate_triggered')
def look_for_update(self):
w = UpdateDialog(parent=self)
w.show()

View File

@ -5,7 +5,7 @@ from shutil import copyfile
from importlib.resources import path as resource_path
__all__ = ['config_paths', 'check_for_config', 'read_configuration', 'write_configuration', 'allowed_values', 'write_state', 'read_state']
__all__ = ['config_paths', 'check_for_config', 'read_configuration', 'write_configuration', 'ALLOWED_VALUE', 'write_state', 'read_state']
def check_for_config(make=True):
@ -18,14 +18,13 @@ def check_for_config(make=True):
cwd = pathlib.Path(__file__).parent
copyfile(cwd / 'models' / 'usermodels.py', conf_path / 'usermodels.py')
with resource_path('resources', 'Default.agr') as fp:
copyfile(fp, conf_path / 'Default.agr')
copyfile(fp, conf_path / 'Default.agr')
else:
raise e
def config_paths() -> pathlib.Path:
# TODO adjust for different OS
searchpaths = ['~/.config/nmreval', '~/.auswerten', '/usr/share/nmreval']
conf_path = None
@ -46,8 +45,6 @@ def read_configuration() -> configparser.ConfigParser:
config_file = config_paths() / 'nmreval.cfg'
if not config_file.exists():
write_configuration({'GUI': {'theme': 'normal', 'color': 'light'}})
# raise FileNotFoundError('Configuration file not found')
#
except FileNotFoundError as e:
raise e
@ -67,24 +64,38 @@ def write_configuration(opts: dict):
parser.write(f)
allowed_values = {
ALLOWED_VALUE = {
('GUI', 'theme'): ['normal', 'pokemon'],
('GUI', 'color'): ['light', 'dark'],
}
def write_state(opts: dict):
def write_state(new_opts: dict):
config_file = config_paths() / 'guistate.ini'
old_opts = read_state()
old_opts.update(opts)
with config_file.open('wb') as f:
pickle.dump(old_opts, f)
opts = read_state()
opts.update(new_opts)
parser = configparser.ConfigParser()
parser.read_dict(opts)
with config_file.open('w') as f:
parser.write(f)
def read_state() -> dict:
config_file = config_paths() / 'guistate.ini'
if not config_file.exists():
return {}
return {'History': {'recent path': pathlib.Path.home()}}
with config_file.open('rb') as f:
return pickle.load(f)
try:
opts = pickle.load(f)
opts['recent path'] = opts.get('recent_path', pathlib.Path.home())
return {'History': opts}
except pickle.UnpicklingError:
parser = configparser.ConfigParser()
parser.read(config_file)
return parser

View File

@ -5,34 +5,14 @@ from ..math.mittagleffler import mlf
ArrayLike = TypeVar('ArrayLike')
def valid_function(expr: str, extra_namespace: dict = None):
local = {'mlf': mlf}
if extra_namespace is not None:
local.update(extra_namespace)
try:
return ne.evaluate(expr, {}, local), True
except:
return None, False
def open_bug_report():
form_entries = {
'description': 'Please state the nature of the medical emergency.',
'title': 'Everything is awesome?',
'assign[0]': 'dominik',
'subscribers[0]': 'dominik',
'tag': 'nmreval',
'priority': 'normal',
'status': 'open',
}
full_url = 'https://chaos3.fkp.physik.tu-darmstadt.de/maniphest/task/edit/?'
for k, v in form_entries.items():
full_url += f'{k}={v}&'
full_url.replace(' ', '+')
import webbrowser
webbrowser.open(full_url)
#
# def valid_function(expr: str, extra_namespace: dict = None):
#
# local = {'mlf': mlf}
# if extra_namespace is not None:
# local.update(extra_namespace)
#
# try:
# return ne.evaluate(expr, {}, local), True
# except:
# return None, False

View File

@ -136,7 +136,7 @@
<x>0</x>
<y>0</y>
<width>1386</width>
<height>30</height>
<height>20</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -501,10 +501,6 @@
<addaction name="action_sort_pts"/>
</widget>
<action name="action_close">
<property name="icon">
<iconset theme="window-close">
<normaloff>../../../../../.designer/backup</normaloff>../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>&amp;Quit</string>
</property>
@ -542,10 +538,6 @@
</property>
</action>
<action name="action_delete_sets">
<property name="icon">
<iconset theme="edit-delete">
<normaloff>../../../../../.designer/backup</normaloff>../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>&amp;Delete Set</string>
</property>
@ -564,10 +556,6 @@
</property>
</action>
<action name="action_reset">
<property name="icon">
<iconset theme="edit-clear">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>&amp;Reset</string>
</property>
@ -576,10 +564,6 @@
</property>
</action>
<action name="actionDocumentation">
<property name="icon">
<iconset theme="help-about">
<normaloff>../../../../../../../../.designer/backup</normaloff>../../../../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>&amp;Documentation</string>
</property>
@ -705,10 +689,6 @@
</property>
</action>
<action name="actionRefresh">
<property name="icon">
<iconset theme="view-refresh">
<normaloff>../../../../../.designer/backup</normaloff>../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>Refresh</string>
</property>
@ -779,10 +759,6 @@
</property>
</action>
<action name="actionDelete_window">
<property name="icon">
<iconset theme="edit-delete">
<normaloff>../../../../../.designer/backup</normaloff>../../../../../.designer/backup</iconset>
</property>
<property name="text">
<string>Delete graph</string>
</property>
@ -914,10 +890,6 @@
</property>
</action>
<action name="actionPrint">
<property name="icon">
<iconset theme="document-print">
<normaloff>../../../.designer/backup</normaloff>../../../.designer/backup</iconset>
</property>
<property name="text">
<string>Print...</string>
</property>