CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
ExampleApp.py606 linesDownload Raw Back to examples
1import keyword2import os3import pkgutil4import re5import subprocess6import sys7from argparse import Namespace8from collections import OrderedDict9from functools import lru_cache10from typing import Optional11 12import pyqtgraph as pg13from pyqtgraph.Qt import QT_LIB, QtCore, QtGui, QtWidgets14 15app = pg.mkQApp()16 17 18path = os.path.abspath(os.path.dirname(__file__))19sys.path.insert(0, path)20 21import exampleLoaderTemplate_generic as ui_template22import utils23 24# based on https://github.com/art1415926535/PyQt5-syntax-highlighting25 26QRegularExpression = QtCore.QRegularExpression27 28QFont = QtGui.QFont29QColor = QtGui.QColor30QTextCharFormat = QtGui.QTextCharFormat31QSyntaxHighlighter = QtGui.QSyntaxHighlighter32 33 34def charFormat(color, style='', background=None):35    """36    Return a QTextCharFormat with the given attributes.37    """38    _color = pg.functions.mkColor(color)39 40    _format = QTextCharFormat()41    _format.setForeground(_color)42    if 'bold' in style:43        _format.setFontWeight(QFont.Weight.Bold)44    if 'italic' in style:45        _format.setFontItalic(True)46    if background is not None:47        _format.setBackground(pg.mkColor(background))48 49    return _format50 51 52class LightThemeColors:53 54    Red = "#B71C1C"55    Pink = "#FCE4EC"56    Purple = "#4A148C"57    DeepPurple = "#311B92"58    Indigo = "#1A237E"59    Blue = "#0D47A1"60    LightBlue = "#01579B"61    Cyan = "#006064"62    Teal = "#004D40"63    Green = "#1B5E20"64    LightGreen = "#33691E"65    Lime = "#827717"66    Yellow = "#F57F17"67    Amber = "#FF6F00"68    Orange = "#E65100"69    DeepOrange = "#BF360C"70    Brown = "#3E2723"71    Grey = "#212121"72    BlueGrey = "#263238"73 74 75class DarkThemeColors:76 77    Red = "#F44336"78    Pink = "#F48FB1"79    Purple = "#CE93D8"80    DeepPurple = "#B39DDB"81    Indigo = "#9FA8DA"82    Blue = "#90CAF9"83    LightBlue = "#81D4FA"84    Cyan = "#80DEEA"85    Teal = "#80CBC4"86    Green = "#A5D6A7"87    LightGreen = "#C5E1A5"88    Lime = "#E6EE9C"89    Yellow = "#FFF59D"90    Amber = "#FFE082"91    Orange = "#FFCC80"92    DeepOrange = "#FFAB91"93    Brown = "#BCAAA4"94    Grey = "#EEEEEE"95    BlueGrey = "#B0BEC5"96 97 98LIGHT_STYLES = {99    'keyword': charFormat(LightThemeColors.Blue, 'bold'),100    'operator': charFormat(LightThemeColors.Red, 'bold'),101    'brace': charFormat(LightThemeColors.Purple),102    'defclass': charFormat(LightThemeColors.Indigo, 'bold'),103    'string': charFormat(LightThemeColors.Amber),104    'string2': charFormat(LightThemeColors.DeepPurple),105    'comment': charFormat(LightThemeColors.Green, 'italic'),106    'self': charFormat(LightThemeColors.Blue, 'bold'),107    'numbers': charFormat(LightThemeColors.Teal),108}109 110 111DARK_STYLES = {112    'keyword': charFormat(DarkThemeColors.Blue, 'bold'),113    'operator': charFormat(DarkThemeColors.Red, 'bold'),114    'brace': charFormat(DarkThemeColors.Purple),115    'defclass': charFormat(DarkThemeColors.Indigo, 'bold'),116    'string': charFormat(DarkThemeColors.Amber),117    'string2': charFormat(DarkThemeColors.DeepPurple),118    'comment': charFormat(DarkThemeColors.Green, 'italic'),119    'self': charFormat(DarkThemeColors.Blue, 'bold'),120    'numbers': charFormat(DarkThemeColors.Teal),121}122 123 124class PythonHighlighter(QSyntaxHighlighter):125    """Syntax highlighter for the Python language.126    """127    # Python keywords128    keywords = keyword.kwlist129 130    # Python operators131    operators = [132        r'=',133        # Comparison134        r'==', r'!=', r'<', r'<=', r'>', r'>=',135        # Arithmetic136        r'\+', r"-", r'\*', r'/', r'//', r'%', r'\*\*',137        # In-place138        r'\+=', r'-=', r'\*=', r'/=', r'\%=',139        # Bitwise140        r'\^', r'\|', r'&', r'~', r'>>', r'<<',141    ]142 143    # Python braces144    braces = [145        r'\{', r'\}', r'\(', r'\)', r'\[', r'\]',146    ]147 148    def __init__(self, document):149        super().__init__(document)150 151        # Multi-line strings (expression, flag, style)152        self.tri_single = (QRegularExpression("'''"), 1, 'string2')153        self.tri_double = (QRegularExpression('"""'), 2, 'string2')154 155        rules = []156 157        # Keyword, operator, and brace rules158        rules += [(r'\b%s\b' % w, 0, 'keyword')159                  for w in PythonHighlighter.keywords]160        rules += [(o, 0, 'operator')161                  for o in PythonHighlighter.operators]162        rules += [(b, 0, 'brace')163                  for b in PythonHighlighter.braces]164 165        # All other rules166        rules += [167            # 'self'168            (r'\bself\b', 0, 'self'),169 170            # 'def' followed by an identifier171            (r'\bdef\b\s*(\w+)', 1, 'defclass'),172            # 'class' followed by an identifier173            (r'\bclass\b\s*(\w+)', 1, 'defclass'),174 175            # Numeric literals176            (r'\b[+-]?[0-9]+[lL]?\b', 0, 'numbers'),177            (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, 'numbers'),178            (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, 'numbers'),179 180            # Double-quoted string, possibly containing escape sequences181            (r'"[^"\\]*(\\.[^"\\]*)*"', 0, 'string'),182            # Single-quoted string, possibly containing escape sequences183            (r"'[^'\\]*(\\.[^'\\]*)*'", 0, 'string'),184 185            # From '#' until a newline186            (r'#[^\n]*', 0, 'comment'),187        ]188        self.rules = rules189        self.searchText = None190 191    @property192    def styles(self):193        app = QtWidgets.QApplication.instance()194        return DARK_STYLES if app.property('darkMode') else LIGHT_STYLES195 196    def highlightBlock(self, text):197        """Apply syntax highlighting to the given block of text.198        """199        # Do other syntax formatting200        rules = self.rules.copy()201        for expression, nth, format in rules:202            format = self.styles[format]203 204            for n, match in enumerate(re.finditer(expression, text)):205                if n < nth:206                    continue207                start = match.start()208                length = match.end() - start209                self.setFormat(start, length, format)210 211        self.applySearchHighlight(text)212        self.setCurrentBlockState(0)213 214        # Do multi-line strings215        in_multiline = self.match_multiline(text, *self.tri_single)216        if not in_multiline:217            in_multiline = self.match_multiline(text, *self.tri_double)218 219    def match_multiline(self, text, delimiter, in_state, style):220        """Do highlighting of multi-line strings. 221        222        =========== ==========================================================223        delimiter   (QRegularExpression) for triple-single-quotes or 224                    triple-double-quotes225        in_state    (int) to represent the corresponding state changes when 226                    inside those strings. Returns True if we're still inside a227                    multi-line string when this function is finished.228        style       (str) representation of the kind of style to use229        =========== ==========================================================230        """231        # If inside triple-single quotes, start at 0232        if self.previousBlockState() == in_state:233            start = 0234            add = 0235        # Otherwise, look for the delimiter on this line236        else:237            match = delimiter.match(text)238            start = match.capturedStart()239            # Move past this match240            add = match.capturedLength()241 242        # As long as there's a delimiter match on this line...243        while start >= 0:244            # Look for the ending delimiter245            match = delimiter.match(text, start + add)246            end = match.capturedEnd()247            # Ending delimiter on this line?248            if end >= add:249                length = end - start + add + match.capturedLength()250                self.setCurrentBlockState(0)251            # No; multi-line string252            else:253                self.setCurrentBlockState(in_state)254                length = len(text) - start + add255            # Apply formatting256            self.setFormat(start, length, self.styles[style])257            # Highlighting sits on top of this formatting258            # Look for the next match259            match = delimiter.match(text, start + length)260            start = match.capturedStart()261 262        self.applySearchHighlight(text)263 264        # Return True if still inside a multi-line string, False otherwise265        if self.currentBlockState() == in_state:266            return True267        else:268            return False269 270    def applySearchHighlight(self, text):271        if not self.searchText:272            return273        expr = f'(?i){self.searchText}'274        palette: QtGui.QPalette = app.palette()275        color = palette.highlight().color()276        fgndColor = palette.color(palette.ColorGroup.Current,277                                  palette.ColorRole.Text).name()278        style = charFormat(fgndColor, background=color.name())279        for match in re.finditer(expr, text):280            start = match.start()281            length = match.end() - start282            self.setFormat(start, length, style)283 284 285def unnestedDict(exDict):286    """Converts a dict-of-dicts to a singly nested dict for non-recursive parsing"""287    out = {}288    for kk, vv in exDict.items():289        if isinstance(vv, dict):290            out.update(unnestedDict(vv))291        else:292            out[kk] = vv293    return out294 295 296 297class ExampleLoader(QtWidgets.QMainWindow):298    # update qtLibCombo item order to match bindings in the UI file and recreate299    # the templates files if you change bindings.300    bindings = {'PyQt6': 0, 'PySide6': 1, 'PyQt5': 2, 'PySide2': 3}301    modules = tuple(m.name for m in pkgutil.iter_modules())302    def __init__(self):303        QtWidgets.QMainWindow.__init__(self)304        self.ui = ui_template.Ui_Form()305        self.cw = QtWidgets.QWidget()306        self.setCentralWidget(self.cw)307        self.ui.setupUi(self.cw)308        self.setWindowTitle("PyQtGraph Examples")309        self.codeBtn = QtWidgets.QPushButton('Run Edited Code')310        self.codeLayout = QtWidgets.QGridLayout()311        self.ui.codeView.setLayout(self.codeLayout)312        self.hl = PythonHighlighter(self.ui.codeView.document())313        app = QtWidgets.QApplication.instance()314        policy = QtWidgets.QSizePolicy.Policy.Expanding315        self.codeLayout.addItem(QtWidgets.QSpacerItem(100,100, policy, policy), 0, 0)316        self.codeLayout.addWidget(self.codeBtn, 1, 1)317        self.codeBtn.hide()318 319        textFil = self.ui.exampleFilter320        self.curListener = None321        self.ui.exampleFilter.setFocus()322        self.ui.qtLibCombo.addItems(self.bindings.keys())323        self.ui.qtLibCombo.setCurrentIndex(self.bindings[QT_LIB])324 325 326        def onComboChanged(searchType):327            if self.curListener is not None:328                self.curListener.disconnect()329            self.curListener = textFil.textChanged330            # In case the regex was invalid before switching to title search,331            # ensure the "invalid" color is reset332            self.ui.exampleFilter.setStyleSheet('')333            if searchType == 'Content Search':334                self.curListener.connect(self.filterByContent)335            else:336                self.hl.searchText = None337                self.curListener.connect(self.filterByTitle)338            # Fire on current text, too339            self.curListener.emit(textFil.text())340 341        self.ui.searchFiles.currentTextChanged.connect(onComboChanged)342        onComboChanged(self.ui.searchFiles.currentText())343 344        self.itemCache = []345        self.populateTree(self.ui.exampleTree.invisibleRootItem(), utils.examples_)346        self.ui.exampleTree.expandAll()347 348        self.resize(1000,500)349        self.show()350        self.ui.splitter.setSizes([250,750])351 352        self.oldText = self.ui.codeView.toPlainText()353        self.ui.loadBtn.clicked.connect(self.loadFile)354        self.ui.exampleTree.currentItemChanged.connect(self.showFile)355        self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile)356        self.ui.codeView.textChanged.connect(self.onTextChange)357        self.codeBtn.clicked.connect(self.runEditedCode)358        self.updateCodeViewTabWidth(self.ui.codeView.font())359 360    def event(self, event: Optional[QtCore.QEvent]):361        if event is None:362            return super().event(None)363        if event.type() in [364            QtCore.QEvent.Type.ApplicationPaletteChange,365        ]:366            app = pg.mkQApp()367            try:368                darkMode = app.styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark369            except AttributeError:370                palette = app.palette()371                windowTextLightness = palette.color(QtGui.QPalette.ColorRole.WindowText).lightness()372                windowLightness = palette.color(QtGui.QPalette.ColorRole.Window).lightness()373                darkMode = windowTextLightness > windowLightness374            app.setProperty('darkMode', darkMode)375            self.hl = PythonHighlighter(self.ui.codeView.document())376        return super().event(event)377 378    def updateCodeViewTabWidth(self,font):379        """380        Change the codeView tabStopDistance to 4 spaces based on the size of the current font381        """382        fm = QtGui.QFontMetrics(font)383        tabWidth = fm.horizontalAdvance(' ' * 4)384        # the default value is 80 pixels! that's more than 2x what we want.385        self.ui.codeView.setTabStopDistance(tabWidth)386 387    def showEvent(self, event) -> None:388        super(ExampleLoader, self).showEvent(event)389        disabledColor = QColor(QtCore.Qt.GlobalColor.red)390        for name, idx in self.bindings.items():391            disableBinding = name not in self.modules392            if disableBinding:393                item = self.ui.qtLibCombo.model().item(idx)394                item.setData(disabledColor, QtCore.Qt.ItemDataRole.ForegroundRole)395                item.setEnabled(False)396                item.setToolTip(f'{item.text()} is not installed')397 398    def onTextChange(self):399        """400        textChanged fires when the highlighter is reassigned the same document.401        Prevent this from showing "run edited code" by checking for actual402        content change403        """404        newText = self.ui.codeView.toPlainText()405        if newText != self.oldText:406            self.oldText = newText407            self.codeEdited() 408 409    def filterByTitle(self, text):410        self.showExamplesByTitle(self.getMatchingTitles(text))411        self.hl.setDocument(self.ui.codeView.document())412 413    def filterByContent(self, text=None):414        # If the new text isn't valid regex, fail early and highlight the search filter red to indicate a problem415        # to the user416        validRegex = True417        try:418            re.compile(text)419            self.ui.exampleFilter.setStyleSheet('')420        except re.error:421            colors = DarkThemeColors if app.property('darkMode') else LightThemeColors422            errorColor = pg.mkColor(colors.Red)423            validRegex = False424            errorColor.setAlpha(100)425            # Tuple prints nicely :)426            self.ui.exampleFilter.setStyleSheet(f'background: rgba{errorColor.getRgb()}')427        if not validRegex:428            return429        checkDict = unnestedDict(utils.examples_)430        self.hl.searchText = text431        # Need to reapply to current document432        self.hl.setDocument(self.ui.codeView.document())433        titles = []434        text = text.lower()435        for kk, vv in checkDict.items():436            if isinstance(vv, Namespace):437                vv = vv.filename438            filename = os.path.join(path, vv)439            contents = self.getExampleContent(filename).lower()440            if text in contents:441                titles.append(kk)442        self.showExamplesByTitle(titles)443 444    def getMatchingTitles(self, text, exDict=None, acceptAll=False):445        if exDict is None:446            exDict = utils.examples_447        text = text.lower()448        titles = []449        for kk, vv in exDict.items():450            matched = acceptAll or text in kk.lower()451            if isinstance(vv, dict):452                titles.extend(self.getMatchingTitles(text, vv, acceptAll=matched))453            elif matched:454                titles.append(kk)455        return titles456 457    def showExamplesByTitle(self, titles):458        QTWI = QtWidgets.QTreeWidgetItemIterator459        flag = QTWI.IteratorFlag.NoChildren460        treeIter = QTWI(self.ui.exampleTree, flag)461        item = treeIter.value()462        while item is not None:463            parent = item.parent()464            show = (item.childCount() or item.text(0) in titles)465            item.setHidden(not show)466 467            # If all children of a parent are gone, hide it468            if parent:469                hideParent = True470                for ii in range(parent.childCount()):471                    if not parent.child(ii).isHidden():472                        hideParent = False473                        break474                parent.setHidden(hideParent)475 476            treeIter += 1477            item = treeIter.value()478 479    def simulate_black_mode(self):480        """481        used to simulate MacOS "black mode" on other platforms482        intended for debug only, as it manage only the QPlainTextEdit483        """484        # first, a dark background485        c = QtGui.QColor('#171717')486        p = self.ui.codeView.palette()487        p.setColor(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, c)488        p.setColor(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, c)489        self.ui.codeView.setPalette(p)490        # then, a light font491        f = QtGui.QTextCharFormat()492        f.setForeground(QtGui.QColor('white'))493        self.ui.codeView.setCurrentCharFormat(f)494        # finally, override application automatic detection495        app = QtWidgets.QApplication.instance()496        app.setProperty('darkMode', True)497 498    def populateTree(self, root, examples):499        bold_font = None500        for key, val in examples.items():501            item = QtWidgets.QTreeWidgetItem([key])502            self.itemCache.append(item) # PyQt 4.9.6 no longer keeps references to these wrappers,503                                        # so we need to make an explicit reference or else the .file504                                        # attribute will disappear.505            if isinstance(val, OrderedDict):506                self.populateTree(item, val)507            elif isinstance(val, Namespace):508                item.file = val.filename509                if 'recommended' in val:510                    if bold_font is None:511                        bold_font = item.font(0)512                        bold_font.setBold(True)513                    item.setFont(0, bold_font)514            else:515                item.file = val516            root.addChild(item)517 518    def currentFile(self):519        item = self.ui.exampleTree.currentItem()520        if hasattr(item, 'file'):521            return os.path.join(path, item.file)522        return None523 524    def loadFile(self, *, edited=False):525        # make *edited* keyword-only so it is not confused for extra arguments526        # sent by ui signals527        qtLib = self.ui.qtLibCombo.currentText()528        env = dict(os.environ, PYQTGRAPH_QT_LIB=qtLib)529        example_path = os.path.abspath(os.path.dirname(__file__))530        path = os.path.dirname(os.path.dirname(example_path))531        env['PYTHONPATH'] = f'{path}'532        if edited:533            proc = subprocess.Popen([sys.executable, '-'], stdin=subprocess.PIPE, cwd=example_path, env=env)534            code = self.ui.codeView.toPlainText().encode('UTF-8')535            proc.stdin.write(code)536            proc.stdin.close()537        else:538            fn = self.currentFile()539            if fn is None:540                return541            subprocess.Popen([sys.executable, fn], cwd=path, env=env)542 543    def showFile(self):544        fn = self.currentFile()545        text = self.getExampleContent(fn)546        self.ui.codeView.setPlainText(text)547        self.ui.loadedFileLabel.setText(fn)548        self.codeBtn.hide()549 550    @lru_cache(100)551    def getExampleContent(self, filename):552        if filename is None:553            self.ui.codeView.clear()554            return555        if os.path.isdir(filename):556            filename = os.path.join(filename, '__main__.py')557        with open(filename, "r") as currentFile:558            text = currentFile.read()559        return text560 561    def codeEdited(self):562        self.codeBtn.show()563 564    def runEditedCode(self):565        self.loadFile(edited=True)566 567    def keyPressEvent(self, event):568        super().keyPressEvent(event)569        if not (event.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier):570            return571        key = event.key()572        Key = QtCore.Qt.Key573 574        # Allow quick navigate to search575        if key == Key.Key_F:576            self.ui.exampleFilter.setFocus()577            event.accept()578            return579 580        if key not in [Key.Key_Plus, Key.Key_Minus, Key.Key_Underscore, Key.Key_Equal, Key.Key_0]:581            return582        font = self.ui.codeView.font()583        oldSize = font.pointSize()584        if key == Key.Key_Plus or key == Key.Key_Equal:585            font.setPointSize(oldSize + max(oldSize*.15, 1))586        elif key == Key.Key_Minus or key == Key.Key_Underscore:587            newSize = oldSize - max(oldSize*.15, 1)588            font.setPointSize(max(newSize, 1))589        elif key == Key.Key_0:590            # Reset to original size591            font.setPointSize(10)592        self.ui.codeView.setFont(font)593        self.updateCodeViewTabWidth(font)594        event.accept()595 596def main():597    app = pg.mkQApp()598    loader = ExampleLoader()599    loader.ui.exampleTree.setCurrentIndex(600        loader.ui.exampleTree.model().index(0,0)601    )602    pg.exec()603 604if __name__ == '__main__':605    main()606 
Aluode/PerceptionLabPortable · CoolFace