BUGFIX: VFT;

change to src layout
This commit is contained in:
dominik
2022-10-20 17:23:15 +02:00
parent 89ce4bab9f
commit 8d148b639b
445 changed files with 1387 additions and 1920 deletions

View File

@ -0,0 +1,80 @@
import sys
if sys.version_info < (3, 7):
HAS_IMPORTLIB_RESOURCE = False
from pkg_resources import resource_filename
else:
HAS_IMPORTLIB_RESOURCE = True
from importlib.resources import path
from ..Qt import QtGui, QtWidgets
# def get_path_importlib(package, resource):
# return path(package, resource)
#
#
# def _get_path_pkg(package, resource):
# return resource_filename(package, resource)
#
#
# if HAS_IMPORTLIB_RESOURCE:
# get_path = get_path_importlib
# else:
# get_path = _get_path_pkg
def make_action_icons(widget):
global HAS_IMPORTLIB_RESOURCE
icon_type = QtWidgets.QApplication.instance().theme
from json import loads
if HAS_IMPORTLIB_RESOURCE:
with path('resources.icons', 'icons.json') as fp:
with fp.open('r') as f:
icon_list = loads(f.read())
for ac, img in icon_list[widget.objectName()].items():
dirname = 'resources.icons.%s_light' % icon_type
with path(dirname, img+'.png') as imgpath:
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(str(imgpath)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
getattr(widget, ac).setIcon(icon)
else:
with open(resource_filename('resources.icons', 'icons.json'), 'r') as f:
icon_list = loads(f.read())
for ac, img in icon_list[widget.objectName()].items():
dirname = 'resources.icons.%s_light' % icon_type
imgpath = resource_filename(dirname, img+'.png')
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(str(imgpath)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
getattr(widget, ac).setIcon(icon)
def get_icon(icon_name):
try:
icon_type = QtWidgets.QApplication.instance().theme
except AttributeError:
icon_type = 'normal'
global HAS_IMPORTLIB_RESOURCE
if icon_name != 'logo':
dirname = f'resources.icons.{icon_type}_light'
else:
dirname = 'resources.icons'
if HAS_IMPORTLIB_RESOURCE:
with path(dirname, icon_name+'.png') as imgpath:
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(str(imgpath)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
return icon
else:
imgpath = resource_filename(dirname, icon_name+'.png')
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(imgpath), QtGui.QIcon.Normal, QtGui.QIcon.Off)
return icon

View File

@ -0,0 +1,262 @@
# CodeEditor based on QT example, Python syntax highlighter found on Python site
from ..Qt import QtGui, QtCore, QtWidgets
def _make_textformats(color, style=''):
"""Return a QTextCharFormat with the given attributes.
"""
_color = QtGui.QColor()
_color.setNamedColor(color)
_format = QtGui.QTextCharFormat()
_format.setForeground(_color)
if 'bold' in style:
_format.setFontWeight(QtGui.QFont.Bold)
if 'italic' in style:
_format.setFontItalic(True)
return _format
# Syntax styles that can be shared by all languages
STYLES = {
'keyword': _make_textformats('blue'),
'operator': _make_textformats('black'),
'brace': _make_textformats('black'),
'defclass': _make_textformats('black', 'bold'),
'string': _make_textformats('darkGreen'),
'comment': _make_textformats('gray', 'italic'),
'self': _make_textformats('brown', 'italic'),
'property': _make_textformats('brown'),
'numbers': _make_textformats('darkRed'),
}
class PythonHighlighter(QtGui.QSyntaxHighlighter):
"""
Syntax highlighter for the Python language.
"""
# Python keywords
keywords = [
'and', 'assert', 'break', 'class', 'continue', 'def',
'del', 'elif', 'else', 'except', 'exec', 'finally',
'for', 'from', 'global', 'if', 'import', 'in',
'is', 'lambda', 'not', 'or', 'pass', 'print',
'raise', 'return', 'try', 'while', 'yield',
'None', 'True', 'False', 'object'
]
def __init__(self, document):
super().__init__(document)
# Multi-line strings (expression, flag, style)
# FIXME: The triple-quotes in these two lines will mess up the
# syntax highlighting from this point onward
self.tri_single = (QtCore.QRegExp("r'{3}"), 1, STYLES['string'])
self.tri_double = (QtCore.QRegExp('r?"{3}'), 2, STYLES['string'])
rules = []
# Keyword, operator, and brace rules
rules += [(rf'\b{w}\b', 0, STYLES['keyword']) for w in PythonHighlighter.keywords]
# Other rules
rules += [
# 'self'
(r'\bself\b', 0, STYLES['self']),
# 'def' followed by an identifier
(r'\bdef\b\s*(\w+)', 1, STYLES['defclass']),
# 'class' followed by an identifier
(r'\bclass\b\s*(\w+)', 1, STYLES['defclass']),
# @ followed by a word
(r'\s*@(\w+)\s*', 0, STYLES['property']),
# Numeric literals
(r'\b[+-]?\d+[lL]?\b', 0, STYLES['numbers']),
(r'\b[+-]?0[xX][\dA-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
(r'\b[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b', 0, STYLES['numbers']),
# Double-quoted string, possibly containing escape sequences
(r'[rf]?"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
# Single-quoted string, possibly containing escape sequences
(r"[rf]?'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),
# From '#' until a newline
(r'#[^\n]*', 0, STYLES['comment']),
]
# Build a QRegExp for each pattern
self.rules = [(QtCore.QRegExp(pat), index, fmt) for (pat, index, fmt) in rules]
def highlightBlock(self, text):
"""
Apply syntax highlighting to the given block of text.
"""
# Do other syntax formatting
for expression, nth, rule in self.rules:
index = expression.indexIn(text, 0)
while index >= 0:
# We actually want the index of the nth match
index = expression.pos(nth)
length = len(expression.cap(nth))
self.setFormat(index, length, rule)
index = expression.indexIn(text, index + length)
self.setCurrentBlockState(0)
# Do multi-line strings
in_multiline = self.match_multiline(text, *self.tri_single)
if not in_multiline:
in_multiline = self.match_multiline(text, *self.tri_double)
def match_multiline(self, text, delimiter, in_state, style):
"""
Highlighting of multi-line strings. ``delimiter`` should be a
``QRegExp`` for triple-single-quotes or triple-double-quotes, and
``in_state`` should be a unique integer to represent the corresponding
state changes when inside those strings. Returns True if we're still
inside a multi-line string when this function is finished.
"""
# If inside triple-single quotes, start at 0
if self.previousBlockState() == in_state:
start = 0
add = 0
# Otherwise, look for the delimiter on this line
else:
start = delimiter.indexIn(text)
# Move past this match
add = delimiter.matchedLength()
# As long as there's a delimiter match on this line...
while start >= 0:
# Look for the ending delimiter
end = delimiter.indexIn(text, start + add)
# Ending delimiter on this line?
if end >= add:
length = end - start + add + delimiter.matchedLength()
self.setCurrentBlockState(0)
# No; multi-line string
else:
self.setCurrentBlockState(in_state)
try:
length = text.length() - start + add
except AttributeError:
length = len(text) - start + add
# Apply formatting
self.setFormat(start, length, style)
# Look for the next match
start = delimiter.indexIn(text, start + length)
# Return True if still inside a multi-line string, False otherwise
if self.currentBlockState() == in_state:
return True
else:
return False
class LineNumbers(QtWidgets.QWidget):
def __init__(self, editor):
super().__init__(editor)
self.editor = editor
def sizeHint(self):
return QtCore.QSize(self.editor.width_linenumber, 0)
def paintEvent(self, event):
self.editor.paintevent_linenumber(event)
class CodeEditor(QtWidgets.QPlainTextEdit):
# more or less a direct translation of the Qt example
def __init__(self, parent):
super().__init__(parent)
self.current_linenumber = LineNumbers(self)
self.blockCountChanged.connect(self.update_width_linenumber)
self.updateRequest.connect(self.update_current_area)
self.cursorPositionChanged.connect(self.highlight_current_line)
self.update_width_linenumber(0)
self.highlight = PythonHighlighter(self.document())
def keyPressEvent(self, evt):
if evt.key() == QtCore.Qt.Key_Tab:
# use spaces instead of tab
self.insertPlainText(' '*4)
elif evt.key() == QtCore.Qt.Key_Insert:
self.setOverwriteMode(not self.overwriteMode())
else:
super().keyPressEvent(evt)
@property
def width_linenumber(self):
digits = 1
count = max(1, self.blockCount())
while count >= 10:
count /= 10
digits += 1
space = 6 + self.fontMetrics().width('9') * digits
return space
def update_width_linenumber(self, _):
self.setViewportMargins(self.width_linenumber, 0, 0, 0)
def update_current_area(self, rect, dy):
if dy:
self.current_linenumber.scroll(0, dy)
else:
self.current_linenumber.update(0, rect.y(), self.current_linenumber.width(), rect.height())
if rect.contains(self.viewport().rect()):
self.update_width_linenumber(0)
def resizeEvent(self, evt):
super().resizeEvent(evt)
cr = self.contentsRect()
self.current_linenumber.setGeometry(QtCore.QRect(cr.left(), cr.top(), self.width_linenumber, cr.height()))
def paintevent_linenumber(self, evt):
painter = QtGui.QPainter(self.current_linenumber)
painter.fillRect(evt.rect(), QtCore.Qt.lightGray)
block = self.firstVisibleBlock()
block_number = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
# Just to make sure I use the right font
height = self.fontMetrics().height()
while block.isValid() and (top <= evt.rect().bottom()):
if block.isVisible() and (bottom >= evt.rect().top()):
number = str(block_number + 1)
painter.setPen(QtCore.Qt.black)
painter.drawText(0, top, self.current_linenumber.width() - 3, height,
QtCore.Qt.AlignRight, number)
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
block_number += 1
def highlight_current_line(self):
extra_selections = []
if not self.isReadOnly():
selection = QtWidgets.QTextEdit.ExtraSelection()
line_color = QtGui.QColor(QtCore.Qt.yellow).lighter(180)
selection.format.setBackground(line_color)
selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extra_selections.append(selection)
self.setExtraSelections(extra_selections)

View File

@ -0,0 +1,80 @@
from nmreval.configs import config_paths
from nmreval.lib.colors import Colors, available_cycles
from ..Qt import QtWidgets, QtCore, QtGui
from .._py.color_palette import Ui_Dialog
class ColorDialog(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._palettes = available_cycles
self.palette_combobox.addItems(self._palettes.keys())
self.colorlist.installEventFilter(self)
def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent) -> bool:
if evt.type() == evt.KeyPress:
if evt.key() == QtCore.Qt.Key_Delete:
self.colorlist.takeItem(self.colorlist.currentRow())
return True
return super().eventFilter(src, evt)
@QtCore.pyqtSlot(name='on_add_palette_button_pressed')
@QtCore.pyqtSlot(name='on_append_palette_button_pressed')
def set_palette(self, clear: bool = False):
if self.sender() == self.add_palette_button or clear:
self.colorlist.clear()
for color in self._palettes[self.palette_combobox.currentText()]:
item = QtWidgets.QListWidgetItem(color.name)
item.setData(QtCore.Qt.DecorationRole, QtGui.QColor.fromRgbF(*color.rgb(normed=True)))
self.colorlist.addItem(item)
@QtCore.pyqtSlot(name='on_add_color_button_pressed')
def add_color(self):
color = self.color_combobox.value
item = QtWidgets.QListWidgetItem(color.name)
item.setData(QtCore.Qt.DecorationRole, QtGui.QColor.fromRgbF(*color.rgb(normed=True)))
self.colorlist.addItem(item)
@QtCore.pyqtSlot(name='on_save_button_pressed')
def save_new_palette(self):
if not self.colorlist.count():
_ = QtWidgets.QMessageBox.warning(self, 'Error', 'No colors to save.')
return
if not self.new_name_edit.text():
_ = QtWidgets.QMessageBox.warning(self, 'Error', 'New list has no name.')
return
color_name = self.new_name_edit.text() # type: str
if color_name in self._palettes:
_ = QtWidgets.QMessageBox.warning(self, 'Error', 'Name is already used.')
return
color_file = config_paths() / 'colors.cfg'
new_palette = []
with color_file.open('a') as f:
f.write('#-- %s\n' % color_name)
for i in range(self.colorlist.count()):
item = self.colorlist.item(i)
r, g, b = item.data(QtCore.Qt.DecorationRole).getRgbF()[:3]
new_palette.append(Colors.from_rgb(r, g, b, normed=True))
f.write(f'{r*255:.1f}, {g*255:.1f}, {b*255:.1f}\n')
f.write('\n')
self._palettes[color_name] = new_palette
self.palette_combobox.addItem(color_name)
def load_palette(self, name: str):
idx = self.palette_combobox.findText(name)
if idx != -1:
self.palette_combobox.setCurrentIndex(idx)
self.set_palette(clear=True)

View File

@ -0,0 +1,156 @@
import os
from nmreval.configs import *
from nmreval.io.graceeditor import GraceEditor
from ..Qt import QtWidgets, QtGui
from .._py.agroptiondialog import Ui_Dialog
class QGraceSettings(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._default_path = config_paths().joinpath('Default.agr')
self._default = GraceEditor(self._default_path)
self.setup_ui()
def setup_ui(self):
page = self._default.size
self.widthDoubleSpinBox.setValue(page[0])
self.heightDoubleSpinBox.setValue(page[1])
view = self._default.get_property(0, 'view')
self.leftMarginDoubleSpinBox.setValue(self._default.convert(view[0], direction='abs'))
self.bottomMarginDoubleSpinBox.setValue(self._default.convert(view[1], direction='abs'))
self.rightMarginDoubleSpinBox.setValue(page[0]-self._default.convert(view[2], direction='abs'))
self.topMarginDoubleSpinBox.setValue(page[1]-self._default.convert(view[3], direction='abs'))
class GraceMsgBox(QtWidgets.QDialog):
def __init__(self, fname, parent=None):
super(GraceMsgBox, self).__init__(parent=parent)
agr = GraceEditor()
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])
layout.addWidget(label, 1, 1, 1, 2)
self.button_grp = QtWidgets.QButtonGroup(self)
self.overwrite_radiobutton = QtWidgets.QRadioButton('Overwrite file', parent=self)
self.overwrite_radiobutton.setChecked(True)
self.button_grp.addButton(self.overwrite_radiobutton, id=0)
layout.addWidget(self.overwrite_radiobutton, 2, 1, 1, 2)
self.new_graph_button = QtWidgets.QRadioButton('Create new graph', parent=self)
self.button_grp.addButton(self.new_graph_button, id=1)
layout.addWidget(self.new_graph_button, 3, 1, 1, 2)
self.addgraph_button = QtWidgets.QRadioButton('Add sets to graph', parent=self)
self.button_grp.addButton(self.addgraph_button, id=3)
layout.addWidget(self.addgraph_button, 4, 1, 1, 1)
self.graph_combobox = QtWidgets.QComboBox(self)
self.graph_combobox.addItems(['G' + str(g.idx) for g in agr.graphs])
layout.addWidget(self.graph_combobox, 4, 2, 1, 1)
self.buttonbox = QtWidgets.QDialogButtonBox(self)
self.buttonbox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
layout.addWidget(self.buttonbox, 5, 1, 1, 2)
self.buttonbox.rejected.connect(self.reject)
self.buttonbox.accepted.connect(self.accept)
def accept(self) -> None:
super().accept()
self.setResult(self.button_grp.checkedId())
if self.button_grp.checkedId() == 3:
self.setResult(int(self.graph_combobox.currentText()[1:]) + 2)
def reject(self) -> None:
super().reject()
self.setResult(-1)
class GeneralConfiguration(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(3, 3, 3, 3)
intro = QtWidgets.QLabel('Changes become active after restart.')
layout.addWidget(intro)
parser = read_configuration()
for sec in parser.sections():
group = QtWidgets.QGroupBox(sec, self)
layout2 = QtWidgets.QGridLayout()
layout2.setContentsMargins(3, 3, 3, 3)
row = 0
for key, value in parser.items(sec):
label = QtWidgets.QLabel(key.capitalize(), self)
layout2.addWidget(label, row, 0)
if (sec, key) in allowed_values:
edit = QtWidgets.QComboBox(self)
edit.addItems(allowed_values[(sec, key)])
edit.setCurrentIndex(edit.findText(value))
else:
edit = QtWidgets.QLineEdit(self)
edit.setText(value)
try:
_ = float(value)
edit.setValidator(QtGui.QDoubleValidator())
except ValueError:
pass
layout2.addWidget(edit, row, 1)
row += 1
group.setLayout(layout2)
layout.addWidget(group)
self.buttonbox = QtWidgets.QDialogButtonBox(self)
self.buttonbox.setStandardButtons(self.buttonbox.Ok|self.buttonbox.Cancel)
self.buttonbox.rejected.connect(self.close)
self.buttonbox.accepted.connect(self.accept)
layout.addWidget(self.buttonbox)
self.setLayout(layout)
def accept(self):
options = {}
for section in self.findChildren(QtWidgets.QGroupBox):
args = {}
layout = section.layout()
for row in range(layout.rowCount()):
key = layout.itemAtPosition(row, 0).widget().text()
value_widget = layout.itemAtPosition(row, 1).widget()
if isinstance(value_widget, QtWidgets.QComboBox):
value = value_widget.currentText()
elif isinstance(value_widget, QtWidgets.QLineEdit):
value = value_widget.text()
elif isinstance(value_widget, QtWidgets.QDoubleSpinBox):
value = value_widget.text()
else:
raise TypeError('Config key %s has unknown type %s' % (key, repr(value_widget)))
args[key] = value
options[section.title()] = args
write_configuration(options)
super().accept()

View File

@ -0,0 +1,55 @@
from functools import wraps
from ..Qt import QtWidgets
def update_indexes(func):
@wraps(func)
def wrapped(self, *args, **kwargs):
ret_val = func(self, *args, **kwargs)
self.blockSignals(True)
iterator = QtWidgets.QTreeWidgetItemIterator(self)
i = j = 0
while iterator.value():
item = iterator.value()
if item is not None:
if item.parent() is None:
item.setText(1, 'g[{}]'.format(i))
i += 1
j = 0
else:
item.setText(1, '.s[{}]'.format(j))
j += 1
iterator += 1
self.blockSignals(False)
return ret_val
return wrapped
def plot_update(func):
@wraps(func)
def wrapped(self, *args, **kwargs):
ret_val = func(self, *args, **kwargs)
m = self._data.mask
_x = self._data.x
_y = self._data.y
self.plot_real.setData(x=_x[m], y=_y.real[m], name=self._data.name)
if self.plot_imag is not None:
self.plot_imag.setData(x=_x[m], y=_y.imag[m], name=self._data.name)
if self.plot_error is not None:
_y_err = self._data.y_err
self.plot_error.setData(x=_x[m], y=_y.real[m], top=_y_err[m], bottom=_y_err[m])
self.dataChanged.emit(self.id)
return ret_val
return wrapped

235
src/gui_qt/lib/delegates.py Normal file
View File

@ -0,0 +1,235 @@
import re
from nmreval.lib.colors import BaseColor, Colors
from nmreval.lib.lines import LineStyle
from nmreval.lib.symbols import SymbolStyle, make_symbol_pixmap
from ..Qt import QtWidgets, QtGui, QtCore
class PropertyDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter: QtGui.QPainter, options: QtWidgets.QStyleOptionViewItem, idx: QtCore.QModelIndex):
r = idx.data(QtCore.Qt.DisplayRole)
if r is not None:
if isinstance(r, BaseColor):
painter.save()
c = QtGui.QColor(*r.value)
rect = options.rect
painter.fillRect(rect, QtGui.QBrush(c))
painter.restore()
elif isinstance(r, LineStyle):
painter.save()
pen = QtGui.QPen()
pal = QtGui.QGuiApplication.palette()
pen.setColor(pal.color(QtGui.QPalette.Text))
pen.setWidth(2)
pen.setStyle(r.value)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
rect = options.rect
rect.adjust(5, 0, -5, 0)
mid = (rect.bottom()+rect.top()) / 2
painter.drawLine(rect.left(), mid, rect.right(), mid)
painter.restore()
elif isinstance(r, SymbolStyle):
painter.save()
pen = QtGui.QPen()
pal = QtGui.QGuiApplication.palette()
pen.setColor(pal.color(QtGui.QPalette.Text))
painter.setPen(pen)
pm = make_symbol_pixmap(r)
painter.drawPixmap(options.rect.topLeft()+QtCore.QPoint(3, (options.rect.height()-pm.height())/2), pm)
style = QtWidgets.QApplication.style()
text_rect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options, None)
text_rect.adjust(5+pm.width(), 0, 0, 0)
painter.drawText(text_rect, options.displayAlignment, r.name)
painter.restore()
else:
super().paint(painter, options, idx)
def createEditor(self, parent: QtWidgets.QWidget,
options: QtWidgets.QStyleOptionViewItem, idx: QtCore.QModelIndex) -> QtWidgets.QWidget:
data = idx.data()
if isinstance(data, BaseColor):
editor = ColorListEditor(parent)
elif isinstance(data, LineStyle):
editor = LineStyleEditor(parent)
elif isinstance(data, SymbolStyle):
editor = SymbolStyleEditor(parent)
elif isinstance(data, float):
editor = SpinBoxEditor(parent)
else:
editor = super().createEditor(parent, options, idx)
return editor
def setEditorData(self, editor: QtWidgets.QWidget, idx: QtCore.QModelIndex):
data = idx.data()
if isinstance(data, (BaseColor, LineStyle, SymbolStyle)):
editor.value = data
else:
super().setEditorData(editor, idx)
def setModelData(self, editor: QtWidgets.QWidget, model: QtCore.QAbstractItemModel, idx: QtCore.QModelIndex):
data = idx.data()
if isinstance(data, (BaseColor, LineStyle, SymbolStyle)):
model.setData(idx, editor.value)
else:
super().setModelData(editor, model, idx)
class ColorListEditor(QtWidgets.QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.populateList()
@QtCore.pyqtProperty(BaseColor, user=True)
def value(self):
return self.itemData(self.currentIndex())
@value.setter
def value(self, val):
for i in range(self.count()):
if val.name == self.itemData(i).name:
self.setCurrentIndex(i)
break
def populateList(self):
for i, colorName in enumerate(Colors):
color = QtGui.QColor(*colorName.value)
self.insertItem(i, colorName.name)
self.setItemData(i, colorName)
px = QtGui.QPixmap(self.iconSize())
px.fill(color)
self.setItemData(i, QtGui.QIcon(px), QtCore.Qt.DecorationRole)
class SpinBoxEditor(QtWidgets.QDoubleSpinBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setValue(1.0)
self.setSingleStep(0.1)
self.setDecimals(1)
@QtCore.pyqtProperty(float, user=True)
def value(self):
return super().value()
@value.setter
def value(self, val):
super().setValue(val)
class LineStyleEditor(QtWidgets.QComboBox):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setItemDelegate(LineStyleDelegate())
self.populate()
@QtCore.pyqtProperty(int, user=True)
def value(self):
return self.itemData(self.currentIndex())
@value.setter
def value(self, val):
for i in range(self.count()):
if val == self.itemData(i):
self.setCurrentIndex(i)
break
def populate(self):
for i, style in enumerate(LineStyle):
self.insertItem(i, re.sub(r'([A-Z])', r' \g<1>', style.name))
self.setItemData(i, style)
def paintEvent(self, evt: QtGui.QPaintEvent):
if self.currentData() is not None:
painter = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
pen = QtGui.QPen()
pal = QtGui.QGuiApplication.palette()
pen.setColor(pal.color(QtGui.QPalette.Text))
pen.setWidth(2)
pen.setStyle(self.currentData().value)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
rect = painter.style().subControlRect(QtWidgets.QStyle.CC_ComboBox,
opt, QtWidgets.QStyle.SC_ComboBoxEditField, None)
rect.adjust(+10, 0, -10, 0)
mid = (rect.bottom() + rect.top()) / 2
painter.drawLine(rect.left(), mid, rect.right(), mid)
painter.end()
else:
super().paintEvent(evt)
class LineStyleDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
data = index.data(QtCore.Qt.UserRole)
if data is not None:
pen = QtGui.QPen()
pal = QtGui.QGuiApplication.palette()
pen.setColor(pal.color(QtGui.QPalette.Text))
pen.setWidth(2)
pen.setStyle(data.value)
pen.setCapStyle(QtCore.Qt.RoundCap)
painter.setPen(pen)
rect = option.rect
rect.adjust(+10, 0, -10, 0)
mid = (rect.bottom()+rect.top()) / 2
painter.drawLine(rect.left(), mid, rect.right(), mid)
else:
QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
class SymbolStyleEditor(QtWidgets.QComboBox):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.populate()
@QtCore.pyqtProperty(SymbolStyle, user=True)
def value(self):
return self.itemData(self.currentIndex())
@value.setter
def value(self, val):
for i in range(self.count()):
if val == self.itemData(i):
self.setCurrentIndex(i)
break
def populate(self):
for i, s in enumerate(SymbolStyle):
self.insertItem(i, re.sub(r'([A-Z])', r' \g<1>', s.name))
self.setItemData(i, s)
self.setItemData(i, make_symbol_pixmap(s), QtCore.Qt.DecorationRole)
class HeaderDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
header_option = QtWidgets.QStyleOptionHeader()
header_option.rect = option.rect
style = QtWidgets.QApplication.style()
style.drawControl(QtWidgets.QStyle.CE_HeaderSection, header_option, painter)
if option.state & QtWidgets.QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())

View File

@ -0,0 +1,65 @@
from ..Qt import QtWidgets, QtCore
class ExpandableWidget(QtWidgets.QWidget):
expansionChanged = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self._init_ui()
self._widget = None
self._expanded = False
self.toolButton.clicked.connect(self.changeVisibility)
def _init_ui(self):
self.verticalLayout = QtWidgets.QVBoxLayout(self)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setSpacing(0)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSpacing(0)
self.toolButton = QtWidgets.QToolButton(self)
self.toolButton.setArrowType(QtCore.Qt.RightArrow)
self.toolButton.setStyleSheet('border: 0')
self.toolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.horizontalLayout.addWidget(self.toolButton)
self.line = QtWidgets.QFrame(self)
self.line.setFrameShape(QtWidgets.QFrame.HLine)
self.horizontalLayout.addWidget(self.line)
self.verticalLayout.addLayout(self.horizontalLayout)
def addWidget(self, widget: QtWidgets.QWidget):
self._widget = widget
self._widget.setVisible(self._expanded)
self.layout().addWidget(widget)
def setText(self, text: str):
self.toolButton.setText(text)
def isExpanded(self):
return self._expanded
def changeVisibility(self):
if not self._widget:
return
self.setExpansion(not self._expanded)
self.expansionChanged.emit()
def setExpansion(self, state: bool):
self.blockSignals(True)
if state:
self.toolButton.setArrowType(QtCore.Qt.DownArrow)
else:
self.toolButton.setArrowType(QtCore.Qt.RightArrow)
self._expanded = state
self._widget.setVisible(state)
self.blockSignals(False)

406
src/gui_qt/lib/forms.py Normal file
View File

@ -0,0 +1,406 @@
from numpy import inf
from nmreval.utils.text import convert
from ..Qt import QtGui, QtCore, QtWidgets
from .._py.mean_form import Ui_mean_form
class QDelayWidget(QtWidgets.QWidget):
get_data = QtCore.pyqtSignal(str)
newDelay = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.vals = ''
self.dim = 0
self.plainTextEdit.setVisible(False)
def __call__(self):
self.vals = ''
self.dim = 0
self.comboBox.clear()
self.comboBox.blockSignals(True)
self.comboBox.addItem('New list...')
self.comboBox.blockSignals(True)
self.plainTextEdit.setVisible(False)
def add_items(self, item):
self.comboBox.blockSignals(True)
if isinstance(item, list):
self.comboBox.insertItems(0, item)
else:
self.comboBox.insertItem(0, item)
self.comboBox.setCurrentIndex(0)
self.comboBox.blockSignals(False)
@QtCore.pyqtSlot(name='on_toolButton_clicked')
def show_values(self):
if self.plainTextEdit.isVisible():
self.plainTextEdit.clear()
self.plainTextEdit.setVisible(False)
elif self.comboBox.currentText() == 'New list...':
self.newDelay.emit()
else:
self.get_data.emit(self.comboBox.currentText())
self.plainTextEdit.setVisible(True)
self.plainTextEdit.setPlainText(self.vals)
class LineEdit(QtWidgets.QLineEdit):
values_requested = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
def contextMenuEvent(self, evt):
menu = self.createStandardContextMenu()
request_action = menu.addAction('Use value of sets')
action = menu.exec(evt.globalPos())
if action == request_action:
self.values_requested.emit()
class LineEditPost(QtWidgets.QLineEdit):
values_requested = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.suffix = ''
self.prefix = ''
self.editingFinished.connect(self.add_fixes)
def add_fixes(self):
self.setText(self.prefix+super().text()+self.suffix)
def text(self):
text = super().text()
if text.startswith(self.prefix):
text = text[len(self.prefix):]
if text.endswith(self.suffix):
text = text[:-len(self.suffix)]
return text
class FormWidget(QtWidgets.QWidget):
types = {
'float': (float, QtGui.QDoubleValidator),
'int': (int, QtGui.QIntValidator),
'str': (str, lambda: 0),
}
valueChanged = QtCore.pyqtSignal(object)
stateChanged = QtCore.pyqtSignal(bool)
def __init__(self, name: str, validator: str = 'float', fixable: bool = False, parent=None):
super().__init__(parent=parent)
self._init_ui()
self._name = name
self._type = FormWidget.types[validator][0]
self.vals.setValidator(FormWidget.types[validator][1]())
self.vals.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0)
self.label.setText(convert(name))
self._checkable = fixable
self.checkBox.setVisible(fixable)
self.checkBox.stateChanged.connect(lambda x: self.stateChanged.emit(True if x else False))
def _init_ui(self):
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.setSpacing(3)
self.label = QtWidgets.QLabel(self)
layout.addWidget(self.label)
layout.addSpacerItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum))
self.vals = QtWidgets.QLineEdit(self)
layout.addWidget(self.vals)
self.checkBox = QtWidgets.QCheckBox('Fix?', self)
layout.addWidget(self.checkBox)
@property
def value(self):
try:
return self._type(self.vals.text().replace(',', '.'))
except ValueError:
return {float: 0.0, int: 0, str: ''}[self._type]
@value.setter
def value(self, val):
if self._type == 'str':
self.vals.setText(val)
elif self._type == 'int':
self.vals.setText(f'{float(val):.0f}')
else:
self.vals.setText(f'{float(val):.5g}')
def setChecked(self, enable):
if self._checkable:
self.checkBox.setCheckState(QtCore.Qt.Checked if enable else QtCore.Qt.Unchecked)
else:
print(f'Parameter {self._name} is not variable')
def isChecked(self):
return self.checkBox.isChecked()
class SelectionWidget(QtWidgets.QWidget):
selectionChanged = QtCore.pyqtSignal(str, object)
def __init__(self, label: str, argname: str, opts: dict, parent=None):
super().__init__(parent=parent)
self._init_ui()
self.label.setText(convert(label))
for k in opts.keys():
self.comboBox.addItem(k)
self.argname = argname
self.options = opts
self.comboBox.currentIndexChanged.connect(lambda idx: self.selectionChanged.emit(self.argname, self.value))
def _init_ui(self):
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.setSpacing(2)
self.label = QtWidgets.QLabel(self)
layout.addWidget(self.label)
layout.addSpacerItem(QtWidgets.QSpacerItem(65, 20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum))
self.comboBox = QtWidgets.QComboBox(self)
layout.addWidget(self.comboBox)
self.setLayout(layout)
@property
def value(self):
return {self.argname: self.options[self.comboBox.currentText()]}
def get_parameter(self):
return str(self.comboBox.currentText())
@value.setter
def value(self, val):
if isinstance(val, dict):
val = list(val.values())[0]
key = [k for k, v in self.options.items() if v == val][0]
self.comboBox.setCurrentIndex(self.comboBox.findText(key))
class Widget(QtWidgets.QWidget, Ui_mean_form):
valueChanged = QtCore.pyqtSignal()
def __init__(self, name: str, tree: dict, collapsing: bool = False, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._tree = {}
self._collapse = collapsing
self.label.setText(convert(name))
self.lineEdit.setValidator(QtGui.QDoubleValidator())
self.set_graphs(tree)
self.change_graph(0)
if self._collapse:
self.digit_checkbox.hide()
self.frame.hide()
self.data_checkbox.stateChanged.connect(self.collapse_widgets)
else:
self.data_checkbox.stateChanged.connect(self.change_mode)
self.digit_checkbox.stateChanged.connect(self.change_mode)
def set_graphs(self, graph: dict):
self.graph_combobox.blockSignals(True)
self._tree.clear()
for key, (name, _) in graph.items():
self.graph_combobox.addItem(name, userData=key)
self.graph_combobox.blockSignals(False)
self._tree.update(graph)
self.change_graph(0)
@QtCore.pyqtSlot(int, name='on_graph_combobox_currentIndexChanged')
def change_graph(self, idx: int):
self.set_combobox.clear()
key = self.graph_combobox.itemData(idx, QtCore.Qt.UserRole)
if key is not None:
for set_key, set_name in self._tree[key][1]:
self.set_combobox.addItem(set_name, userData=set_key)
def on_lineEdit_textChanged(self, text=''):
if text:
self.valueChanged.emit()
@property
def value(self):
if self.data_checkbox.isChecked():
return self.set_combobox.currentData()
else:
try:
return float(self.lineEdit.text())
except ValueError:
return
@QtCore.pyqtSlot(int)
def change_mode(self, state: int):
box = self.sender()
other_box = self.data_checkbox if box == self.digit_checkbox else self.digit_checkbox
if (state == QtCore.Qt.Unchecked) and (other_box.checkState() == QtCore.Qt.Unchecked):
box.blockSignals(True)
box.setCheckState(QtCore.Qt.Checked)
box.blockSignals(False)
return
other_box.blockSignals(True)
other_box.setChecked(False)
other_box.blockSignals(False)
self.valueChanged.emit()
@QtCore.pyqtSlot(int)
def collapse_widgets(self, state: int):
data_is_checked = state == QtCore.Qt.Checked
self.frame.setVisible(data_is_checked)
self.lineEdit.setVisible(not data_is_checked)
class CheckBoxHeader(QtWidgets.QHeaderView):
clicked = QtCore.pyqtSignal(int, bool)
_x_offset = 3
_y_offset = 0 # This value is calculated later, based on the height of the paint rect
_width = 20
_height = 20
def __init__(self, column_indices, orientation=QtCore.Qt.Horizontal, parent=None):
super().__init__(orientation, parent)
self.setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
# self.setClickable(True)
if isinstance(column_indices, list) or isinstance(column_indices, tuple):
self.column_indices = column_indices
elif isinstance(column_indices, int):
self.column_indices = [column_indices]
else:
raise RuntimeError('column_indices must be a list, tuple or integer')
self.isChecked = {}
for column in self.column_indices:
self.isChecked[column] = 0
def paintSection(self, painter, rect, logicalIndex):
painter.save()
super().paintSection(painter, rect, logicalIndex)
painter.restore()
self._y_offset = int((rect.height()-self._width)/2.)
if logicalIndex in self.column_indices:
option = QtWidgets.QStyleOptionButton()
option.rect = QtCore.QRect(rect.x() + self._x_offset, rect.y() + self._y_offset, self._width, self._height)
option.state = QtWidgets.QStyle.State_Enabled | QtWidgets.QStyle.State_Active
if self.isChecked[logicalIndex] == 2:
option.state |= QtWidgets.QStyle.State_NoChange
elif self.isChecked[logicalIndex]:
option.state |= QtWidgets.QStyle.State_On
else:
option.state |= QtWidgets.QStyle.State_Off
self.style().drawControl(QtWidgets.QStyle.CE_CheckBox,option,painter)
def updateCheckState(self, index, state):
self.isChecked[index] = state
self.viewport().update()
def mousePressEvent(self, event):
index = self.logicalIndexAt(event.pos())
if 0 <= index < self.count():
x = self.sectionPosition(index)
if x + self._x_offset < event.pos().x() < x + self._x_offset + self._width and self._y_offset < event.pos().y() < self._y_offset + self._height:
if self.isChecked[index] == 1:
self.isChecked[index] = 0
else:
self.isChecked[index] = 1
self.clicked.emit(index, self.isChecked[index])
self.viewport().update()
else:
super().mousePressEvent(event)
else:
super().mousePressEvent(event)
class CustomRangeDialog(QtWidgets.QDialog):
""" Simple dialog to enter x and y limits to fit regions"""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.gl = QtWidgets.QGridLayout()
self.gl.addWidget(QtWidgets.QLabel('Leave empty for complete range'), 0, 0, 1, 4)
self._limits = [[], []]
for i, orient in enumerate(['x', 'y'], start=1):
self.gl.addWidget(QtWidgets.QLabel(orient + ' from'), i, 0)
self.gl.addWidget(QtWidgets.QLabel('to'), i, 2)
for j in [0, 1]:
lim = QtWidgets.QLineEdit()
lim.setValidator(QtGui.QDoubleValidator())
self._limits[i-1].append(lim)
self.gl.addWidget(lim, i, 2*j+1)
self.buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok)
self.buttonbox.accepted.connect(self.accept)
self.gl.addWidget(self.buttonbox, 3, 0, 1, 4)
self.setLayout(self.gl)
@property
def limits(self):
ret_val = []
for orient in self._limits:
for i, w in enumerate(orient):
val = w.text()
if val == '':
ret_val.append(-inf if i == 0 else inf)
else:
ret_val.append(float(val))
return ret_val
class ElideComboBox(QtWidgets.QComboBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.view().setTextElideMode(QtCore.Qt.ElideRight)
def paintEvent(self, evt: QtGui.QPaintEvent):
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter = QtWidgets.QStylePainter(self)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
rect = self.style().subControlRect(QtWidgets.QStyle.CC_ComboBox, opt, QtWidgets.QStyle.SC_ComboBoxEditField, self)
opt.currentText = painter.fontMetrics().elidedText(opt.currentText, QtCore.Qt.ElideRight, rect.width())
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)

380
src/gui_qt/lib/gol.py Normal file
View File

@ -0,0 +1,380 @@
from __future__ import annotations
import numbers
import numpy as np
import sys
from itertools import accumulate
from ..Qt import QtWidgets, QtGui, QtCore
from .._py.gol import Ui_Form
def circle(radius):
pxl = []
for x in range(int(np.ceil(radius/np.sqrt(2)))):
y = round(np.sqrt(radius**2-x**2))
pxl.extend([[x, y], [y, x], [x, -y], [y, -x],
[-x, -y], [-y, -x], [-x, y], [-y, x]])
return np.array(pxl)
def square(a):
pxl = []
pxl.extend(list(zip(range(-a, a+1), [a]*(2*a+1))))
pxl.extend(list(zip(range(-a, a+1), [-a]*(2*a+1))))
pxl.extend(list(zip([a]*(2*a+1), range(-a, a+1))))
pxl.extend(list(zip([-a]*(2*a+1), range(-a, a+1))))
return np.array(pxl)
def diamond(a):
pxl = []
for x in range(int(a+1)):
y = a-x
pxl.extend([[x, y], [-x, y], [x, -y], [-x, -y]])
return np.array(pxl)
def plus(a):
pxl = np.zeros((4*int(a)+2, 2), dtype=int)
pxl[:2*int(a)+1, 0] = np.arange(-a, a+1)
pxl[2*int(a)+1:, 1] = np.arange(-a, a+1)
return pxl
# birth, survival
predefined_rules = {
'Conway': ('23', '3'),
'Life34': ('34', '34'),
'Coagulation': ('235678', '378'),
'Corrosion': ('12345', '45'),
'Long life': ('5', '345'),
'Maze': ('12345', '3'),
'Coral': ('45678', '3'),
'Pseudo life': ('238', '357'),
'Flakes': ('012345678', '3'),
'Gnarl': ('1', '1'),
'Fabric': ('12', '1'),
'Assimilation': ('4567', '345'),
'Diamoeba': ('5678', '35678'),
'High life': ('23', '36'),
'More Maze': ('1235', '3'),
'Replicator': ('1357', '1357'),
'Seed': ('', '2'),
'Serviette': ('', '234'),
'More coagulation': ('235678', '3678'),
'Domino': ('125', '36'),
'Anneal': ('35678', '4678'),
}
class GameOfLife:
colors = [
[31, 119, 180],
[255, 127, 14],
[44, 160, 44],
[214, 39, 40],
[148, 103, 189],
[140, 86, 75]
]
def __init__(self,
size: tuple = (400, 400),
pattern: np.ndarray = None,
fill: float | list[float] = 0.05,
num_pop: int = 1,
rules: str | tuple = ('23', '3')
):
self.populations = num_pop if pattern is None else 1
self._size = size
self._world = np.zeros(shape=(*self._size, self.populations), dtype=np.uint8)
self._neighbors = np.zeros(shape=self._world.shape, dtype=np.uint8)
self._drawing = 255 * np.zeros(shape=(*self._size, 3), dtype=np.uint8)
self.fill = np.zeros(self.populations)
self._populate(fill, pattern)
if isinstance(rules, str):
try:
b_rule, s_rule = predefined_rules[rules]
except KeyError:
raise ValueError('Rule is not predefined')
else:
b_rule, s_rule = rules
self.survival_condition = np.array([int(c) for c in s_rule])
self.birth_condition = np.array([int(c) for c in b_rule])
# indexes for neighbors
self._neighbor_idx = [
[[slice(None), slice(1, None)], [slice(None), slice(0, -1)]], # N (:, 1:), S (:, _-1)
[[slice(1, None), slice(1, None)], [slice(0, -1), slice(0, -1)]], # NE (1:, 1:), SW (:-1, :-1)
[[slice(1, None), slice(None)], [slice(0, -1), slice(None)]], # E (1:, :), W (:-1, :)
[[slice(1, None), slice(0, -1)], [slice(0, -1), slice(1, None)]] # SE (1:, :-1), NW (:-1:, 1:)
]
def _populate(self, fill, pattern):
if pattern is None:
if isinstance(fill, numbers.Number):
fill = [fill]*self.populations
prob = (np.random.default_rng().random(size=self._size))
lower_lim = 0
for i, upper_lim in enumerate(accumulate(fill)):
self._world[:, :, i] = (lower_lim <= prob) & (prob < upper_lim)
lower_lim = upper_lim
else:
pattern = np.asarray(pattern)
x_step = self._size[0]//(self.populations+1)
y_step = self._size[1]//(self.populations+1)
for i in range(self.populations):
self._world[-pattern[:, 0]+(i+1)*x_step, pattern[:, 1]+(i+1)*y_step, i] = 1
for i in range(self.populations):
self.fill[i] = self._world[:, :, i].sum() / (self._size[0]*self._size[1])
def tick(self):
n = self._neighbors
w = self._world
n[...] = 0
for idx_1, idx_2 in self._neighbor_idx:
n[tuple(idx_1)] += w[tuple(idx_2)]
n[tuple(idx_2)] += w[tuple(idx_1)]
birth = ((np.in1d(n, self.birth_condition).reshape(n.shape)) & (w.sum(axis=-1) == 0)[:, :, None])
survive = ((np.in1d(n, self.survival_condition).reshape(n.shape)) & (w == 1))
w[...] = 0
w[birth | survive] = 1
def draw(self, shade: int):
if shade == 0:
self._drawing[...] = 0
elif shade == 1:
self._drawing -= (self._drawing/4).astype(np.uint8)
self._drawing = self._drawing.clip(0, 127)
for i in range(self.populations):
self._drawing[(self._world[:, :, i] == 1)] = self.colors[i]
self.fill[i] = self._world[:, :, i].sum() / (self._size[0]*self._size[1])
return self._drawing
class QGameOfLife(QtWidgets.QDialog, Ui_Form):
SPEEDS = [0.5, 1, 2, 5, 7.5, 10, 12.5, 15, 20, 25, 30, 40, 50]
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._size = (100, 100)
self.game = None
self._step = 0
self._shading = 1
self._speed = 5
self.timer = QtCore.QTimer()
self.timer.setInterval(100)
self.timer.timeout.connect(self.tick)
self._init_ui()
def _init_ui(self):
self.item = None
self.scene = QtWidgets.QGraphicsScene()
self.item = self.scene.addPixmap(QtGui.QPixmap())
self.view.setScene(self.scene)
self.rule_cb.addItems(list(predefined_rules.keys()))
self.random_widgets = []
for _ in range(6):
w = QSliderText(15, parent=self)
w.slider.valueChanged.connect(self.set_max_population)
self.verticalLayout.addWidget(w)
self.random_widgets.append(w)
self.birth_line.setValidator(QtGui.QIntValidator())
self.survival_line.setValidator(QtGui.QIntValidator())
for w in self.random_widgets[1:] + [self.object_widget]:
w.hide()
self.setGeometry(QtWidgets.QStyle.alignedRect(QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter,
self.size(), QtWidgets.qApp.desktop().availableGeometry()))
self.view.resizeEvent = self.resizeEvent
@QtCore.pyqtSlot(int)
def on_object_combobox_currentIndexChanged(self, idx: int):
for w in self.random_widgets + [self.object_widget, self.rand_button_wdgt]:
w.hide()
if idx == 0:
self.random_widgets[0].show()
self.rand_button_wdgt.show()
else:
self.object_widget.show()
@QtCore.pyqtSlot()
def on_add_random_button_clicked(self):
if self.object_combobox.currentIndex() != 0:
return
for w in self.random_widgets[1:]:
if not w.isVisible():
w.show()
break
@QtCore.pyqtSlot()
def on_remove_random_button_clicked(self):
if self.object_combobox.currentIndex() != 0:
return
for w in reversed(self.random_widgets[1:]):
if w.isVisible():
w.hide()
break
@QtCore.pyqtSlot(str)
def on_rule_cb_currentIndexChanged(self, entry: str):
rule = predefined_rules[entry]
self.birth_line.setText(rule[1])
self.survival_line.setText(rule[0])
@QtCore.pyqtSlot(int)
def set_max_population(self, _: int):
over_population = -100
num_tribes = -1
for w in self.random_widgets:
if w.isVisible():
over_population += w.slider.value()
num_tribes += 1
if over_population > 0:
for w in self.random_widgets:
if w == self.sender() or w.isHidden():
continue
w.setValue(max(0, int(w.slider.value()-over_population/num_tribes)))
@QtCore.pyqtSlot()
def on_start_button_clicked(self):
self.pause_button.setChecked(False)
self._step = 0
self.current_step.setText(f'{self._step} steps')
self._size = (int(self.height_box.value()), int(self.width_box.value()))
pattern = None
num_pop = 0
fill = []
pattern_size = self.object_size.value()
if 2*pattern_size >= max(self._size):
pattern_size = int(np.floor(max(self._size[0]-1, self._size[1]-1) / 2))
idx = self.object_combobox.currentIndex()
if idx == 0:
for w in self.random_widgets:
if w.isVisible():
num_pop += 1
fill.append(w.slider.value()/100)
else:
pattern = [None, circle, square, diamond, plus][idx](pattern_size)
self.game = GameOfLife(self._size, pattern=pattern, fill=fill, num_pop=num_pop,
rules=(self.birth_line.text(), self.survival_line.text()))
self.draw()
self.view.fitInView(self.item, QtCore.Qt.KeepAspectRatio)
self.timer.start()
def tick(self):
self.game.tick()
self.draw()
self._step += 1
self.current_step.setText(f'{self._step} steps')
self.cover_label.setText('\n'.join([f'Color {i+1}: {f*100:.2f} %' for i, f in enumerate(self.game.fill)]))
@QtCore.pyqtSlot()
def on_faster_button_clicked(self):
self._speed = min(self._speed+1, len(QGameOfLife.SPEEDS)-1)
new_speed = QGameOfLife.SPEEDS[self._speed]
self.timer.setInterval(int(1000/new_speed))
self.velocity_label.setText(f'{new_speed:.1f} steps/s')
@QtCore.pyqtSlot()
def on_slower_button_clicked(self):
self._speed = max(self._speed-1, 0)
new_speed = QGameOfLife.SPEEDS[self._speed]
self.timer.setInterval(int(1000/new_speed))
self.velocity_label.setText(f'{new_speed:.1f} steps/s')
@QtCore.pyqtSlot()
def on_pause_button_clicked(self):
if self.pause_button.isChecked():
self.timer.stop()
else:
self.timer.start()
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
def on_buttonGroup_buttonClicked(self, button):
self._shading = [self.radioButton, self.vanish_shadow, self.full_shadow].index(button)
def draw(self):
bitmap = self.game.draw(shade=self._shading)
h, w, c = bitmap.shape
image = QtGui.QImage(bitmap.tobytes(), w, h, w*c, QtGui.QImage.Format_RGB888)
self.scene.removeItem(self.item)
pixmap = QtGui.QPixmap.fromImage(image)
self.item = self.scene.addPixmap(pixmap)
@QtCore.pyqtSlot(int)
def on_hide_button_stateChanged(self, state: int):
self.option_frame.setVisible(not state)
self.view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
def resizeEvent(self, evt):
super().resizeEvent(evt)
self.view.fitInView(self.item, QtCore.Qt.KeepAspectRatio)
def showEvent(self, evt):
super().showEvent(evt)
self.view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
class QSliderText(QtWidgets.QWidget):
def __init__(self, value: int, parent=None):
super().__init__(parent=parent)
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(3)
self.slider = QtWidgets.QSlider(self)
self.slider.setOrientation(QtCore.Qt.Horizontal)
self.slider.setMaximum(100)
self.slider.setTickPosition(self.slider.TicksBothSides)
self.slider.setTickInterval(5)
self.value = QtWidgets.QLabel(self)
self.slider.valueChanged.connect(lambda x: self.value.setText(f'{x} %'))
layout.addWidget(self.slider)
layout.addWidget(self.value)
self.setLayout(layout)
self.setValue(value)
def setValue(self, value: int):
self.slider.setValue(value)
self.value.setText(f'{value} %')

216
src/gui_qt/lib/namespace.py Normal file
View File

@ -0,0 +1,216 @@
import inspect
import re
from collections import namedtuple
import numpy as np
from nmreval import models
from nmreval.configs import config_paths
from nmreval.lib.importer import find_models, import_
from nmreval.utils import constants as constants
from nmreval.utils.text import convert
from ..Qt import QtWidgets, QtCore
from .._py.namespace_widget import Ui_Form
class Namespace:
def __init__(self, fitfuncs=False, const=False, basic=False):
self.namespace = {}
self.groupings = {}
self.top_levels = {}
if basic:
self.add_namespace({'x': (None, 'x values'), 'y': (None, 'x values'), 'y_err': (None, 'y error values'),
'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), 'np': (np, 'numpy module')},
parents=('Basic', 'General'))
self.add_namespace({'sin': (np.sin, 'Sine', 'sin(PIKA)'), 'cos': (np.cos, 'Cosine', 'cos(PIKA)'),
'tan': (np.tan, 'Tangens', 'tan(PIKA)'), 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'),
'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'),
'exp': (np.exp, 'Exponential', 'exp(PIKA)'), 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'),
'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]',
'lin_range(start, stop, N)'),
'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]',
'lin_range(start, stop, N)')},
parents=('Basic', 'Functions'))
self.add_namespace({'max': (np.max, 'Maximum value', 'max(PIKA)'),
'min': (np.min, 'Minimum value', 'min(PIKA)'),
'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'),
'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)')},
parents=('Basic', 'Values'))
if const:
self.add_namespace({'e': (constants.e, 'e / As'), 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'),
'Eu': (constants.Eu,), 'h': (constants.h, 'h / eVs'),
'hbar': (constants.hbar, 'hbar / eVs'), 'kB': (constants.kB, 'kB / eV/K'),
'mu0': (constants.mu0, 'mu0 / Vs/Am'), 'NA': (constants.NA, 'NA / 1/mol'),
'pi': (constants.pi,), 'R': (constants.R, 'R / eV')},
parents=('Constants', 'Maybe useful'))
self.add_namespace({f'gamma["{k}"]': (v, k, f'gamma["{k}"]') for k, v in constants.gamma.items()},
parents=('Constants', 'Magnetogyric ratios (in 1/(sT))'))
if fitfuncs:
self.make_dict_from_fitmodule(models)
try:
usermodels = import_(config_paths() / 'usermodels.py')
self.make_dict_from_fitmodule(usermodels, parent='User-defined')
except FileNotFoundError:
pass
def __str__(self):
ret = '\n'.join([f'{k}: {v}' for k, v in self.groupings.items()])
return ret
def make_dict_from_fitmodule(self, module, parent='Fit function'):
fitfunc_dict = {}
for funcs in find_models(module):
name = funcs.name
if funcs.type not in fitfunc_dict:
fitfunc_dict[funcs.type] = {}
func_list = fitfunc_dict[funcs.type]
func_args = 'x, ' + ', '.join(convert(p, old='latex', new='str', brackets=False) for p in funcs.params)
# keywords arguments need names in function
sig = inspect.signature(funcs.func)
for p in sig.parameters.values():
if p.default != p.empty:
func_args += ', ' + str(p)
func_list[f"{funcs.__name__}"] = (funcs.func, name, f"{funcs.__name__}({func_args})")
for k, v in fitfunc_dict.items():
self.add_namespace(v, parents=(parent, k))
def add_namespace(self, namespace, parents):
if parents[0] in self.top_levels:
if parents[1] not in self.top_levels[parents[0]]:
self.top_levels[parents[0]].append(parents[1])
else:
self.top_levels[parents[0]] = [parents[1]]
if (parents[0], parents[1]) not in self.groupings:
self.groupings[(parents[0], parents[1])] = list(namespace.keys())
else:
self.groupings[(parents[0], parents[1])].extend(list(namespace.keys()))
self.namespace.update(namespace)
def flatten(self):
ret_dic = {}
graph = namedtuple('graphs', ['s'])
sets = namedtuple('sets', ['x', 'y', 'y_err', 'value', 'fit'], defaults=(None,))
gs = re.compile(r'g\[(\d+)].s\[(\d+)].(x|y(?:_err)*|value|fit)')
for k, v in self.namespace.items():
m = gs.match(k)
if m:
if 'g' not in ret_dic:
ret_dic['g'] = {}
g_num, s_num, pos = m.groups()
if int(g_num) not in ret_dic['g']:
ret_dic['g'][int(g_num)] = graph({})
gg = ret_dic['g'][int(g_num)]
if int(s_num) not in gg.s:
gg.s[int(s_num)] = sets('', '', '', '')
ss = gg.s[int(s_num)]
if pos == 'fit':
if ss.fit is None:
gg.s[int(s_num)] = ss._replace(**{pos: {}})
ss = gg.s[int(s_num)]
ss.fit[k[m.end()+2:-2]] = v[0]
else:
ret_dic['g'][int(g_num)].s[int(s_num)] = ss._replace(**{pos: v[0]})
else:
ret_dic[k] = v[0]
return ret_dic
class QNamespaceWidget(QtWidgets.QWidget, Ui_Form):
selected = QtCore.pyqtSignal(str)
sendKey = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self.namespace = None
def make_namespace(self):
self.set_namespace(Namespace(fitfuncs=True, const=True, basic=True))
@QtCore.pyqtSlot(int, name='on_groups_comboBox_currentIndexChanged')
def show_subgroup(self, idx: int):
name = self.groups_comboBox.itemText(idx)
self.subgroups_comboBox.blockSignals(True)
self.subgroups_comboBox.clear()
for item in self.namespace.top_levels[name]:
self.subgroups_comboBox.addItem(item)
self.subgroups_comboBox.blockSignals(False)
self.show_namespace(0)
@QtCore.pyqtSlot(int, name='on_subgroups_comboBox_currentIndexChanged')
def show_namespace(self, idx: int):
subspace = self.namespace.groupings[(self.groups_comboBox.currentText(), self.subgroups_comboBox.itemText(idx))]
self.namespace_table.clear()
self.namespace_table.setRowCount(0)
for entry in subspace:
key_item = QtWidgets.QTableWidgetItem(entry)
key_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
vals = self.namespace.namespace[entry]
if len(vals) < 3:
alias = entry
else:
alias = vals[2]
if len(vals) < 2:
display = entry
else:
display = vals[1]
value_item = QtWidgets.QTableWidgetItem(display)
value_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
key_item.setData(QtCore.Qt.UserRole, alias)
key_item.setData(QtCore.Qt.UserRole+1, entry)
value_item.setData(QtCore.Qt.UserRole, alias)
value_item.setData(QtCore.Qt.UserRole+1, entry)
row = self.namespace_table.rowCount()
self.namespace_table.setRowCount(row+1)
self.namespace_table.setItem(row, 0, key_item)
self.namespace_table.setItem(row, 1, value_item)
self.namespace_table.resizeColumnsToContents()
def set_namespace(self, namespace):
self.namespace = namespace
self.groups_comboBox.blockSignals(True)
self.groups_comboBox.clear()
for k in self.namespace.top_levels.keys():
self.groups_comboBox.addItem(k)
self.groups_comboBox.blockSignals(False)
self.show_subgroup(0)
@QtCore.pyqtSlot(QtWidgets.QTableWidgetItem, name='on_namespace_table_itemDoubleClicked')
def item_selected(self, item: QtWidgets.QTableWidgetItem):
self.selected.emit(item.data(QtCore.Qt.UserRole))
self.sendKey.emit(item.data(QtCore.Qt.UserRole+1))

View File

@ -0,0 +1,463 @@
import numpy as np
from pyqtgraph import (
InfiniteLine,
ErrorBarItem,
LinearRegionItem, mkBrush,
mkColor, mkPen,
PlotDataItem,
LegendItem,
)
from nmreval.lib.colors import BaseColor, Colors
from nmreval.lib.lines import LineStyle
from nmreval.lib.symbols import SymbolStyle
from ..Qt import QtCore, QtGui
"""
Subclasses of pyqtgraph items, mostly to take care of log-scaling.
pyqtgraph looks for function "setLogMode" for logarithmic axes, so needs to be implemented.
"""
class LogInfiniteLine(InfiniteLine):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.logmode = [False, False]
def setLogMode(self, xmode, ymode):
"""
Does only work for vertical and horizontal lines
"""
if self.logmode == [xmode, ymode]:
return
new_p = list(self.p[:])
if (self.angle == 90) and (self.logmode[0] != xmode):
if xmode:
new_p[0] = np.log10(new_p[0]+np.finfo(float).eps)
else:
new_p[0] = 10**new_p[0]
if (self.angle == 0) and (self.logmode[1] != ymode):
if ymode:
new_p[1] = np.log10(new_p[1]+np.finfo(float).eps)
else:
new_p[1] = 10**new_p[1]
self.logmode = [xmode, ymode]
if np.all(np.isfinite(new_p)):
self.setPos(new_p)
else:
self.setPos(self.p)
self.sigPositionChanged.emit(self)
def setValue(self, v):
if isinstance(v, QtCore.QPointF):
v = [v.x(), v.y()]
with np.errstate(divide='ignore'):
if isinstance(v, (list, tuple)):
for i in [0, 1]:
if self.logmode[i]:
v[i] = np.log10(v[i]+np.finfo(float).eps)
else:
if self.angle == 90:
if self.logmode[0]:
v = [np.log10(v+np.finfo(float).eps), 0]
else:
v = [v, 0]
elif self.angle == 0:
if self.logmode[1]:
v = [0, np.log10(v+np.finfo(float).eps)]
else:
v = [0, v]
else:
raise ValueError('LogInfiniteLine: Diagonal lines need two values')
self.setPos(v)
def value(self):
p = self.getPos()
if self.angle == 0:
return 10**p[1] if self.logmode[1] else p[1]
elif self.angle == 90:
return 10**p[0] if self.logmode[0] else p[0]
else:
if self.logmode[0]:
p[0] = 10**p[0]
if self.logmode[1]:
p[1] = 10**p[1]
return p
class ErrorBars(ErrorBarItem):
def __init__(self, **opts):
self.log = [False, False]
opts['xData'] = opts.get('x', None)
opts['yData'] = opts.get('y', None)
opts['topData'] = opts.get('top', None)
opts['bottomData'] = opts.get('bottom', None)
super().__init__(**opts)
def setLogMode(self, x_mode, y_mode):
if self.log == [x_mode, y_mode]:
return
self._make_log_scale(x_mode, y_mode)
self.log[0] = x_mode
self.log[1] = y_mode
super().setData()
def setData(self, **opts):
self.opts.update(opts)
self.opts['xData'] = opts.get('x', self.opts['xData'])
self.opts['yData'] = opts.get('y', self.opts['yData'])
self.opts['topData'] = opts.get('top', self.opts['topData'])
self.opts['bottomData'] = opts.get('bottom', self.opts['bottomData'])
if any(self.log):
self._make_log_scale(*self.log)
super().setData()
def _make_log_scale(self, x_mode, y_mode):
_x = self.opts['xData']
_xmask = np.logical_not(np.isnan(_x))
if x_mode:
with np.errstate(all='ignore'):
_x = np.log10(_x)
_xmask = np.logical_not(np.isnan(_x))
_y = self.opts['yData']
_ymask = np.ones(_y.size, dtype=bool)
_top = self.opts['topData']
_bottom = self.opts['bottomData']
if y_mode:
with np.errstate(all='ignore'):
logtop = np.log10(self.opts['topData']+_y)
logbottom = np.log10(_y-self.opts['bottomData'])
_y = np.log10(_y)
_ymask = np.logical_not(np.isnan(_y))
logbottom[logbottom == -np.inf] = _y[logbottom == -np.inf]
_bottom = np.nan_to_num(np.maximum(_y-logbottom, 0))
logtop[logtop == -np.inf] = _y[logtop == -np.inf]
_top = np.nan_to_num(np.maximum(logtop-_y, 0))
_mask = np.logical_and(_xmask, _ymask)
self.opts['x'] = _x[_mask]
self.opts['y'] = _y[_mask]
self.opts['top'] = _top[_mask]
self.opts['bottom'] = _bottom[_mask]
class PlotItem(PlotDataItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.opts['linecolor'] = (0, 0, 0)
self.opts['symbolcolor'] = (0, 0, 0)
if self.opts['pen'] is not None:
pen = self.opts['pen']
if isinstance(pen, tuple):
self.opts['linecolor'] = pen
else:
c = pen.color()
self.opts['linecolor'] = c.red(), c.green(), c.blue()
if self.symbol != SymbolStyle.No:
c = self.opts['symbolBrush'].color()
self.opts['symbolcolor'] = c.red(), c.green(), c.blue()
def __getitem__(self, item):
return self.opts.get(item, None)
@property
def symbol(self):
return SymbolStyle.from_str(self.opts['symbol'])
@property
def symbolcolor(self):
sc = self.opts['symbolcolor']
if isinstance(sc, tuple):
return Colors(sc)
elif isinstance(sc, str):
return Colors.from_str(sc)
else:
return sc
@property
def symbolsize(self):
return self.opts['symbolSize']
@property
def linestyle(self) -> LineStyle:
pen = self.opts['pen']
if pen is None:
return LineStyle.No
else:
return LineStyle(pen.style())
@property
def linewidth(self) -> float:
pen = self.opts['pen']
if pen is None:
return 1.
else:
return pen.widthF()
@property
def linecolor(self) -> Colors:
lc = self.opts['linecolor']
if isinstance(lc, tuple):
return Colors(lc)
elif isinstance(lc, str):
return Colors.from_str(lc)
else:
return lc
def updateItems(self):
"""
We override this function so that curves with nan/inf values can be displayed.
Newer versions close this bug differently (https://github.com/pyqtgraph/pyqtgraph/pull/1058)
but this works somewhat.
"""
curveArgs = {}
for k, v in [('pen', 'pen'), ('shadowPen', 'shadowPen'), ('fillLevel', 'fillLevel'),
('fillOutline', 'fillOutline'), ('fillBrush', 'brush'), ('antialias', 'antialias'),
('connect', 'connect'), ('stepMode', 'stepMode')]:
curveArgs[v] = self.opts[k]
scatterArgs = {}
for k, v in [('symbolPen', 'pen'), ('symbolBrush', 'brush'), ('symbol', 'symbol'), ('symbolSize', 'size'),
('data', 'data'), ('pxMode', 'pxMode'), ('antialias', 'antialias')]:
if k in self.opts:
scatterArgs[v] = self.opts[k]
x, y = self.getData()
if x is None:
x = []
if y is None:
y = []
if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None):
is_finite = np.isfinite(x) & np.isfinite(y)
all_finite = np.all(is_finite)
if not all_finite:
# remove all bad values
x = x[is_finite]
y = y[is_finite]
self.curve.setData(x=x, y=y, **curveArgs)
self.curve.show()
else:
self.curve.hide()
if scatterArgs['symbol'] is not None:
if self.opts.get('stepMode', False) is True:
x = 0.5 * (x[:-1] + x[1:])
self.scatter.setData(x=x, y=y, **scatterArgs)
self.scatter.show()
else:
self.scatter.hide()
def set_symbol(self, symbol=None, size=None, color=None):
print(symbol, type(symbol), isinstance(symbol, SymbolStyle))
if symbol is not None:
if isinstance(symbol, int):
self.setSymbol(SymbolStyle(symbol).to_str())
elif isinstance(symbol, SymbolStyle):
self.setSymbol(symbol.to_str())
else:
self.setSymbol(symbol)
if color is not None:
self.set_color(color, symbol=True)
if size is not None:
self.setSymbolSize(size)
def set_color(self, color, symbol=False, line=False):
if isinstance(color, BaseColor):
color = color.rgb()
elif isinstance(color, QtGui.QColor):
color = color.getRgb()[:3]
if symbol:
self.setSymbolBrush(mkBrush(color))
self.setSymbolPen(mkPen(color=color))
self.opts['symbolcolor'] = color
if line:
pen = self.opts['pen']
self.opts['linecolor'] = color
if pen is not None:
pen.setColor(mkColor(color))
self.opts['pen'] = pen
self.updateItems()
def set_line(self, style=None, width=None, color=None):
pen = self.opts['pen']
if pen is None:
pen = mkPen(style=QtCore.Qt.NoPen)
if width is not None:
pen.setWidthF(width)
if style is not None:
if isinstance(style, LineStyle):
style = style.value
pen.setStyle(style)
self.opts['pen'] = pen
self.updateItems()
if color is not None:
self.set_color(color, symbol=False, line=True)
def get_data_opts(self) -> dict:
x, y = self.xData, self.yData
if (x is None) or (len(x) == 0):
return {}
opts = self.opts
item_dic = {
'x': x, 'y': y,
'name': opts['name'],
'symbolsize': opts['symbolSize']
}
if opts['symbol'] is None:
item_dic['symbol'] = SymbolStyle.No
item_dic['symbolcolor'] = None
else:
item_dic['symbol'] = SymbolStyle.from_str(opts['symbol'])
item_dic['symbolcolor'] = opts['symbolcolor']
pen = opts['pen']
if pen is not None:
item_dic['linestyle'] = LineStyle(pen.style())
item_dic['linecolor'] = opts['linecolor']
item_dic['linewidth'] = pen.widthF()
else:
item_dic['linestyle'] = LineStyle.No
item_dic['linecolor'] = None
item_dic['linewidth'] = 0.0
if item_dic['linecolor'] is None and item_dic['symbolcolor'] is None:
item_dic['symbolcolor'] = Colors.Black.rgb()
elif item_dic['linecolor'] is None:
item_dic['linecolor'] = item_dic['symbolcolor']
elif item_dic['symbolcolor'] is None:
item_dic['symbolcolor'] = item_dic['linecolor']
return item_dic
class RegionItem(LinearRegionItem):
def __init__(self, *args, **kwargs):
self.mode = kwargs.pop('mode', 'half')
super().__init__(*args, **kwargs)
self.logmode = False
self.first = True
def setLogMode(self, xmode, _):
if self.logmode == xmode:
return
if xmode:
new_region = [np.log10(self.lines[0].value()), np.log10(self.lines[1].value())]
if np.isnan(new_region[1]):
new_region[1] = self.lines[1].value()
if np.isnan(new_region[0]):
new_region[0] = new_region[1]/10.
else:
new_region = [10**self.lines[0].value(), 10**self.lines[1].value()]
self.logmode = xmode
self.setRegion(new_region)
def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == self._orientation_axis[self.orientation]:
r = self.getRegion()
if self.logmode:
r = np.log10(r[0]), np.log10(r[1])
return r
else:
return None
def getRegion(self):
region = super().getRegion()
if self.logmode:
return 10**region[0], 10**region[1]
else:
return region
def boundingRect(self):
# overwrite to draw correct rect in logmode
br = self.viewRect() # bounds of containing ViewBox mapped to local coords.
rng = self.getRegion()
if self.logmode:
rng = np.log10(rng[0]), np.log10(rng[1])
if self.orientation in ('vertical', LinearRegionItem.Vertical):
br.setLeft(rng[0])
br.setRight(rng[1])
length = br.height()
br.setBottom(br.top() + length * self.span[1])
br.setTop(br.top() + length * self.span[0])
else:
br.setTop(rng[0])
br.setBottom(rng[1])
length = br.width()
br.setRight(br.left() + length * self.span[1])
br.setLeft(br.left() + length * self.span[0])
br = br.normalized()
if self._bounds != br:
self._bounds = br
self.prepareGeometryChange()
return br
class LegendItemBlock(LegendItem):
"""
Simple subclass that stops dragging legend outside of view
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.layout.setContentsMargins(1, 1, 1, 1)
def mouseDragEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
ev.accept()
dpos = ev.pos() - ev.lastPos()
vb_rect = self.parentItem().rect()
pos = self.pos()
# upper left corner and a point a little more to the bottom right must be inside
if vb_rect.contains(pos+dpos) and vb_rect.contains(pos+dpos+QtCore.QPointF(20., 20.)):
self.autoAnchor(pos + dpos)
else:
self.autoAnchor(pos)

110
src/gui_qt/lib/randpok.py Normal file
View File

@ -0,0 +1,110 @@
import os.path
import json
import urllib.request
import webbrowser
import random
from ..Qt import QtGui, QtCore, QtWidgets
from .._py.pokemon import Ui_Dialog
random.seed()
class QPokemon(QtWidgets.QDialog, Ui_Dialog):
def __init__(self, number=None, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._js = json.load(open(os.path.join(path_to_module, 'utils', 'pokemon.json'), 'r'), encoding='UTF-8')
self._id = 0
if number is not None and number in range(1, len(self._js)+1):
poke_nr = f'{number:03d}'
self._id = number
else:
poke_nr = f'{random.randint(1, len(self._js)):03d}'
self._id = int(poke_nr)
self._pokemon = None
self.show_pokemon(poke_nr)
self.label_15.linkActivated.connect(lambda x: webbrowser.open(x))
self.buttonBox.clicked.connect(self.randomize)
self.next_button.clicked.connect(self.show_next)
self.prev_button.clicked.connect(self.show_prev)
def show_pokemon(self, nr):
self._pokemon = self._js[nr]
self.setWindowTitle('Pokémon: ' + self._pokemon['Deutsch'])
for i in range(self.tabWidget.count(), -1, -1):
print('i', self.tabWidget.count(), i)
try:
self.tabWidget.widget(i).deleteLater()
except AttributeError:
pass
for n, img in self._pokemon['Bilder']:
w = QtWidgets.QWidget()
vl = QtWidgets.QVBoxLayout()
l = QtWidgets.QLabel(self)
l.setAlignment(QtCore.Qt.AlignHCenter)
pixmap = QtGui.QPixmap()
try:
pixmap.loadFromData(urllib.request.urlopen(img, timeout=0.5).read())
except IOError:
l.setText(n)
else:
sc_pixmap = pixmap.scaled(256, 256, QtCore.Qt.KeepAspectRatio)
l.setPixmap(sc_pixmap)
vl.addWidget(l)
w.setLayout(vl)
self.tabWidget.addTab(w, n)
if len(self._pokemon['Bilder']) <= 1:
self.tabWidget.tabBar().setVisible(False)
else:
self.tabWidget.tabBar().setVisible(True)
self.tabWidget.adjustSize()
self.name.clear()
keys = ['National-Dex', 'Kategorie', 'Typ', 'Größe', 'Gewicht', 'Farbe', 'Link']
label_list = [self.pokedex_nr, self.category, self.poketype, self.weight, self.height, self.color, self.info]
for (k, label) in zip(keys, label_list):
v = self._pokemon[k]
if isinstance(v, list):
v = os.path.join('', *v)
if k == 'Link':
v = '<a href={}>{}</a>'.format(v, v)
label.setText(v)
for k in ['Deutsch', 'Japanisch', 'Englisch', 'Französisch']:
v = self._pokemon[k]
self.name.addItem(k + ': ' + v)
self.adjustSize()
def randomize(self, idd):
if idd.text() == 'Retry':
new_number = f'{random.randint(1, len(self._js)):03d}'
self._id = int(new_number)
self.show_pokemon(new_number)
else:
self.close()
def show_next(self):
new_number = self._id + 1
if new_number > len(self._js):
new_number = 1
self._id = new_number
self.show_pokemon(f'{new_number:03d}')
def show_prev(self):
new_number = self._id - 1
if new_number == 0:
new_number = len(self._js)
self._id = new_number
self.show_pokemon(f'{new_number:03d}')

View File

@ -0,0 +1,45 @@
from math import inf
from ..Qt import QtWidgets, QtGui
class SciSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.validator = QtGui.QDoubleValidator(self)
self.setMinimum(-inf)
self.setMaximum(inf)
self.setDecimals(1000)
self.precision = 0.001
self._prev_value = float(self.lineEdit().text())
def valueFromText(self, text: str) -> float:
try:
self._prev_value = float(self.cleanText())
except ValueError:
pass
return self._prev_value
def textFromValue(self, value: float) -> str:
if value == 0:
return '0'
else:
return f'{value:.3e}'
def stepBy(self, step: int):
self._prev_value = self.value()
new_value = self._prev_value
if new_value != 0.0:
new_value *= 10**(step/19.)
else:
new_value = 0.001
self.setValue(new_value)
self.lineEdit().setText(f'{new_value:.3e}')
def validate(self, text, pos):
return self.validator.validate(text, pos)

777
src/gui_qt/lib/stuff.py Normal file
View File

@ -0,0 +1,777 @@
import random
import time
import numpy as np
from ..Qt import QtWidgets, QtCore, QtGui
__all__ = ['Game']
class Game(QtWidgets.QDialog):
def __init__(self, mode, parent=None):
super().__init__(parent=parent)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(3, 3, 3, 3)
self.label = QtWidgets.QLabel(self)
layout.addWidget(self.label)
self.startbutton = QtWidgets.QPushButton('Start', self)
self.startbutton.clicked.connect(self.start)
layout.addWidget(self.startbutton)
if mode == 'tetris':
self._setup_tetris()
else:
self._setup_snake()
layout.addWidget(self.board)
self.setStyleSheet("""
Board {
border: 5px solid black;
}
QPushButton {
font-weight: bold;
}
""")
self.setLayout(layout)
self.board.new_status.connect(self.new_message)
def _setup_tetris(self):
self.board = TetrisBoard(self)
self.setGeometry(200, 100, 276, 546+self.startbutton.height()+self.label.height())
self.setWindowTitle('Totally not Tetris')
def _setup_snake(self):
self.board = SnakeBoard(self)
self.setGeometry(200, 100, 406, 406+self.startbutton.height()+self.label.height())
self.setWindowTitle('Snakey')
def start(self):
self.board.start()
@QtCore.pyqtSlot(str)
def new_message(self, msg: str):
self.label.setText(msg)
class Board(QtWidgets.QFrame):
new_status = QtCore.pyqtSignal(str)
SPEED = 1000
WIDTH = 10
HEIGHT = 10
def __init__(self, parent=None):
super().__init__(parent=parent)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.next_move)
self.score = 0
self._speed = self.SPEED
self._ispaused = False
self._isdead = True
self.setFrameStyle(QtWidgets.QFrame.Box | QtWidgets.QFrame.Raised)
self.setLineWidth(3)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
def _init_game(self):
raise NotImplementedError
@property
def cellwidth(self):
return int(self.contentsRect().width() // self.WIDTH)
# square height
@property
def cellheight(self):
return int(self.contentsRect().height() // self.HEIGHT)
def start(self):
if self._isdead:
self._init_game()
self.new_status.emit(f'Score: {self.score}')
self.timer.start(self._speed)
self._isdead = False
self.setFocus()
def stop(self, msg):
self.new_status.emit(f'Score {self.score} // ' + msg)
self._isdead = True
self.timer.stop()
def pause(self):
if self._ispaused and not self._isdead:
self.new_status.emit(f'Score {self.score}')
self.timer.start(self._speed)
else:
self.new_status.emit(f'Score {self.score} // Paused')
self.timer.stop()
self._ispaused = not self._ispaused
def next_move(self):
raise NotImplementedError
def draw_square(self, painter, x, y, color):
color = QtGui.QColor(color)
painter.fillRect(x+1, y+1, self.cellwidth-2, self.cellheight-2, color)
def draw_circle(self, painter, x, y, color):
painter.save()
color = QtGui.QColor(color)
painter.setPen(QtGui.QPen(color))
painter.setBrush(QtGui.QBrush(color))
painter.drawEllipse(x+1, y+1, self.cellwidth, self.cellheight)
painter.restore()
def keyPressEvent(self, evt):
if evt.key() == QtCore.Qt.Key_P:
self.pause()
else:
super().keyPressEvent(evt)
class SnakeBoard(Board):
SPEED = 125
WIDTH = 35
HEIGHT = 35
def __init__(self, parent=None):
super().__init__(parent=parent)
self.snake = [[int(SnakeBoard.WIDTH//2), int(SnakeBoard.HEIGHT//2)],
[int(SnakeBoard.WIDTH//2)+1, int(SnakeBoard.HEIGHT//2)]]
self.current_x_head, self.current_y_head = self.snake[0]
self.direction = 'l'
self.next_direction = []
self._direction_pairs = {'l': 'r', 'u': 'd', 'r': 'l', 'd': 'u'}
self.food = None
self.grow_snake = False
def _init_game(self):
self.snake = [[int(SnakeBoard.WIDTH//2), int(SnakeBoard.HEIGHT//2)],
[int(SnakeBoard.WIDTH//2)+1, int(SnakeBoard.HEIGHT//2)]]
self.current_x_head, self.current_y_head = self.snake[0]
self.direction = 'l'
self.food = None
self.grow_snake = False
self.new_food()
def next_move(self):
self.snake_move()
self.got_food()
self.check_death()
self.update()
def snake_move(self):
if self.next_direction:
turn = self.next_direction.pop()
if self.direction != self._direction_pairs[turn]:
self.direction = turn
if self.direction == 'l':
self.current_x_head -= 1
elif self.direction == 'r':
self.current_x_head += 1
# y increases top to bottom
elif self.direction == 'u':
self.current_y_head -= 1
elif self.direction == 'd':
self.current_y_head += 1
head = (self.current_x_head, self.current_y_head)
self.snake.insert(0, head)
if not self.grow_snake:
self.snake.pop()
else:
self.new_status.emit(f'Score: {self.score}')
self.grow_snake = False
def got_food(self):
head = self.snake[0]
if self.food == head:
self.new_food()
self.grow_snake = True
self.score += 1
def new_food(self):
x = random.randint(3, SnakeBoard.WIDTH-3)
y = random.randint(3, SnakeBoard.HEIGHT-3)
while (x, y) in self.snake:
x = random.randint(3, SnakeBoard.WIDTH-3)
y = random.randint(3, SnakeBoard.HEIGHT-3)
self.food = (x, y)
def check_death(self):
rip_message = ''
is_dead = False
if (self.current_x_head < 1) or (self.current_x_head > SnakeBoard.WIDTH-2) or \
(self.current_y_head < 1) or (self.current_y_head > SnakeBoard.HEIGHT-2):
rip_message = 'Snake found wall :('
is_dead = True
head = self.snake[0]
for snake_i in self.snake[1:]:
if snake_i == head:
rip_message = 'Snake bit itself :('
is_dead = True
break
if is_dead:
self.stop(rip_message)
def keyPressEvent(self, event):
key = event.key()
if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_A):
self.next_direction.append('l')
elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_D):
self.next_direction.append('r')
elif key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_S):
self.next_direction.append('d')
elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_W):
self.next_direction.append('u')
else:
return super().keyPressEvent(event)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
rect = self.contentsRect()
boardtop = rect.bottom() - SnakeBoard.HEIGHT * self.cellheight
boardleft = rect.left()
for pos in self.snake:
self.draw_circle(painter,
int(boardleft+pos[0]*self.cellwidth),
int(boardtop+pos[1]*self.cellheight),
'blue')
if self.food is not None:
self.draw_square(painter,
int(boardleft+self.food[0]*self.cellwidth),
int(boardtop+self.food[1]*self.cellheight),
'orange')
class TetrisBoard(Board):
WIDTH = 10
HEIGHT = 20
SPEED = 300
def __init__(self, parent=None):
super().__init__(parent=parent)
self._shapes = {
1: ZShape,
2: SShape,
3: Line,
4: TShape,
5: Square,
6: MirrorL
}
self._init_game()
def _init_game(self):
self.curr_x = 0
self.curr_y = 0
self.curr_piece = None
self.board = np.zeros((TetrisBoard.WIDTH, TetrisBoard.HEIGHT + 2), dtype=int)
def next_move(self):
if self.curr_piece is None:
self.make_new_piece()
else:
self.move_down()
def try_move(self, piece, x, y):
if piece is None:
return False
if x+piece.x.min() < 0 or x+piece.x.max() >= TetrisBoard.WIDTH:
return False
if y-piece.y.max() < 0 or y-piece.y.min() >= TetrisBoard.HEIGHT+2:
return False
if np.any(self.board[piece.x+x, y-piece.y]) != 0:
return False
if piece != self.curr_piece:
self.curr_piece = piece
self.curr_x = x
self.curr_y = y
self.update()
return True
def make_new_piece(self):
new_piece = self._shapes[random.randint(1, len(self._shapes))]()
startx = TetrisBoard.WIDTH//2
starty = TetrisBoard.HEIGHT+2 - 1 + new_piece.y.min()
if not self.try_move(new_piece, startx, starty):
self.stop('Game over :(')
def move_down(self):
if not self.try_move(self.curr_piece, self.curr_x, self.curr_y-1):
self.final_destination_reached()
def drop_to_bottom(self):
new_y = self.curr_y
while new_y > 0:
if not self.try_move(self.curr_piece, self.curr_x, new_y-1):
break
new_y -= 1
self.final_destination_reached()
def final_destination_reached(self):
x = self.curr_x+self.curr_piece.x
y = self.curr_y-self.curr_piece.y
self.board[x, y] = next(k for k, v in self._shapes.items() if isinstance(self.curr_piece, v))
self.remove_lines()
self.curr_piece = None
self.make_new_piece()
def remove_lines(self):
full_rows = np.where(np.all(self.board, axis=0))[0]
num_rows = len(full_rows)
if num_rows:
temp = np.zeros_like(self.board)
temp[:, :temp.shape[1]-num_rows] = np.delete(self.board, full_rows, axis=1)
self.board = temp
self.score += num_rows
self.new_status.emit(f'Lines: {self.score}')
if self.score % 10 == 0:
self._speed += 0.9
self.timer.setInterval(int(self._speed))
self.update()
def keyPressEvent(self, event):
key = event.key()
if self.curr_piece is None:
return super().keyPressEvent(event)
if key == QtCore.Qt.Key_Left:
self.try_move(self.curr_piece, self.curr_x-1, self.curr_y)
elif key == QtCore.Qt.Key_Right:
self.try_move(self.curr_piece, self.curr_x+1, self.curr_y)
elif key == QtCore.Qt.Key_Down:
if not self.try_move(self.curr_piece.rotate(), self.curr_x, self.curr_y):
self.curr_piece.rotate(clockwise=False)
elif key == QtCore.Qt.Key_Up:
if not self.try_move(self.curr_piece.rotate(clockwise=False), self.curr_x, self.curr_y):
self.curr_piece.rotate()
elif key == QtCore.Qt.Key_Space:
self.drop_to_bottom()
else:
super().keyPressEvent(event)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
rect = self.contentsRect()
board_top = rect.bottom() - TetrisBoard.HEIGHT*self.cellheight
for i in range(TetrisBoard.WIDTH):
for j in range(TetrisBoard.HEIGHT):
shape = self.board[i, j]
if shape:
color = self._shapes[shape].color
self.draw_square(painter,
rect.left() + i*self.cellwidth,
board_top + (TetrisBoard.HEIGHT-j-1)*self.cellheight, color)
if self.curr_piece is not None:
x = self.curr_x + self.curr_piece.x
y = self.curr_y - self.curr_piece.y
for i in range(4):
if TetrisBoard.HEIGHT < y[i]+1:
continue
self.draw_square(painter, rect.left() + x[i] * self.cellwidth,
board_top + (TetrisBoard.HEIGHT-y[i]-1) * self.cellheight,
self.curr_piece.color)
class Tetromino:
SHAPE = np.array([[0], [0]])
color = None
def __init__(self):
self.shape = self.SHAPE
def rotate(self, clockwise: bool = True):
if clockwise:
self.shape = np.vstack((-self.shape[1], self.shape[0]))
else:
self.shape = np.vstack((self.shape[1], -self.shape[0]))
return self
@property
def x(self):
return self.shape[0]
@property
def y(self):
return self.shape[1]
class ZShape(Tetromino):
SHAPE = np.array([[0, 0, -1, -1],
[-1, 0, 0, 1]])
color = 'green'
class SShape(Tetromino):
SHAPE = np.array([[0, 0, 1, 1],
[-1, 0, 0, 1]])
color = 'purple'
class Line(Tetromino):
SHAPE = np.array([[0, 0, 0, 0],
[-1, 0, 1, 2]])
color = 'red'
class TShape(Tetromino):
SHAPE = np.array([[-1, 0, 1, 0],
[0, 0, 0, 1]])
color = 'orange'
class Square(Tetromino):
SHAPE = np.array([[0, 1, 0, 1],
[0, 0, 1, 1]])
color = 'yellow'
class LShape(Tetromino):
SHAPE = np.array([[-1, 0, 0, 0],
[1, -1, 0, 1]])
color = 'blue'
class MirrorL(Tetromino):
SHAPE = np.array([[1, 0, 0, 0],
[-1, -1, 0, 1]])
color = 'lightGray'
class Field(QtWidgets.QToolButton):
NUM_COLORS = {
1: '#1e46a4',
2: '#f28e2b',
3: '#e15759',
4: '#76b7b2',
5: '#59a14f',
6: '#edc948',
7: '#b07aa1',
8: '#ff9da7',
'X': '#ff0000',
}
flag_change = QtCore.pyqtSignal(bool)
def __init__(self, x, y, parent=None):
super(Field, self).__init__(parent=parent)
self.setFixedSize(QtCore.QSize(30, 30))
f = self.font()
f.setPointSize(24)
f.setWeight(75)
self.setFont(f)
self.x = x
self.y = y
self.neighbors = []
self.grid = self.parent()
_size = self.grid.gridsize
for x_i in range(max(0, self.x-1), min(self.x+2, _size[0])):
for y_i in range(max(0, self.y-1), min(self.y+2, _size[1])):
if (x_i, y_i) == (self.x, self.y):
continue
self.neighbors.append((x_i, y_i))
self.is_mine = False
self.is_flagged = False
self.is_known = False
self.has_died = False
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
if self.grid.status == 'finished':
return
if evt.button() == QtCore.Qt.RightButton:
self.set_flag()
elif evt.button() == QtCore.Qt.LeftButton:
self.select()
else:
super().mousePressEvent(evt)
def __str__(self) -> str:
if self.is_mine:
return 'X'
else:
return str(self.visible_mines)
@property
def visible_mines(self) -> int:
return sum(self.grid.map[x_i][y_i].is_mine for x_i, y_i in self.neighbors)
@QtCore.pyqtSlot()
def select(self):
if self.is_flagged:
return
if self.is_mine:
self.setText('X')
num = 'X'
self.has_died = True
self.setStyleSheet(f'color: {self.NUM_COLORS[num]}')
else:
self.is_known = True
self.setEnabled(False)
self.setAutoRaise(True)
num = self.visible_mines
if num != 0:
self.setText(str(num))
self.setStyleSheet(f'color: {self.NUM_COLORS[num]}')
else:
self.grid.reveal_neighbors(self)
self.clicked.emit()
@QtCore.pyqtSlot()
def set_flag(self):
if self.is_flagged:
self.setText('')
else:
self.setText('!')
self.is_flagged = not self.is_flagged
self.has_died = False
self.clicked.emit()
self.flag_change.emit(self.is_flagged)
class QMines(QtWidgets.QMainWindow):
LEVELS = {
'easy': ((9, 9), 10),
'middle': ((16, 16), 40),
'hard': ((16, 30), 99),
'very hard': ((16, 30), 10),
}
def __init__(self, parent=None):
super().__init__(parent=parent)
self.map = []
self.status = 'waiting'
self.open_fields = 0
self.num_flags = 0
self._start = 0
self.gridsize, self.mines = (1, 1), 1
self._init_ui()
def _init_ui(self):
layout = QtWidgets.QGridLayout()
layout.setSpacing(0)
self.central = QtWidgets.QWidget()
self.setCentralWidget(self.central)
layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding),
0, 0)
self.grid_layout = QtWidgets.QGridLayout()
self.grid_layout.setSpacing(0)
self.map_widget = QtWidgets.QFrame()
self.map_widget.setFrameStyle(2)
self.map_widget.setLayout(self.grid_layout)
layout.addWidget(self.map_widget, 1, 1)
self.new_game = QtWidgets.QPushButton('New game')
self.new_game.pressed.connect(self._init_map)
layout.addWidget(self.new_game, 0, 1)
layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding),
2, 2)
self.central.setLayout(layout)
self.timer = QtCore.QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.update_time)
self.timer_message = QtWidgets.QLabel('0 s')
self.statusBar().addWidget(self.timer_message)
self.mine_message = QtWidgets.QLabel(f'0 / {self.mines}')
self.statusBar().addWidget(self.mine_message)
self.dead_message = QtWidgets.QLabel('')
self.statusBar().addWidget(self.dead_message)
self.level_cb = QtWidgets.QComboBox()
self.level_cb.addItems(list(self.LEVELS.keys()))
self.level_cb.currentTextChanged.connect(self._init_map)
self.statusBar().addPermanentWidget(self.level_cb)
self._init_map('easy')
def _init_map(self, lvl: str = None):
if lvl is None:
lvl = self.level_cb.currentText()
self._clear_map()
self.gridsize, self.mines = QMines.LEVELS[lvl]
w, h = self.gridsize
self.map = [[None] * h for _ in range(w)]
for x in range(w):
for y in range(h):
pos = Field(x, y, parent=self)
pos.clicked.connect(self.start_game)
pos.clicked.connect(self.update_status)
pos.flag_change.connect(self.update_flag)
self.grid_layout.addWidget(pos, x+1, y+1)
self.map[x][y] = pos
self.set_mines()
def _clear_map(self):
self.status = 'waiting'
self.open_fields = 0
self.num_flags = 0
self._start = 0
self.map = []
while self.grid_layout.count():
child = self.grid_layout.takeAt(0)
w = child.widget()
if w:
self.grid_layout.removeWidget(w)
w.deleteLater()
else:
self.grid_layout.removeItem(child)
def set_mines(self):
count = 0
w, h = self.gridsize
while count < self.mines:
n = random.randint(0, w * h - 1)
row = n // h
col = n % h
pos = self.map[row][col] # type: Field
if pos.is_mine:
continue
pos.is_mine = True
count += 1
self.status = 'waiting'
self.open_fields = 0
self.num_flags = 0
self._start = 0
self.timer_message.setText('0 s')
self.mine_message.setText(f'0 / {self.mines}')
self.dead_message.setText('')
def reveal_neighbors(self, pos: Field):
for x_i, y_i in pos.neighbors:
field_i = self.map[x_i][y_i] # type: Field
if field_i.isEnabled():
field_i.select()
def start_game(self):
if self.status == 'waiting':
self.status = 'running'
self._start = time.time()
self.timer.start(1000)
def update_time(self):
if self.status == 'running':
self.timer_message.setText(f'{time.time()-self._start:3.0f} s')
def update_status(self):
if self.status == 'finished':
return
pos = self.sender() # type: Field
if pos.is_known:
self.open_fields += 1
if self.open_fields == self.gridsize[0] * self.gridsize[1] - self.mines:
self.timer.stop()
_ = QtWidgets.QMessageBox.information(self, 'Game finished', 'Game finished!!!')
self.status = 'finished'
elif pos.has_died:
dead_cnt = self.dead_message.text()
if dead_cnt == '':
self.dead_message.setText('(Deaths: 1)')
else:
self.dead_message.setText(f'(Deaths: {int(dead_cnt[9:-1])+1})')
@QtCore.pyqtSlot(bool)
def update_flag(self, state: bool):
num_mines = int(self.mine_message.text().split()[0])
if state:
num_mines += 1
else:
num_mines -= 1
self.mine_message.setText(f'{num_mines} / {self.mines}')

102
src/gui_qt/lib/styles.py Normal file
View File

@ -0,0 +1,102 @@
from . import HAS_IMPORTLIB_RESOURCE
from ..Qt import QtGui, QtWidgets
class DarkPalette(QtGui.QPalette):
def __init__(self):
super().__init__()
self.setColor(QtGui.QPalette.Base, QtGui.QColor(42, 42, 42))
self.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(66, 66, 66))
self.setColor(QtGui.QPalette.Window, QtGui.QColor(93, 93, 93))
self.setColor(QtGui.QPalette.ToolTipBase, QtGui.QColor(93, 93, 93))
self.setColor(QtGui.QPalette.Button, QtGui.QColor(93, 93, 93))
self.setColor(QtGui.QPalette.WindowText, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.ToolTipText, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.Text, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.BrightText, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.Shadow, QtGui.QColor(20, 20, 20))
self.setColor(QtGui.QPalette.Dark, QtGui.QColor(35, 35, 35))
self.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218))
self.setColor(QtGui.QPalette.Link, QtGui.QColor(220, 220, 220))
self.setColor(QtGui.QPalette.LinkVisited, QtGui.QColor(108, 180, 218))
# disabled
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Base, QtGui.QColor(80, 80, 80))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Window, QtGui.QColor(80, 80, 80))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(127, 127, 127))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(127, 127, 127))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, QtGui.QColor(127, 127, 127))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(127, 127, 127))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, QtGui.QColor(80, 80, 80))
class LightPalette(QtGui.QPalette):
def __init__(self):
super().__init__()
self.setColor(QtGui.QPalette.Base, QtGui.QColor(237, 237, 237))
self.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(225, 225, 225))
self.setColor(QtGui.QPalette.Window, QtGui.QColor(240, 240, 240))
self.setColor(QtGui.QPalette.ToolTipBase, QtGui.QColor(240, 240, 240))
self.setColor(QtGui.QPalette.Button, QtGui.QColor(240, 240, 240))
self.setColor(QtGui.QPalette.WindowText, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.BrightText, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.ToolTipText, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(0, 0, 0))
self.setColor(QtGui.QPalette.Shadow, QtGui.QColor(20, 20, 20))
self.setColor(QtGui.QPalette.Dark, QtGui.QColor(225, 225, 225))
self.setColor(QtGui.QPalette.Highlight, QtGui.QColor(218, 66, 42))
self.setColor(QtGui.QPalette.Link, QtGui.QColor(0, 162, 232))
self.setColor(QtGui.QPalette.LinkVisited, QtGui.QColor(222, 222, 222))
# disabled
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Base, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Window, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.HighlightedText, QtGui.QColor(115, 115, 115))
self.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Highlight, QtGui.QColor(190, 190, 190))
class MyProxyStyle(QtWidgets.QProxyStyle):
def __init__(self, color):
super().__init__()
if color == 'dark':
self._palette = DarkPalette()
else:
self._palette = LightPalette()
def polish(self, obj):
if isinstance(obj, QtGui.QPalette):
return self._palette
elif isinstance(obj, QtWidgets.QApplication):
if HAS_IMPORTLIB_RESOURCE:
from importlib.resources import path
with path('resources.icons', 'style.qss') as fp:
with fp.open('r') as f:
obj.setStyleSheet(f.read())
else:
from pkg_resources import resource_filename
with open(resource_filename('resources.icons', 'style.qss'), 'r') as f:
obj.setStyleSheet(f.read())
else:
return super().polish(obj)

31
src/gui_qt/lib/tables.py Normal file
View File

@ -0,0 +1,31 @@
from ..Qt import QtWidgets, QtGui, QtCore
class TreeWidget(QtWidgets.QTreeWidget):
def keyPressEvent(self, evt: QtGui.QKeyEvent):
if evt.key() == QtCore.Qt.Key_Space:
sets = []
from_parent = []
for idx in self.selectedIndexes():
if idx.column() != 0:
continue
item = self.itemFromIndex(idx)
if item.parent() is None:
is_selected = item.checkState(0)
self.blockSignals(True)
for i in range(item.childCount()):
child = item.child(i)
# child.setCheckState(0, is_selected)
from_parent.append(child)
self.blockSignals(False)
item.setCheckState(0, QtCore.Qt.Unchecked if is_selected == QtCore.Qt.Checked else QtCore.Qt.Checked)
else:
sets.append(item)
for it in sets:
if it in from_parent:
continue
it.setCheckState(0, QtCore.Qt.Unchecked if it.checkState(0) == QtCore.Qt.Checked else QtCore.Qt.Checked)
else:
super().keyPressEvent(evt)

245
src/gui_qt/lib/undos.py Normal file
View File

@ -0,0 +1,245 @@
import copy
from numpy import argsort
from ..Qt import QtWidgets, QtCore
from ..data.container import FitContainer
from ..graphs.graphwindow import QGraphWindow
class ApodizationCommand(QtWidgets.QUndoCommand):
def __init__(self, data, apod_values: list, apod_func: object):
super().__init__('Apodization')
self.__data = data
self.__y = copy.deepcopy(data.data.y)
self.__apod_func = apod_func
self.__apod_values = apod_values
def undo(self):
# doing a copy (again) to ensure two different objects
self.__data.y = copy.deepcopy(self.__y)
def redo(self):
self.__data.apply('ap', (self.__apod_values, self.__apod_func))
class CutCommand(QtWidgets.QUndoCommand):
def __init__(self, data, *limits):
super().__init__('Apodization')
self.__data = data
self.__data_data = copy.deepcopy(data.data)
self.__limits = limits
def undo(self):
# doing a copy (again) to ensure two different objects
self.__data.data = copy.deepcopy(self.__data_data)
def redo(self):
self.__data.apply('cut', self.__limits)
class PhaseCommand(QtWidgets.QUndoCommand):
def __init__(self, data, ph0: float, ph1: float, pvt: float):
super().__init__('Phase correction')
self.__phase = (ph0, ph1, pvt)
self.__data = data
def undo(self):
self.__data.apply('ph', (-self.__phase[0], -self.__phase[1], self.__phase[2]))
def redo(self):
self.__data.apply('ph', self.__phase)
class ShiftCommand(QtWidgets.QUndoCommand):
def __init__(self, data, value, mode):
super().__init__('Fourier')
self.__data = data
self.__original = copy.deepcopy(self.__data.data)
self.__args = (value, mode)
def undo(self):
self.__data.data = copy.deepcopy(self.__original)
def redo(self):
self.__data.apply('ls', self.__args)
class NormCommand(QtWidgets.QUndoCommand):
def __init__(self, data, mode):
super().__init__('Normalize')
self.__data = data
self.__mode = mode
self.__scale = 1.
def undo(self):
self.__data.y *= self.__scale
self.__data.y_err *= self.__scale
def redo(self):
max_value = self.__data.y.max()
self.__data.apply('norm', (self.__mode,))
self.__scale = max_value / self.__data.y.max()
class CenterCommand(QtWidgets.QUndoCommand):
def __init__(self, data):
super().__init__('Normalize')
self.__data = data
self.__offset = 0.
def undo(self):
_x = self.__data.data.x
_x += self.__offset
self.__data.x = _x
def redo(self):
x0 = self.__data.x[0]
self.__data.apply('center', ())
self.__offset = x0 - self.__data.x[0]
class ZerofillCommand(QtWidgets.QUndoCommand):
def __init__(self, data):
super().__init__('Zero filling')
self.__data = data
def undo(self):
self.__data.apply('zf', (-1,))
def redo(self):
self.__data.apply('zf', (1,))
class BaselineCommand(QtWidgets.QUndoCommand):
def __init__(self, data):
super().__init__('Baseline correction')
self.__baseline = None
self.__data = data
def undo(self):
self.__data.y += self.__baseline
def redo(self):
y_prev = self.__data.y[-1]
self.__data.apply('bl', tuple())
self.__baseline = y_prev - self.__data.y[-1]
class BaselineSplineCommand(QtWidgets.QUndoCommand):
def __init__(self, data, baseline):
super().__init__('Baseline correction')
self.__baseline = baseline
self.__data = data
def undo(self):
self.__data.apply('bls', (-self.__baseline,))
def redo(self):
self.__data.apply('bls', (self.__baseline,))
class FourierCommand(QtWidgets.QUndoCommand):
def __init__(self, data):
super().__init__('Fourier')
self.__data = data
self.__original = copy.deepcopy(self.__data.data)
def undo(self):
self.__data.data = copy.deepcopy(self.__original)
def redo(self):
self.__data.apply('ft', tuple())
class SortCommand(QtWidgets.QUndoCommand):
def __init__(self, data):
super().__init__('Sort')
self.__data = data
self.__sort = None
def undo(self):
self.__data.unsort(self.__sort)
def redo(self):
self.__sort = argsort(argsort(self.__data.data.x))
self.__data.apply('sort', tuple())
class DeleteGraphCommand(QtWidgets.QUndoCommand):
def __init__(self, container: dict, key: str,
signal1: QtCore.pyqtSignal, signal2: QtCore.pyqtSignal):
super().__init__('Delete graph')
# Deletion of GraphWindow is more complicated because C++ object is destroyed
self.__container = container
_value = self.__container[key]
self.__value = self.__container[key].get_state()
self.__key = key
self.__signal_add = signal1
self.__signal_remove = signal2
def redo(self):
self.__signal_remove.emit(self.__key)
del self.__container[self.__key]
def undo(self):
q = QGraphWindow().set_state(self.__value)
self.__container[self.__key] = q
self.__signal_add.emit(self.__key)
class DeleteCommand(QtWidgets.QUndoCommand):
def __init__(self, container, key, signal1, signal2):
super().__init__('Delete data')
self.__container = container
self.__value = self.__container[key]
self.__key = key
self.__signal_add = signal1
self.__signal_remove = signal2
def redo(self):
self.__signal_remove.emit(self.__key)
if isinstance(self.__value, FitContainer):
try:
self.__container[self.__value.fitted_key]._fits.remove(self.__key)
except KeyError:
pass
del self.__container[self.__key]
def undo(self):
self.__container[self.__key] = self.__value
if isinstance(self.__value, FitContainer):
try:
self.__container[self.__value.fitted_key]._fits.append(self.__key)
except KeyError:
pass
self.__signal_add.emit([self.__key], self.__value.graph)
class EvalCommand(QtWidgets.QUndoCommand):
def __init__(self, container: dict, key: str, new_data, title: str):
super().__init__(title)
self.__container = container
self.__value = copy.deepcopy(self.__container[key].data)
self.__replacement = new_data
self.__key = key
def redo(self):
self.__container[self.__key].data = self.__replacement
def undo(self):
self.__container[self.__key].data = self.__value

View File

@ -0,0 +1,132 @@
from __future__ import annotations
from pathlib import Path
from ..Qt import QtWidgets, QtCore, QtGui
from ..lib.codeeditor import CodeEditor
class QUsermodelEditor(QtWidgets.QMainWindow):
modelsChanged = QtCore.pyqtSignal()
def __init__(self, fname: str | Path = None, parent=None):
super().__init__(parent=parent)
self._init_gui()
self.fname = None
self._dir = None
if fname is not None:
self.read_file(fname)
def _init_gui(self):
self.centralwidget = QtWidgets.QWidget(self)
layout = QtWidgets.QVBoxLayout(self.centralwidget)
layout.setContentsMargins(3, 3, 3, 3)
layout.setSpacing(3)
self.edit_field = CodeEditor(self.centralwidget)
font = QtGui.QFont('default')
font.setStyleHint(font.Monospace)
font.setPointSize(10)
self.edit_field.setFont(font)
layout.addWidget(self.edit_field)
self.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(self)
self.setStatusBar(self.statusbar)
self.menubar = self.menuBar()
self.menuFile = QtWidgets.QMenu('File', self.menubar)
self.menubar.addMenu(self.menuFile)
self.menuFile.addAction('Open...', self.open_file, QtGui.QKeySequence.Open)
self.menuFile.addAction('Save', self.overwrite_file, QtGui.QKeySequence.Save)
self.menuFile.addAction('Save as...', self.save_file, QtGui.QKeySequence('Ctrl+Shift+S'))
self.menuFile.addSeparator()
self.menuFile.addAction('Close', self.close, QtGui.QKeySequence.Quit)
self.resize(800, 600)
self.setGeometry(QtWidgets.QStyle.alignedRect(
QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter,
self.size(), QtWidgets.qApp.desktop().availableGeometry()
))
@property
def is_modified(self):
return self.edit_field.document().isModified()
@is_modified.setter
def is_modified(self, val: bool):
self.edit_field.document().setModified(val)
@QtCore.pyqtSlot()
def open_file(self):
overwrite = self.changes_saved
if overwrite:
fname, _ = QtWidgets.QFileDialog.getOpenFileName(directory=str(self._dir))
if fname:
self.read_file(fname)
def read_file(self, fname: str | Path):
self.set_fname_opts(fname)
with self.fname.open('r') as f:
self.edit_field.setPlainText(f.read())
def set_fname_opts(self, fname: str | Path):
self.fname = Path(fname)
self._dir = self.fname.parent
self.setWindowTitle('Edit ' + str(fname))
@property
def changes_saved(self) -> bool:
if not self.is_modified:
return True
ret = QtWidgets.QMessageBox.question(self, 'Time to think',
'<h4><p>The document was modified.</p>\n'
'<p>Do you want to save changes?</p></h4>',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No |
QtWidgets.QMessageBox.Cancel)
if ret == QtWidgets.QMessageBox.Yes:
self.save_file()
if ret == QtWidgets.QMessageBox.No:
self.is_modified = False
return not self.is_modified
@QtCore.pyqtSlot()
def save_file(self):
outfile, _ = QtWidgets.QFileDialog().getSaveFileName(parent=self, caption='Save file', directory=str(self._dir))
if outfile:
with open(outfile, 'w') as f:
f.write(self.edit_field.toPlainText())
self.set_fname_opts(outfile)
self.is_modified = False
return self.is_modified
@QtCore.pyqtSlot()
def overwrite_file(self):
if self.fname is not None:
with self.fname.open('w') as f:
f.write(self.edit_field.toPlainText())
self.modelsChanged.emit()
self.is_modified = False
def closeEvent(self, evt: QtGui.QCloseEvent):
if not self.changes_saved:
evt.ignore()
else:
super().closeEvent(evt)

50
src/gui_qt/lib/utils.py Normal file
View File

@ -0,0 +1,50 @@
from contextlib import contextmanager
from numpy import linspace
from scipy.interpolate import interp1d
from ..Qt import QtGui, QtWidgets, QtCore
@contextmanager
def busy_cursor():
try:
cursor = QtGui.QCursor(QtCore.Qt.ForbiddenCursor)
QtWidgets.QApplication.setOverrideCursor(cursor)
yield
finally:
QtWidgets.QApplication.restoreOverrideCursor()
class RdBuCMap:
# taken from Excel sheet from colorbrewer.org
_rdbu = [
(103, 0, 31),
(178, 24, 43),
(214, 96, 77),
(244, 165, 130),
(253, 219, 199),
(247, 247, 247),
(209, 229, 240),
(146, 197, 222),
(67, 147, 195),
(33, 102, 172),
(5, 48, 97)
]
def __init__(self, vmin=-1., vmax=1.):
self.min = vmin
self.max = vmax
self.spline = [interp1d(linspace(self.max, self.min, num=11), [rgb[i] for rgb in RdBuCMap._rdbu])
for i in range(3)]
def color(self, val: float):
if val > self.max:
col = QtGui.QColor.fromRgb(*RdBuCMap._rdbu[0])
elif val < self.min:
col = QtGui.QColor.fromRgb(*RdBuCMap._rdbu[-1])
else:
col = QtGui.QColor.fromRgb(*(float(self.spline[i](val)) for i in range(3)))
return col