BUGFIX: VFT;
change to src layout
This commit is contained in:
80
src/gui_qt/lib/__init__.py
Normal file
80
src/gui_qt/lib/__init__.py
Normal 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
|
262
src/gui_qt/lib/codeeditor.py
Normal file
262
src/gui_qt/lib/codeeditor.py
Normal 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)
|
80
src/gui_qt/lib/color_dialog.py
Normal file
80
src/gui_qt/lib/color_dialog.py
Normal 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)
|
156
src/gui_qt/lib/configurations.py
Normal file
156
src/gui_qt/lib/configurations.py
Normal 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()
|
55
src/gui_qt/lib/decorators.py
Normal file
55
src/gui_qt/lib/decorators.py
Normal 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
235
src/gui_qt/lib/delegates.py
Normal 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())
|
65
src/gui_qt/lib/expandablewidget.py
Normal file
65
src/gui_qt/lib/expandablewidget.py
Normal 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
406
src/gui_qt/lib/forms.py
Normal 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
380
src/gui_qt/lib/gol.py
Normal 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
216
src/gui_qt/lib/namespace.py
Normal 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))
|
463
src/gui_qt/lib/pg_objects.py
Normal file
463
src/gui_qt/lib/pg_objects.py
Normal 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
110
src/gui_qt/lib/randpok.py
Normal 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}')
|
45
src/gui_qt/lib/spinboxes.py
Normal file
45
src/gui_qt/lib/spinboxes.py
Normal 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
777
src/gui_qt/lib/stuff.py
Normal 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
102
src/gui_qt/lib/styles.py
Normal 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
31
src/gui_qt/lib/tables.py
Normal 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
245
src/gui_qt/lib/undos.py
Normal 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
|
132
src/gui_qt/lib/usermodeleditor.py
Normal file
132
src/gui_qt/lib/usermodeleditor.py
Normal 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
50
src/gui_qt/lib/utils.py
Normal 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
|
Reference in New Issue
Block a user