Skip to content
Snippets Groups Projects
Select Git revision
  • 70bf3b632ba180b790dca06bbd829acabb52bdb6
  • master default
2 results

qt_util.py

Blame
  • 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