Select Git revision
Jiří Kalvoda authored
qt_util.py 11.91 KiB
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from dataclasses import dataclass
from enum import Enum
import re
import types
import traceback
qtoverride = lambda x: x # only documentation of code
def no_space(lay):
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
return lay
class FlowLayout(QLayout):
# Taken from https://doc.qt.io/qtforpython/examples/example_widgets_layouts_flowlayout.html
# Modificated
def __init__(self, parent=None):
super().__init__(parent)
if parent is not None:
self.setContentsMargins(QMargins(0, 0, 0, 0))
self._item_list = []
def __del__(self):
item = self.takeAt(0)
while item:
item = self.takeAt(0)
@qtoverride
def addItem(self, item):
self._item_list.append(item)
@qtoverride
def count(self):
return len(self._item_list)
@qtoverride
def itemAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list[index]
return None
@qtoverride
def takeAt(self, index):
if 0 <= index < len(self._item_list):
return self._item_list.pop(index)
return None
@qtoverride
def expandingDirections(self):
return Qt.Orientation(0)
@qtoverride
def hasHeightForWidth(self):
return True
@qtoverride
def heightForWidth(self, width):
height = self._do_layout(QRect(0, 0, width, 0), True)
return height
@qtoverride
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self._do_layout(rect, False)
@qtoverride
def sizeHint(self):
return self.minimumSize()
@qtoverride
def minimumSize(self):
size = QSize()
for item in self._item_list:
size = size.expandedTo(item.minimumSize())
size += QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top())
return size
def _do_layout(self, rect, test_only):
@dataclass
class Member:
widget: QWidget
x: int
y: int
width: int
height: int
def set_geometry(self):
self.widget.setGeometry(QRect(QPoint(self.x, self.y), QSize(self.width, self.height)))
members = [[]]
x = rect.x()
y = rect.y()
line_height = 0
spacing = self.spacing()
for item in self._item_list:
hint = item.sizeHint()
style = item.widget().style()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
space_x = 0
space_y = 0
next_x = x + hint.width() + space_x
if next_x - space_x > rect.right() and line_height > 0:
x = rect.x()
y = y + line_height + space_y
next_x = x + hint.width() + space_x
line_height = 0
members.append([])
members[-1].append(Member(item, x, y, hint.width(), hint.height()))
x = next_x
line_height = max(line_height, item.sizeHint().height())
for i in members:
h = max([j.height for j in i])
w = sum([j.width for j in i])
max_w = sum([j.widget.maximumSize().width() for j in i])
could_add = max_w - w
to_add = min(could_add, rect.width()-w)
add_x = 0
for j in i:
j.height = h
j.x += add_x
if could_add:
this_could_add = j.widget.maximumSize().width() - j.width
add = to_add * this_could_add // could_add
to_add -= add
could_add -= this_could_add
j.width += add
add_x += add
if not test_only:
for i in members:
for j in i:
j.set_geometry()
return y + line_height - rect.y()
class QNoArrowScrollArea(QScrollArea):
def __init__(self, parent=None):
super().__init__(parent)
@qtoverride
def keyPressEvent(self, event):
if event.key() in {Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down}:
event.ignore()
else:
super().keyPressEvent(event)
class DragFeature(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.press_event_pos = None
self.press_event_buttons = None
@qtoverride
def mousePressEvent(self, event):
event.accept()
self.press_event_pos = event.pos()
self.press_event_buttons = event.buttons()
@qtoverride
def mouseReleaseEvent(self, event):
if self.press_event_pos is not None:
if (event.pos() - self.press_event_pos).manhattanLength() < QApplication.startDragDistance():
self.clicked(self.press_event_pos, self.press_event_buttons)
self.press_event_pos = None
@qtoverride
def mouseMoveEvent(self, event):
if self.press_event_pos is not None:
if (event.pos() - self.press_event_pos).manhattanLength() >= QApplication.startDragDistance():
self.drag(self.press_event_pos, self.press_event_buttons)
self.press_event_pos = None
event.accept()
def clicked(self, pos, buttons):
pass
def drag(self, pos, buttons):
d = QDrag(self)
mimeData = QMimeData()
d.setMimeData(mimeData)
dropAction = d.exec()
def get_shortcuts(counts_by_priority, dont_use):
max_depths = []
depth = 0
empty = 1
keys = "fjdkslaqpwoeirughtyvncmbxz"
for i in dont_use:
keys = keys.replace(i.lower(), "")
ab_len = len(keys)
for i in counts_by_priority:
while 2 * i > empty: # use at most 1/2 of empty keys
depth += 1
empty *= ab_len
empty -= i
max_depths.append(depth)
to_use = [""]
to_use_index = 0
for count, max_depth in zip(counts_by_priority, max_depths):
for i in range(count):
d = max_depth
upgrade_eat_empty = (ab_len - 1) * ab_len**(depth - d)
if empty >= upgrade_eat_empty:
d -= 1
empty -= upgrade_eat_empty
while len(to_use[to_use_index]) < d:
to_use += [to_use[to_use_index] + x for x in keys]
to_use_index += 1
yield to_use[to_use_index]
to_use_index += 1
reaming_options = len(keys)**max_depth
reaming_shortcuts = sum(counts_by_priority)
class TextShowWidget(QWidget):
def __init__(self, text, parent=None):
super().__init__(parent)
self._lay = no_space(QVBoxLayout(self))
self._text_area = QTextBrowser(self)
self._bar_lay = QHBoxLayout()
self._find_input = QLineEdit(self)
self._find_msg = QLabel(self)
self._find_input.setPlaceholderText("Find")
self._find_msg.setAutoFillBackground(True)
self._bar_lay.addWidget(self._find_input)
self._bar_lay.addWidget(self._find_msg)
self._lay.addWidget(self._text_area)
self._lay.addItem(self._bar_lay)
self.setLayout(self._lay)
self._find_input.setFocus()
self._find_input.textChanged.connect(self.find_changed)
cf = self._text_area.currentCharFormat()
cf.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
self._text_area.setCurrentCharFormat(cf)
self._text_area.setReadOnly(True)
self._text_area.setPlainText(text)
self.html = self._text_area.toHtml()
self.find_error = None
self.find_count = None
def find(self, s):
def parse_html(html):
strings = []
tags = []
i = 0
while True:
l = html.find('<', i)
if l == -1:
strings.append(html[i:])
break
r = html.find('>', l)
strings.append(html[i:l])
tags.append(html[l:r+1])
i = r + 1
return strings, tags
self.clear_find()
try:
find_regex = re.compile(s, re.IGNORECASE)
except re.error as e:
self.find_error = e
self.set_find_msg()
return
find_error = None
self.find_count = 0
def mark_find(s):
m = find_regex.search(s)
if m:
span = m.span()
self.find_count += 1
s = s[:span[0]] + '<span style="background-color:#FFCC00">' + s[span[0]:span[1]] + "</span>" + mark_find(s[span[1]:])
return s
strings, tags = parse_html(self.html)
x = "".join([a+b for (a,b) in zip(tags + [], map(mark_find, strings))])
self._text_area.setHtml(x)
self.set_find_msg()
def clear_find(self):
self.find_count = None
self._text_area.setHtml(self.html)
def set_find_msg(self):
p = QPalette()
if self.find_error:
self._find_msg.setText(str(self.find_error))
p.setColor(self._find_msg.backgroundRole(), QColor(255,100,0))
elif self.find_count is None:
self._find_msg.setText(f"")
p.setColor(self._find_msg.backgroundRole(), QColor(255,255,255))
elif self.find_count > 0:
self._find_msg.setText(f"{self.find_count}")
p.setColor(self._find_msg.backgroundRole(), QColor(0,255,0))
else:
self._find_msg.setText("Not found")
p.setColor(self._find_msg.backgroundRole(), QColor(255,0,0))
self._find_msg.setPalette(p)
@pyqtSlot()
def find_changed(self):
s = self._find_input.text()
if len(s) >= 3:
self.find(s)
else:
self.clear_find()
def key_to_str(key, mod):
if mod != 0 or not (ord('A') <= key and key <= ord('Z')):
c = "?"
else:
c = chr(key).lower()
if mod & Qt.ShiftModifier != 0:
c.upper()
return c
class QLabelOpeningBracket(QLabel):
@qtoverride
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
x = self.width()
y = self.height()
line_x = x//3
text_h = 12
padding = 3
if y > text_h + 2*padding:
painter.drawLine(line_x, padding, x-2, padding)
painter.drawLine(line_x, padding, line_x, y//2-text_h//2)
painter.drawLine(line_x, y-1-padding, line_x, y//2+text_h//2)
painter.drawLine(line_x, y-1-padding, x-2, y-1-padding)
_qt_jobs = set()
class QtJobState(Enum):
WAITING = 1
RUNNING = 2
DONE = 3
CANCELED = 4
class QtJob:
def __init__(self, f):
_qt_jobs.add(self)
self.f = f
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self._run)
self.timer.start(0)
self.state = QtJobState.WAITING
def _clean(self):
_qt_jobs.remove(self)
def _run(self):
if self.state == QtJobState.WAITING:
try:
r = self.f.send(None)
print(r)
except (StopIteration, GeneratorExit):
self._clean()
except Exception:
traceback.print_exc()
self._clean()
else:
self.timer.start(r)
def cancel(self):
if self.state == QtJobState.RUNNING:
raise NotImplementedError()
if self.state == QtJobState.WAITING:
self.state = QtJobState.CANCELED
self.f.close()
@types.coroutine
def qt_job_wait_ms(time_ms):
yield time_ms
def QtJobDecorator(f):
def l(*arg, **kvarg):
return QtJob(f(*arg, **kvarg))
return l