Skip to content

Instantly share code, notes, and snippets.

@lastforkbender
Last active January 7, 2026 11:29
Show Gist options
  • Select an option

  • Save lastforkbender/ef684691c7bf5bac798517ef07efbdb6 to your computer and use it in GitHub Desktop.

Select an option

Save lastforkbender/ef684691c7bf5bac798517ef07efbdb6 to your computer and use it in GitHub Desktop.
Cecil+ IDE / Spectral Multi-dispatch GUI Works
# Cecil+ Programme Lang /IDE /PyQt5 - Spectral Multi-Dispatching GUI Works - /Py /C /C++ /C#
#
#____________________________________________________________________________________________________________________
#
# CECIL+ CORE FEATURES/SERVICES:
#
# Spectral Multi-Dispatch(SMD) allows dispatch merges along multiple orthogonal axes(type, capability, performance
# classing, resource constraints and much more. A LLM-AI using the Cecil+ language can more easily assemble large
# systems like a game engine by composing small, focused handlers and letting the dispatch system select best-fit
# implementations at runtime or compile time to C. Those technical parsing mechanisms include a built-in AI system
# for Cecil+ SMD composition, Kraken. Below is a more expanded detail of these circumstances:
#
# (1) Spectrum as multi-dimensional capability key
#
# Every service operation is indexed by a vector of axes - e.g. enity type, precision scope, memory budget,
# feature flags, pipe render passes, deterministic/non-deterministic class ordering and etc. The SMD composes
# the most appropriate handlers matching the full spectrum instead of requiring an exact tuple for every
# combination. Handlers can inherit or extend other handlers along specific axes; or as ordered composition
# hooks, weights specific per-axis controlled by the programmer thru keywords such as spawn, migrate and pipe.
# This allows next-gen variable(s) priority with degradation inside matching dedicated handlers with scoring.
#
# (2) Spectrum composability & layered separations
#
# Cecil+ model is built upon deep layering cross-cutting capabilities such as direct/indirect logging, state-
# sync with function params and network transport with piped variable access renderings. With complicated sys
# designs, whereof complex incremental assembled multi-dispatching exist, dedictaed handlers implement those
# concerns without having to duplicate the semantics to class structuring. A already running configuration,
# without having to create every class/function params permutations on the base SMD configurations re-mapped.
# Of this, Kraken's features, specialized variant constraints(registry) easily locates parameter deterministic
# scope and resolve param mishaps via custom spectral object(s) or generating it's own from current objects.
#
# (3) Spectrum parallelism & independent coupling
#
# Instead of coupling concrete types authors, or a LLM-AI can focus upon subsystems independently with spectral
# multi-dispatching. Whereas SMD core registry system composes them @ integration time chosen. Cecil+ keywords
# with spectral swap performance, member, link and superlink always included of a class declaring. Resolving
# profiling & adaptation on the axes or axes slices unit integrations with dispatch loaders. Allowing efficient
# scoring cache dynamics, annotations extension functions/classes within default values already mapped and per
# dimensional indexing vs method-chain composition resolutions more specialized to ahead-of-time error avoiding
# and performance coverage of critical axes fallbacks where needed.
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget, QPlainTextEdit, QTreeView,
QFileSystemModel, QAction, QFileDialog, QMessageBox, QTabWidget, QHBoxLayout,
QLabel, QDialog, QLineEdit, QPushButton, QToolBar, QTextEdit, QStyle, QMenu,
QUndoStack, QUndoCommand, QCheckBox, QComboBox, QGridLayout, QSpacerItem,
QSizePolicy, QListView, QFrame, QAbstractItemView, QTableView, QSplitter,
QToolButton, QListWidget, QListWidgetItem, QHeaderView, QStyledItemDelegate,
QStackedWidget, QProgressBar, QWidgetAction, QTabBar, QStyleOptionButton,
QGraphicsDropShadowEffect, QStatusBar, QScrollArea, QShortcut, QActionGroup)
from PyQt5.QtGui import (QSyntaxHighlighter, QTextCharFormat, QFont, QColor, QTextFormat, QTextCursor, QPainter,
QPalette, QBrush, QLinearGradient, QIcon, QTextDocument, QStandardItemModel, QStandardItem,
QPixmap, QImage, QKeySequence, QFontMetrics, QTextCharFormat, QPen, QPainterPath,
QIntValidator, QPolygon, QPolygonF, QTransform, QPaintEvent, QCursor)
from PyQt5.QtCore import (QRegularExpression, Qt, QRect, QRectF, QSize, QFileInfo, pyqtSignal, QObject, QTimer, QPoint,
QStringListModel, QEvent, QDir, QModelIndex, QThreadPool, QRunnable, pyqtSlot, pyqtProperty,
QPropertyAnimation, QEasingCurve, QCoreApplication)
from datetime import datetime, timezone, timedelta
from typing import List, Optional
from functools import lru_cache
from pathlib import Path
import numpy as np
import zoneinfo
import base64
import math
import time
import gzip
import json
import sys
import os
import re
ICO_ICO, CPL_ICO, INDENT = {}, {}, ' ' * 4
KEYWORDS = ["if", "elif", "else", "loop", "return", "break", "continue", "match", "case", "migrate", "func",
"let", "var", "const", "import", "multiport", "pipe", "operator", "vector", "spawn", "member",
"link", "superlink"]
CTRL_KEYWORDS = {"if", "elif", "else", "loop", "break", "continue", "match", "case", "migrate", "pipe"}
PALETTE = {"keyword": "#7F7F77", "keyword2": "#F50C25", "type": "#BBBB00", "string": "#AAFB88",
"comment": "#AA8B88",
"number": "#59E9EE", "line_highlight": "#505050", "line_number_bg": "#505050",
"line_number_fg": "#7F7F77",
"editor_bg": "#535353", "tab_active": "#59E9EE"}
STRICT_TYPES = ["Int", "Float", "Complex", "String", "Bool", "Array", "Null"]
MENU_STYLESHEET = """
QMenu {padding: 6px; margin: 2px; background: #FBFCFD; border: 0px solid rgba(0, 0, 0, 0.12); border-radius: 0px; color: #373737}
QMenu::icon {width: 36px; margin-left: 6px;}
QMenu {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #59E9EE, stop:0.06 #E9EEF6, stop:0.06 #FBFCFD, stop:1 #FBFCFD)}
QMenu::item {padding: 6px 12px 6px 6px; spacing: 4px;}
QMenu::item:selected {
background: qlineargradient(x:1.0, y1:0, x2:0, y2:1, stop:0 rgba(67, 177, 177, 0.21), stop:8 rgba(67, 188, 188, 0.06)); color: #00AAAA;
margin: 1px; border: 0px solid rgba(0, 0, 0, 0.4); border-radius: 4px}
QMenu::separator {height: 1px; background: rgba(0, 0, 0, 0.06); margin: 6px 4px;}"""
DLG_STYLE_SHEET = """
QDialog QPushButton{border: 0px solid #111; border-radius: 4px; padding: 5px 12px;
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #59E9EE, stop:1 #575757);}
QDialog QPushButton:hover {background: #F3F3F3; color: #535353; border: 1px solid #595959;}
QDialog QPushButton:pressed {background: #E6E6E6; color: #505050; padding-top: 7px;}
QListView::item:hover {background: #636363} QListView::item:selected {background: #535353}"""
MDL_DIR = os.path.dirname(os.path.abspath(__file__))
OPN_CFG_PTH = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'opn_cfg.cfg'
FND_CFG_PTH = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'fnd_cfg.cfg'
FND_LST_PTH = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'fnd_lst.cfg'
RPL_CFG_PTH = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'rpl_cfg.cfg'
TURQUOISE, BG_GRAY, PANEL_GRAY, LIGHT_GRAY, WHITE = "#59E9EE", "#2F3236", "#3B3F43", "#CFCFCF", "#FFFFFF"
def update_cecil_plus_dirs():
fldr_lst = ['Cecil+', '32-bin', '64-bin', '3q-bin', 'ide-settings', 'error-logs', 'lang-bindings',
'lang-definitions',
'lang-task-described', 'lang-exposure', 'lang-task-undescribed', 'lang-segments-assignments',
'lang-segments-classes', 'lang-segments-methods', 'lang-spectral-notations',
'lang-spectral-orthonormals',
'lang-spectral-properties', 'lang-spectral-combinations', 'kraken-ai-allied-scaling',
'kraken-ai-allied-attainments', 'kraken-ai-allied-expressions', 'kraken-ai-allied-constants',
'kraken-ai-allied-sqtpp-disc']
pth = Path(MDL_DIR) / 'Cecil+'
if not os.path.isdir(pth): os.makedirs(pth)
for fldr in fldr_lst:
pth = Path(MDL_DIR) / 'Cecil+' / fldr
if not os.path.isdir(pth): os.makedirs(pth)
update_cecil_plus_dirs()
def get_cecil_plus_fldr_path(fldr):
cp = 'Cecil+'
if fldr == '32-bin': return Path(MDL_DIR) / cp / '32-bin'
elif fldr == '64-bin': return Path(MDL_DIR) / cp / '64-bin'
elif fldr == '3q-bin': return Path(MDL_DIR) / cp / '3q-bin'
elif fldr == 'ide-settings': return Path(MDL_DIR) / cp / 'ide-settings'
elif fldr == 'error-logs': return Path(MDL_DIR) / cp / 'error-logs'
elif fldr == 'lang-bindings': return Path(MDL_DIR) / cp / 'lang-bindings'
elif fldr == 'lang-definitions': return Path(MDL_DIR) / cp / 'lang-definitions'
elif fldr == 'lang-task-described': return Path(MDL_DIR) / cp / 'lang-task-described'
elif fldr == 'lang-exposure': return Path(MDL_DIR) / cp / 'lang-exposure'
elif fldr == 'lang-task-undescribed': return Path(MDL_DIR) / cp / 'lang-task-undescribed'
elif fldr == 'lang-segments-assignments': return Path(MDL_DIR) / cp / 'lang-segments-assignments'
elif fldr == 'lang-segments-classes': return Path(MDL_DIR) / cp / 'lang-segments-classes'
elif fldr == 'lang-segments-methods': return Path(MDL_DIR) / cp / 'lang-segments-methods'
elif fldr == 'lang-spectral-notations': return Path(MDL_DIR) / cp / 'lang-spectral-notations'
elif fldr == 'lang-spectral-orthonormals': return Path(MDL_DIR) / cp / 'lang-spectral-orthonormals'
elif fldr == 'lang-spectral-properties': return Path(MDL_DIR) / cp / 'lang-spectral-properties'
elif fldr == 'lang-spectral-combinations': return Path(MDL_DIR) / cp / 'lang-spectral-combinations'
elif fldr == 'kraken-ai-allied-scaling': return Path(MDL_DIR) / cp / 'kraken-ai-allied-scaling'
elif fldr == 'kraken-ai-allied-attainments': return Path(MDL_DIR) / cp / 'kraken-ai-allied-attainments'
elif fldr == 'kraken-ai-allied-expressions': return Path(MDL_DIR) / cp / 'kraken-ai-allied-expressions'
elif fldr == 'kraken-ai-allied-constants': return Path(MDL_DIR) / cp / 'kraken-ai-allied-constants'
elif fldr == 'kraken-ai-allied-sqtpp-disc': return Path(MDL_DIR) / cp / 'kraken-ai-allied-sqtpp-disc'
def read_file(path):
src = None
with open(path, 'r', encoding='utf-8') as f: src = f.read()
return src
def write_file(path, text):
with open(path, 'w', encoding='utf-8') as f: f.write(text)
def cecil_plus_open_files(parent=None, start_dir=None):
dlg = IDEOpenDialogHorizontal()
if dlg.exec_() == QDialog.Accepted:
pass
def _screen_for_point(pt):
app = QApplication.instance()
try:
scr = app.screenAt(pt)
if scr is None: scr = app.primaryScreen()
except Exception:
scr = app.primaryScreen()
return scr
def build_unicode_icon_png(size, char, c_str):
pix = QPixmap(size, size)
pix.fill(QColor(Qt.transparent))
painter = QPainter(pix)
painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
font = QFont("Segoe UI Symbol")
font.setPixelSize(size)
font.setWeight(QFont.Normal)
painter.setFont(font)
rect = QRect(0, 0, size, size)
painter.setPen(QPen(QColor(c_str)))
painter.drawText(rect, Qt.AlignCenter, char)
painter.end()
return pix
def save_unicode_ico_png(ico_pth, ico_nm, sz, uni_c, c_str):
ico_pth_ico = os.path.join(ico_pth, ico_nm)
pix = build_unicode_icon_png(sz, uni_c, c_str)
pix.save(ico_pth_ico, "PNG")
def recolor_page_icon_for_treeview():
src_pix = ICO_ICO["page"]
dst = QPixmap(src_pix.size())
dst.fill(Qt.transparent)
p = QPainter(dst)
p.drawPixmap(0, 0, src_pix)
p.setCompositionMode(QPainter.CompositionMode_SourceIn)
p.fillRect(dst.rect(), QColor(255, 255, 255))
p.end()
return dst
def _pixmap_from_chunks(chunks):
gz = base64.b64decode("".join(chunks))
png_bytes = gzip.decompress(gz)
pix = QPixmap()
pix.loadFromData(png_bytes)
return pix
def preload_cpl_icons():
CPL_B64 = {"ki_resolver": ("H4sIAAAAAAAC/wE9DMLziVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IA",
"rs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAvSSURBVGhDtVoJU1RX",
"FvafRRBRccEFR01ck2iNmdKUYzI1mcliqpyZmkxi1aScsUboBZBFcKF3abpBFgUUBUVBZVFUXJCG",
"pun19fJNfef1bRFRwCan6lS/5b77zne2e+55vWx5sRXLS+rm5Y+K3+TlM5jnK41WYRlfXIc8g35O",
"zi2xCKtz8goD2fIW87kCs12Yx+T1ZQ4UVVySX3WPvNJkwzIO6B+bxMOJgM6TAQxPBPB0Kogn/mk8",
"C4QwGgjJbyAaQyCqyfVXoQh8oQjGghFMRKIy/tFkQMbxvmJfKIqwFsdUNIaXwTAe+6cxFgzjxXQ4",
"/RvCZDgKXziK59MhuT4w7s8wZRqemEL/2ARGJqcx7PPjwbhfrhWWO7Esp8SCQd8UxkMRTEU1BGIa",
"JiIxvAiEZMKgpiEaTyCWSCKeJKegpY+TqRQSKWSuReIJxBNJGZtIppBMQX51TiKixeGPxASUPxKV",
"Xwo+HdMQ0jQBOcn74agcU0k8nwhHMRmJyjkVRjkJLM9gwTKa3BeO6MKIIAkBEU0k8NQ/LZNTsFgi",
"IYLymFbQkkkkBEAKKQCplM5JzpFMCUA+Q56OxeVaMEYAUbyc1jU/FYkhGNMElCgnkUwrJyXPUQZ5",
"RzKJaCKJkBYXOWPxBIZ8U8g11BGABS8DIRkcSSTFNGU376Hl4TMRlNfDmiYPcmJqIBCJCQABkbEE",
"X5pEWEuI9mg1ugZdaiwUETeilUW7AiIkiiMAKkXNx+eoSGVhKoVzR+JxkYVjCPjBq0nkGut0FxoP",
"RvA8EBQBKPR3DZ3Yc84L54MRPPYHMRGOIRyPi8aUielm1BYnFSG0OIIaX5IUq0U1WkzXHEGSOT8t",
"QSEJKBxPyFw85j1eF04fK9ekNRhvMi7tAVQkg14AMAAZvDT1dFTDqY472FXrwd5zXoxNh8Q/5QVp",
"s3JyTqLMTtNSMwQSimmiaYJQ12gRao3XlJLo10wKmXhJA+PY8XBEhCZAPsf5Kby4mLiiJoBWGNMA",
"GEyitXhCzMlssrWqATvPenDiyi30PBvDVCwmZuRE1BD9OSI+rvstn+fLtfQcYbmeQIjj0teeB0KZ",
"rES30N8Xl7n0QNaP+QyVwHOJhbRlOJ+KD95bZbbpABhYupmZOVKIp1JoHh7Fl/Y2HHVcQd9Ln2hL",
"XpA2d0TTJ6V/030kU8UT8mJmsGHfFKpuD6Kkqx+ewScZN2E2eRnU3SGUnoNMy5MpHMHqCtUtS8H5",
"XlpDeQIzpLgQsxD9WWUNPiCBk0ziZHsvdtd68a+WbpmcWvNH9aBT6VEsQS2mzcxc3zc2IXF00NKC",
"A3XN+Ka+A9W3BiTvK20TKONJYieWVoqyXjyRDmA9w9F1lIJJPKbVJY3SAu8iAmMwMxZqbg/IiwmU",
"rkKBKbieYvXsQwCjU0F8YW3B/gtN2Fnjwb5aLw5cvIyDdc0SZwslSc/UJFM0gCQgCiYGCs+kklOS",
"zkLvImrheGOXmGrPea8IPx8xbRIAl/4tFZewrswpQHjOFX4xxLeRleZn07wASDT73lov8o02nLp2",
"d/btt4ha+sFzXQTeWO6U30/ONmBHtXvRAOajNSqI30cEwFhYZbRhQ6lD3GQ+OtV5RwTfXuXGxjMu",
"bD7jxHcN15YcwLwxQIpqcdwbm8TJjl7kGa3Yf7EJkfj7Xck78AR7zjViXalDqtACkw1HXVcRW4AL",
"LoZyFwKARBC3no9jtcmGnOI6fO26gurbA/AOjcJ0o1+Yx03DOlf1PJA4WGOyYaXRhnXlThxrvI7B",
"cb/E0dwevXhiWb4gACT69o2nrySgqdX1pQ6sNTuQb7Qi32jBlsp6HHVe1TOOpQWHbK0SxCzXN59x",
"YXu1G7+rqsf5O0OSkpeC1pY6Fg6AxPx7xHFFrLC7xoOD1hYJTgYrfZ0WUpsXri8Et7bcgcIyp4wp",
"MNsE1OXhUcn/2RJdelEASK0Pn2G12Ya1pXYJ1vqBEfy34w5Krvfh+4ZO7DvfKHFSVFmPglIH8owW",
"bChzYo3ZjnVldmyuuIQ/WJplnmyJcy8aADVn6uoXENuq3bKgMD7IN0bH5N439e0iMC2ypZJrgQOr",
"DFZsr6zH5gqXpFX3wJPZUy+aPggAidr7wXMNq0w2CWgWWKTuZ68kuLdS0DMufHzWg9WlTuQaLeKv",
"3Mcyd3Nve6pz/jVlPpIsxA3NYolWKO++j21VbtH0Mc81/K3phmQepk9qnkKyJOd6IBYw22UtoVsV",
"mB044mjLOpjFAuwofAgpEMwyKitR2N21HikdtlZcEisdtrWIRej/BEar8Zl1pXacztIKWQEgEQSF",
"pkDMQr+298J+f0TWghF/UH7t90aw91wjNpa7kG+0SwZjS2WV0SrjsyFxoWwAkA7b2iQ1kufKLP9p",
"75UVmbUU44DWWkE2WPBHR1smfj6EONcyVnTZEK3w2fkmiYWLd4ffEujs7UERnjGxlgFdYpH8nW+y",
"Yf/F7NKpVKNcjrMltTYww7CjMROEe+AxVhr0jl2+yYocg0UCOtdglZgovt73FuiFEudaRo1kS7TC",
"Py7fEO0ySJuHda1SsL+6O0TbqiXJl+aW2JB7Wm8/7j/f+MFWoFI+aB2Yi6QCrfGA8zHfT8cS+Km1",
"GzuqGyTQcw02MGXzPoHSYnklVgH876u981a4c9GSAqAV2ABQDd5Pz3lFeMYGA5Z1ERtRtMCaUjtW",
"mx1yne/nM7+09izalXJUZ26pqHHgsWhfWWEtO8gGK/JVp9pkRZ7Rhi1V9Xrxxw600Srjt5xxiRUX",
"Q5KFsk2jM4lW+NbdKQJTKLbZ+RIWc+vLXbLwFFa4ZE3gHmEVS/JSJwpMOpBPLzQuygoSxEsJgGS5",
"OyQb+RUl1kymYU3EvQArWGagTdLrd2JHTQOKquqxtfKStMqLqtz4ynV1wSCWHMD1p2M4bGtFkewN",
"rFhjdkgJ8UmtG59faMKuGo+sB5sqXdhW2YB9tY1ybUO5S+KhsMz+zgVxLlpyACwdWNBtKHfqBR3d",
"w2zHIVsLynru4++Xb+BPrqsoqnDj4+oG/OjtQuXNARyytkqwc5POrPTn+o7ZU89JvwkAVqMUhi7C",
"XRs1yjT6S2u3lNsNg0+wSUrtBhyouwzbvYfSpdt5tiGzXWXAMw3PR0sO4C/uDglYapFC1/YO4Ii9",
"TY7ZqePnpe5n4zje1CUx8ZWzHY57j+XZEd+UVLQsDz4qsUgJPh8tOYAvLC2icVabLKlpEaZGtiYV",
"CDaNf27txp5ar2w9jzffzHTeLvYOievxea7SP7e8f21YUgDcH392oQkFJmYeO/7ZeivTvDV19ckq",
"TRDO+49gvtmPzy948b9rd1Hc2SdfhUhs33xb3yEpdYWRq7QVx7zX3ulOSwaAvs3dGZkuwBLhRNut",
"zH0Kxq0mg3ZoMgDv4BNY+4fR93ISV0ZeyKcoRSy/d9V69QWuWO9s/L7uMnpf+KThO5OWBIBn8CmK",
"Ki8hp/hi5rsxXeBEa88b49jQ4mch9T2NX1n4VYbfHWZuLZlCv3S0SXeD2YzJgG7JTkbX07E35pRa",
"KBsAPc/H8Wv77cy2koKvL3fg+8Yu2ZEtlNi2V0Rr0aK1twdQ0fMAJ9vvoK7vIbzDo+KSMylrADTr",
"T83doqnCMy587bqKKyPPZw/7zUg2NASgPrYplv7lLH+bizhuaGIKt1745LPnoM+PR5NB+Q48F+nf",
"zGZ+ONc/3fLTFN1psZQBwDqE+9bCclc6j7ODoP8fgdcU0x/X8J7RJsytIpu3eQae6/9t4CLGpq7e",
"2NWrUHXMAo/vU2mSm3LGDK/r/53Qr2f+UyHliE12ciwE1ULH52Q+KaeL62TR4CDW6RyoVsOZrMpe",
"2YykBZI/Z0j9r/dC+at6o4p5n2Nz038C4RzMLJyDymBlyppJFGJ4vQaouakQFoGzmQrPAOCEfIAv",
"4ipKzRIQX6KYdX1OWkuzwc1meYZ7gTRwdZ2AKOQ6s1M624p1MGmrcf408zobwjOtqpj3cqQvdFql",
"P5pTN+n7+fUYPqueV8f6uQU5p+uwPDP3zL/ovH6ePkwWC856T2Y+Oec8r5nP0I147//H8JcLQkKo",
"ZwAAAABJRU5ErkJgghHm+b09DAAA")}
names = list(CPL_B64.keys())
for n in names:
chunks = CPL_B64.get(n)
pix = _pixmap_from_chunks(chunks)
CPL_ICO[n] = QIcon(pix)
CPL_B64 = None
def build_unicode_icon_pngs():
unicode_tpl_lst = [("page", "\U0001F4C4"), ("folder", "\U0001F4C1"), ("open_folder", "\U0001F4C2"),
("save", "\U0001F4BE"), ("close", "\u274C"), ("stop", "\u23F9"), ("undo", "\u21B6"),
("redo", "\u21B7"), ("cut", "\u2702"), ("clipboard", "\U0001F4CB"), ("search", "\U0001F50D"),
("paste", "\U0001F501"), ("checkbox", "\u2611"), ("laptop", "\U0001F4BB"),
("package", "\U0001F4E6"), ("next", "\u25B6"), ("previous", "\u25C0"), ("goto", "\u2192"),
("enter", "\u23CE"), ("bug", "\U0001F41B"), ("fast_forward", "\u23E9"), ("triangle", "\u25B7"),
("rewind", "\u23EA"), ("halt", "\u26D4"), ("hammer_wrench", "\U0001F6E0"),
("wrench", "\U0001F527"), ("memo", "\U0001F4DD"), ("microscope", "\U0001F52C"), ("up_arrow", "\u2B06"),
("down_arrow", "\u2B07"),("pencil", "\u270F"), ("bubble", "\U0001F4AC"), ("gear", "\u2699"),
("question", "\u2753"), ("info", "\u2139"), ("tag", "\U0001F516"), ("star", "\u2B50"),
("small_arrow_forward", "\u27A1"), ("small_arrow_backward", "\u2B05"), ("page_curl", "\U0001F4C3"),
("receipt", "\U0001F9FE"), ("warning", "\u26A0"), ("refresh", "\U0001F504"), ("link", "\U0001F517"),
("check", "\u2714"), ("exit", "\u2716"), ("edit", "\u270E"), ("add", "\u2795"), ("remove", "\u2796"),
("delete", "\U0001F5D1"), ("bullet", "\u2022"), ("dot", "\u25CF"), ("circle", "\u25CB"),
("square", "\u25A0"), ("triangle_up", "\u25B2"), ("triangle_down", "\u25BC"),
("ellipsis", "\u22EF"), ("keyboard", "\u2328"), ("grid", "\U0001F4C5"), ("user", "\U0001F464"),
("small_star", "\u2605")]
ico_pth = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'ico-ico'
os.makedirs(ico_pth)
for u in unicode_tpl_lst: save_unicode_ico_png(ico_pth, f"{u[0]}.png", 32, u[1], "#59E9EE")
for u in unicode_tpl_lst: save_unicode_ico_png(ico_pth, f"gray_{u[0]}.png", 32, u[1], "#999999")
def create_cecil_plus_page_icons():
src_pix = recolor_page_icon_for_treeview()
ltr_color = QColor("#424242")
src_ltr = ("Q+", "P+", "S+")
cpl_page_icons = [QIcon(src_pix)]
for i in range(3):
dst = QPixmap(src_pix.size())
dst.fill(Qt.transparent)
p = QPainter(dst)
p.drawPixmap(0, 0, src_pix)
p.setPen(ltr_color)
font = QFont("Segoe UI")
font.setBold(True)
font.setPointSize(int(21 * 0.55))
p.setFont(font)
rect = dst.rect()
p.drawText(rect, Qt.AlignCenter, src_ltr[i])
p.end()
cpl_page_icons.append(QIcon(dst))
return cpl_page_icons
def addMenuDivider(menu, text):
x = QAction(text, menu)
x.setEnabled(False)
x.setSeparator(False)
x.setToolTip('')
menu.addAction(x)
return x
if os.path.isfile(FND_CFG_PTH):
pth = read_file(FND_CFG_PTH).split(':)^(:')
if len(pth[1]) > 0 and len(pth[2]) > 0:
write_file(FND_CFG_PTH, f'closed:)^(:{pth[1]}:)^(:{pth[2]}')
else: write_file(FND_CFG_PTH, f'closed:)^(:null:)^(:null')
else: write_file(FND_CFG_PTH, f'closed:)^(:null:)^(:null')
class FoldRegion:
def __init__(self, start, end, kind):
self.start = start
self.end = end
self.kind = kind
self.collapsed = False
self._id = f"{start}:{end}:{kind}"
class FoldManager:
def __init__(self, editor):
self.editor = editor
self.regions = []
self._map = {}
self._collapsed_ids = set()
def clear(self):
self.regions.clear()
self._map.clear()
def rebuild(self):
try:
doc = self.editor.document()
self.clear()
block = doc.firstBlock()
bnum = 0
while block.isValid():
text = block.text()
stripped = text.strip()
parts = stripped.split()
if stripped.startswith('func'):
seeker = block
seeker_num = bnum
found_open = False
depth = 0
found_end = None
while seeker.isValid():
line = seeker.text()
for ch in line:
if ch == '{':
found_open = True
depth += 1
elif ch == '}' and found_open:
depth -= 1
if depth == 0:
found_end = seeker_num
break
if found_end is not None:
break
seeker = seeker.next()
seeker_num += 1
if found_open and found_end is not None and found_end > bnum:
r = FoldRegion(bnum, found_end, 'func')
self._register_region(r)
block = block.next()
bnum += 1
for r in self.regions:
if r._id in self._collapsed_ids:
r.collapsed = True
self._apply_visibility_for_region(r)
except Exception:
pass
def _register_region(self, region):
self.regions.append(region)
self._map[region.start] = region
def region_for_block(self, block_num):
return self._map.get(block_num)
def toggle_region_at_block(self, block_num, recursive=False):
r = self.region_for_block(block_num)
if not r:
return
if recursive:
if r.collapsed:
self._expand_recursive(r)
else:
self._collapse_recursive(r)
else:
if r.collapsed:
self._expand(r)
else:
self._collapse(r)
self._update_persisted()
self._finalize()
def fold_all(self):
for r in self.regions:
if not r.collapsed:
self._collapse(r)
self._update_persisted()
self._finalize()
def unfold_all(self):
for r in self.regions:
if r.collapsed:
self._expand(r)
self._update_persisted()
self._finalize()
def _collapse(self, region):
doc = self.editor.document()
for i in range(region.start + 1, region.end + 1):
b = doc.findBlockByNumber(i)
if b.isValid():
b.setVisible(False)
b.setLineCount(0)
region.collapsed = True
def _expand(self, region):
doc = self.editor.document()
for i in range(region.start + 1, region.end + 1):
b = doc.findBlockByNumber(i)
if b.isValid():
b.setVisible(True)
region.collapsed = False
def _collapse_recursive(self, outer_region):
for r in self.regions:
if r.start > outer_region.start and r.end <= outer_region.end:
if not r.collapsed:
self._collapse(r)
if not outer_region.collapsed:
self._collapse(outer_region)
def _expand_recursive(self, outer_region):
for r in self.regions:
if r.start >= outer_region.start and r.end <= outer_region.end:
if r.collapsed:
self._expand(r)
if outer_region.collapsed:
self._expand(outer_region)
def _apply_visibility_for_region(self, region):
if region.collapsed:
self._collapse(region)
else:
self._expand(region)
def _update_persisted(self):
self._collapsed_ids = {r._id for r in self.regions if r.collapsed}
def _finalize(self):
doc = self.editor.document()
doc.markContentsDirty(0, doc.characterCount())
self.editor.updateVisibleRangeHighlight()
if hasattr(self.editor, 'highlighter'):
try:
self.editor.highlighter.invalidate_cache_from(0)
except Exception:
pass
class CecilPlusHighlighterQuantum(QSyntaxHighlighter):
def __init__(self, document):
super().__init__(document)
self.keywordFormat = QTextCharFormat()
self.keywordFormat.setForeground(QColor(PALETTE["keyword"]))
self.keywordFormat.setFontWeight(QFont.Bold)
self.keywordFormat2 = QTextCharFormat()
self.keywordFormat2.setForeground(QColor(PALETTE["keyword2"]))
self.keywordFormat2.setFontWeight(QFont.Bold)
self.typeFormat = QTextCharFormat()
self.typeFormat.setForeground(QColor(PALETTE["type"]))
self.typeFormat.setFontWeight(QFont.Bold)
self.stringFormat = QTextCharFormat()
self.stringFormat.setForeground(QColor(PALETTE["string"]))
self.commentFormat = QTextCharFormat()
self.commentFormat.setForeground(QColor(PALETTE["comment"]))
self.commentFormat.setFontItalic(True)
self.numberFormat = QTextCharFormat()
self.numberFormat.setForeground(QColor(PALETTE["number"]))
self.rules = []
for kw in KEYWORDS:
rx = QRegularExpression(r"\b" + kw + r"\b")
fmt = self.keywordFormat2 if kw in CTRL_KEYWORDS else self.keywordFormat
self.rules.append((rx, fmt))
for t in STRICT_TYPES:
rx = QRegularExpression(r"\b" + t + r"\b")
self.rules.append((rx, self.typeFormat))
self.rules.append((QRegularExpression(r'"[^"\\]*(\\.[^"\\]*)*"'), self.stringFormat))
self.rules.append((QRegularExpression(r"'[^'\\]*(\\.[^'\\]*)*'"), self.stringFormat))
self.rules.append((QRegularExpression(r"\b\d+(\.\d+)?\b"), self.numberFormat))
self.rules.append((QRegularExpression(r"//[^\n]*"), self.commentFormat))
self.start_comment_rx = QRegularExpression(r"/\*")
self.end_comment_rx = QRegularExpression(r"\*/")
self._block_state_cache = {}
def highlightBlock(self, text):
self._highlight_one_block(self.currentBlock(), text)
def _highlight_one_block(self, block, text):
try:
block_num = block.blockNumber()
setFormat = self.setFormat
for rx, fmt in self.rules:
it = rx.globalMatch(text)
while it.hasNext():
m = it.next()
setFormat(m.capturedStart(), m.capturedLength(), fmt)
prev_state = 0
if block_num > 0:
prev_state = self._block_state_cache.get(block_num - 1, 0)
if prev_state != 1:
start_match = self.start_comment_rx.match(text)
start_idx = start_match.capturedStart() if start_match.hasMatch() else -1
else: start_idx = 0
new_state = 0
while start_idx >= 0:
end_match = self.end_comment_rx.match(text, start_idx)
if end_match.hasMatch():
end_index = end_match.capturedStart()
length = end_index - start_idx + end_match.capturedLength()
setFormat(start_idx, length, self.commentFormat)
nm = self.start_comment_rx.match(text, start_idx + length)
start_idx = nm.capturedStart() if nm.hasMatch() else -1
new_state = 0
else:
setFormat(start_idx, len(text) - start_idx, self.commentFormat)
new_state = 1
break
self.setCurrentBlockState(new_state)
self._block_state_cache[block_num] = new_state
except Exception:
pass
def highlight_visible_range(self, start_block, end_block):
try:
doc = self.document()
scan_start = None
for bnum in range(start_block - 1, -2, -1):
if bnum == -1:
scan_start = 0
break
if bnum in self._block_state_cache:
scan_start = bnum + 1
break
if scan_start is None: scan_start = 0
block = doc.findBlockByNumber(scan_start)
block_num = scan_start
while block.isValid() and block_num <= end_block:
text = block.text()
apply_format = block_num >= start_block
if apply_format: self._highlight_one_block(block, text)
else:
prev_state = 0
if block_num > 0: prev_state = self._block_state_cache.get(block_num - 1, 0)
if prev_state != 1:
start_match = self.start_comment_rx.match(text)
start_idx = start_match.capturedStart() if start_match.hasMatch() else -1
else: start_idx = 0
new_state = 0
while start_idx >= 0:
end_match = self.end_comment_rx.match(text, start_idx)
if end_match.hasMatch():
end_index = end_match.capturedStart()
length = end_index - start_idx + end_match.capturedLength()
nm = self.start_comment_rx.match(text, start_idx + length)
start_idx = nm.capturedStart() if nm.hasMatch() else -1
new_state = 0
else:
new_state = 1
break
self._block_state_cache[block_num] = new_state
block = block.next()
block_num += 1
except Exception:
pass
def invalidate_cache_from(self, block_number=0):
keys = [k for k in self._block_state_cache if k >= block_number]
for k in keys:
del self._block_state_cache[k]
class CecilPlusHighlighter(QSyntaxHighlighter):
def __init__(self, document):
super().__init__(document)
self.keywordFormat = QTextCharFormat()
self.keywordFormat.setForeground(QColor(PALETTE["keyword"]))
self.keywordFormat.setFontWeight(QFont.Bold)
self.keywordFormat2 = QTextCharFormat()
self.keywordFormat2.setForeground(QColor(PALETTE["keyword2"]))
self.keywordFormat2.setFontWeight(QFont.Bold)
self.typeFormat = QTextCharFormat()
self.typeFormat.setForeground(QColor(PALETTE["type"]))
self.typeFormat.setFontWeight(QFont.Bold)
self.stringFormat = QTextCharFormat()
self.stringFormat.setForeground(QColor(PALETTE["string"]))
self.commentFormat = QTextCharFormat()
self.commentFormat.setForeground(QColor(PALETTE["comment"]))
self.commentFormat.setFontItalic(True)
self.numberFormat = QTextCharFormat()
self.numberFormat.setForeground(QColor(PALETTE["number"]))
self.types = STRICT_TYPES
self.rules = []
for kw in KEYWORDS:
rx = QRegularExpression(r"\b" + kw + r"\b")
fmt = self.keywordFormat2 if kw in CTRL_KEYWORDS else self.keywordFormat
self.rules.append((rx, fmt))
for t in self.types:
rx = QRegularExpression(r"\b" + t + r"\b")
self.rules.append((rx, self.typeFormat))
self.rules.append((QRegularExpression(r'"[^"\\]*(\\.[^"\\]*)*"'), self.stringFormat))
self.rules.append((QRegularExpression(r"'[^'\\]*(\\.[^'\\]*)*'"), self.stringFormat))
self.rules.append((QRegularExpression(r"\b\d+(\.\d+)?\b"), self.numberFormat))
self.rules.append((QRegularExpression(r"//[^\n]*"), self.commentFormat))
def highlightBlock(self, text):
try:
for rx, fmt in self.rules:
it = rx.globalMatch(text)
while it.hasNext():
m = it.next()
start = m.capturedStart()
length = m.capturedLength()
self.setFormat(start, length, fmt)
startExpr = QRegularExpression(r"/\*")
endExpr = QRegularExpression(r"\*/")
startIndex = 0
if self.previousBlockState() != 1:
match = startExpr.match(text)
startIndex = match.capturedStart() if match.hasMatch() else -1
else: startIndex = 0
while startIndex >= 0:
endMatch = endExpr.match(text, startIndex)
if endMatch.hasMatch():
endIndex = endMatch.capturedStart()
length = endIndex-startIndex+endMatch.capturedLength()
self.setFormat(startIndex, length, self.commentFormat)
nextMatch = startExpr.match(text, startIndex+length)
startIndex = nextMatch.capturedStart() if nextMatch.hasMatch() else -1
self.setCurrentBlockState(0)
else:
self.setFormat(startIndex, len(text) - startIndex, self.commentFormat)
self.setCurrentBlockState(1)
break
except Exception:
pass
class LineNumberArea(QWidget):
def __init__(self, editor):
super().__init__(editor)
self.codeEditor = editor
def sizeHint(self):
return QSize(self.codeEditor.lineNumberAreaWidth(), 0)
def paintEvent(self, event):
self.codeEditor.lineNumberAreaPaintEvent(event)
class ClearableLineEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self._action = QAction(QIcon(ICO_ICO["gray_exit"]), '', self)
self._action.triggered.connect(self.clear)
self.addAction(self._action, QLineEdit.TrailingPosition)
self.setClearButtonEnabled(False)
self._icon_size = 16
self._update_action_visibility(self.text())
self.textChanged.connect(self._update_action_visibility)
self.focused = False
def _update_action_visibility(self, text):
self._action.setVisible(bool(text))
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.clear()
return
super().keyPressEvent(event)
class CodeEditor(QPlainTextEdit):
modifiedChanged = pyqtSignal(bool)
def __init__(self, parent=None):
super().__init__(parent)
self.setFont(QFont("Segoe UI Symbol", 13))
self.setStyleSheet(f"background-color: {PALETTE['editor_bg']};")
self.highlighter = CecilPlusHighlighterQuantum(self.document())
self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.lineNumberArea = LineNumberArea(self)
self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
self.updateRequest.connect(self.updateLineNumberArea)
self.cursorPositionChanged.connect(self.highlightCurrentLine)
self.textChanged.connect(self._onTextChanged)
self._saved = True
self.updateLineNumberAreaWidth(0)
self.highlightCurrentLine()
self.rect_selecting = False
self.rect_anchor = None
self.rect_rect = QRect()
self.viewport().installEventFilter(self)
self.fold_manager = FoldManager(self)
self.fold_gutter_width = 16
self._fold_hover_block = None
doc = self.document()
doc.contentsChange.connect(self._onDocumentContentsChange)
doc.blockCountChanged.connect(self._onBlockCountChanged)
doc.contentsChange.connect(self._on_doc_for_folds)
doc.blockCountChanged.connect(self._on_doc_for_folds)
self.updateVisibleRangeHighlight()
# ---- Context Menu ----
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.act_normal = QAction(" Scrollbar Normal width", self, checkable=True)
self.act_double = QAction(" Scrollbar Double width", self, checkable=True)
width_group = QActionGroup(self)
width_group.setExclusive(True)
width_group.addAction(self.act_normal)
width_group.addAction(self.act_double)
self.act_normal.setChecked(True)
self.act_normal.triggered.connect(self.apply_normal_width_scrollbar)
self.act_double.triggered.connect(self.apply_double_width_scrollbar)
self.base_scrollbar_width = 12
self.double_scrollbar_width = self.base_scrollbar_width * 2
self.transparent = False
self.apply_double_width_scrollbar()
# ----------------------
self._on_doc_for_folds()
def show_context_menu(self, pos):
menu = QMenu(self)
menu.addSeparator()
menu.addAction(self.act_normal)
menu.addAction(self.act_double)
menu.addSeparator()
menu.exec_(self.mapToGlobal(pos))
def build_scrollbar_stylesheet(self, width_px: int, transparent: bool):
thumb_alpha = "60" if transparent else "FF"
track_alpha = "20" if transparent else "30"
hover_alpha = "80" if transparent else "CC"
return f"""QPlainTextEdit {{ }}
QPlainTextEdit QScrollBar:vertical {{width: {width_px}px;
background: rgba(0, 0, 0, {int(track_alpha, 16) / 255.0}); margin: 0px;}}
QPlainTextEdit QScrollBar::handle:vertical {{
background: rgba(100, 100, 100, {int(thumb_alpha, 16) / 255.0});
border-radius: {max(2, width_px // 4)}px; min-height: 20px;}}
QPlainTextEdit QScrollBar::handle:vertical:hover {{
background: rgba(255, 255, 255, {int(hover_alpha, 16) / 255.0});}}
QPlainTextEdit QScrollBar::add-line:vertical, QPlainTextEdit QScrollBar::sub-line:vertical {{height: 0px;}}
QPlainTextEdit QScrollBar::add-page:vertical, QPlainTextEdit QScrollBar::sub-page:vertical {{background: transparent;}}"""
def apply_normal_width_scrollbar(self):
self.act_normal.setChecked(True); self.act_double.setChecked(False)
w = self.base_scrollbar_width
self.setStyleSheet(self.build_scrollbar_stylesheet(w, self.transparent))
def apply_double_width_scrollbar(self):
self.act_double.setChecked(True); self.act_normal.setChecked(False)
w = self.double_scrollbar_width
self.setStyleSheet(self.build_scrollbar_stylesheet(w, self.transparent))
def _on_doc_for_folds(self, *args):
old_ids = set(self.fold_manager._collapsed_ids)
self.fold_manager.rebuild()
for r in self.fold_manager.regions:
if r.collapsed:
self.fold_manager._apply_visibility_for_region(r)
self.fold_manager._finalize()
def _make_action(self, text, shortcut, callback):
a = QAction(text, self)
a.setShortcut(shortcut)
a.triggered.connect(callback)
return a
def _fold_current(self, unfold=False, recursive=False):
cursor = self.textCursor()
block_num = cursor.block().blockNumber()
region = self.fold_manager.region_for_block(block_num)
if not region:
for r in self.fold_manager.regions:
if r.start < block_num <= r.end:
region = r
break
if not region:
return
if unfold:
if recursive:
self.fold_manager._expand_recursive(region)
else:
self.fold_manager._expand(region)
else:
if recursive:
self.fold_manager._collapse_recursive(region)
else:
self.fold_manager._collapse(region)
self.fold_manager._update_persisted()
self.fold_manager._finalize()
def _fold_all(self):
self.fold_manager.fold_all()
def _unfold_all(self):
self.fold_manager.unfold_all()
def _onTextChanged(self):
if not self._saved:
return
self._saved = False
self.modifiedChanged.emit(True)
def markSaved(self):
self._saved = True
self.modifiedChanged.emit(False)
def isSaved(self):
return self._saved
def lineNumberAreaWidth(self):
digits = len(str(max(1, self.blockCount())))
space = 8 + self.fontMetrics().horizontalAdvance('9') * digits
return space + getattr(self, 'fold_gutter_width', 16)
def resizeEvent(self, e):
super().resizeEvent(e)
cr = self.contentsRect()
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))
self.updateVisibleRangeHighlight()
def updateLineNumberAreaWidth(self, _):
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
def updateLineNumberArea(self, rect, dy):
if dy: self.lineNumberArea.scroll(0, dy)
else: self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
if rect.contains(self.viewport().rect()): self.updateLineNumberAreaWidth(0)
self.updateVisibleRangeHighlight()
def lineNumberAreaPaintEvent(self, event):
painter = QPainter(self.lineNumberArea)
painter.fillRect(event.rect(), QColor(PALETTE.get("line_number_bg", "#2B2B2B")))
fold_w = getattr(self, 'fold_gutter_width', 16)
painter.fillRect(0, event.rect().top(), fold_w, event.rect().height(), QColor(PALETTE.get("fold_gutter_bg", "#2A2A2A")))
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
bottom = top + int(self.blockBoundingRect(block).height())
fm = self.fontMetrics(); nums_x = fold_w
hover_block = getattr(self, '_fold_hover_block', None)
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number = str(blockNumber + 1)
painter.setPen(QColor(PALETTE["line_number_fg"]))
painter.drawText(nums_x, top, self.lineNumberArea.width() - nums_x - 6, fm.height(), Qt.AlignRight, number)
region = self.fold_manager.region_for_block(blockNumber)
if region and region.end > blockNumber:
center_x = fold_w // 2
center_y = top + fm.height() // 2
size = 14
painter.setPen(Qt.NoPen)
is_hover = (hover_block == blockNumber)
bg_color = QColor(PALETTE.get("fold_hover_bg", "#FFFFFF")) if is_hover else QColor(PALETTE.get("fold_gutter_bg", "#2A2A2A"))
painter.setBrush(bg_color)
rect = QRect(center_x - size, center_y - size, size * 2, size * 2)
painter.drawRoundedRect(rect, 2, 2)
if region.collapsed:
points = [QPoint(center_x - 3, center_y - 4), QPoint(center_x - 3, center_y + 4),
QPoint(center_x + 3, center_y)]
else:
points = [QPoint(center_x - 4, center_y - 2), QPoint(center_x + 4, center_y - 2),
QPoint(center_x, center_y + 4)]
painter.setBrush(QColor(PALETTE.get("fold_icon_fg", "#59E9EE")))
painter.drawPolygon(QPolygon(points))
if not region.collapsed:
line_x = center_x
line_y1 = center_y + size // 2
end_block = self.document().findBlockByNumber(region.end)
if end_block.isValid():
end_top = int(self.blockBoundingGeometry(end_block).translated(self.contentOffset()).top())
line_y2 = end_top + int(self.blockBoundingRect(end_block).height())
painter.setPen(QColor(PALETTE.get("fold_guide", "#FFFFFF")))
painter.drawLine(line_x, line_y1, line_x, line_y2)
block = block.next()
top = bottom
bottom = top + int(self.blockBoundingRect(block).height())
blockNumber += 1
painter.end()
def highlightCurrentLine(self):
extraSelections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
lineColor = QColor(PALETTE["line_highlight"])
selection.format.setBackground(lineColor)
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extraSelections.append(selection)
self.setExtraSelections(extraSelections)
def keyPressEvent(self, e):
if e.key() in (Qt.Key_Return, Qt.Key_Enter):
cursor = self.textCursor()
blockText = cursor.block().text()
indent = ''
for ch in blockText:
if ch in [' ', '\t']: indent += ch
else:
break
super().keyPressEvent(e)
self.insertPlainText(indent)
return
if e.text() in ['{', '(', '[', '"', "'"]:
pairs = {'{': '}', '(': ')', '[': ']', '"': '"', "'": "'"}
closing = pairs.get(e.text())
super().keyPressEvent(e)
self.insertPlainText(closing)
self.moveCursor(QTextCursor.PreviousCharacter)
return
super().keyPressEvent(e)
def eventFilter(self, obj, event):
if obj is self.viewport():
if event.type() in (QEvent.Paint, QEvent.Resize, QEvent.UpdateRequest): self.updateVisibleRangeHighlight()
return super().eventFilter(obj, event)
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
local_x = e.pos().x()
if local_x < self.lineNumberArea.width():
y = e.pos().y()
block = self.firstVisibleBlock()
top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
bnum = block.blockNumber()
while block.isValid():
bottom = top + int(self.blockBoundingRect(block).height())
if y >= top and y < bottom:
region = self.fold_manager.region_for_block(bnum)
if region and region.start == bnum:
modifiers = QApplication.keyboardModifiers()
recursive = bool(modifiers & Qt.ShiftModifier)
self.fold_manager.toggle_region_at_block(bnum, recursive=recursive)
self.updateLineNumberAreaWidth(0)
self.viewport().update()
return
break
block = block.next()
bnum += 1
top = bottom
self._on_doc_for_folds()
return super().mousePressEvent(e)
def updateVisibleRangeHighlight(self):
block = self.firstVisibleBlock()
if not block.isValid():
return
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
height = self.viewport().height()
start_block = block.blockNumber()
b, y = block, top
end_block = start_block
while b.isValid() and y <= top + height:
end_block = b.blockNumber()
y += self.blockBoundingRect(b).height()
b = b.next()
last_block_num = self.blockCount() - 1
if end_block > last_block_num: end_block = last_block_num
if self.highlighter: self.highlighter.highlight_visible_range(start_block, end_block)
def _onDocumentContentsChange(self, position, charsRemoved, charsAdded):
block = self.document().findBlock(position)
if block.isValid(): self.highlighter.invalidate_cache_from(block.blockNumber())
else: self.highlighter.invalidate_cache_from(0)
self.updateVisibleRangeHighlight()
def _onBlockCountChanged(self, _):
self.highlighter.invalidate_cache_from(0)
self.updateVisibleRangeHighlight()
class GridPopup(QFrame):
itemSelected = pyqtSignal(str)
def __init__(self, owner=None, items=None, cols=3, cell_size=120, margin=6):
super().__init__(None, Qt.Popup | Qt.FramelessWindowHint)
self._owner = owner
self.items = items or []
self.cols = max(1, cols)
self.cell_size = cell_size
self.margin = margin
layout = QGridLayout(self)
layout.setSpacing(6)
layout.setContentsMargins(margin, margin, margin, margin)
self.setObjectName("gridPopup")
self.setStyleSheet("""#gridPopup{background: #686868; border-radius: 1px;
border: 1px solid #59E9EE; padding: 6px}""")
self._build_buttons()
self._close_on_focus_lost = True
def _build_buttons(self):
layout = self.layout()
while layout.count():
it = layout.takeAt(0)
w = it.widget()
if w: w.deleteLater()
font = QFont("Segoe UI", 9)
fm = QFontMetrics(font)
for idx, text in enumerate(self.items):
btn = QPushButton(self)
btn.setFont(font)
elided = fm.elidedText(text, Qt.ElideRight, max(10, self.cell_size - 24))
btn.setText(elided)
btn.setFixedSize(self.cell_size, 36)
btn.setCursor(Qt.PointingHandCursor)
btn.setFocusPolicy(Qt.NoFocus)
btn.setStyleSheet("""QPushButton{border: 0px solid #59E9EE; border-radius: 4px; padding: 5px 12px;
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #626262, stop:1 #424242);}
QPushButton:hover {background: #F9F9F9; color: #59E9EE; border: 1px solid #59E9EE;}
QPushButton:pressed {background: #E6E6E6; color: #505050; padding-top: 7px;}""")
btn.clicked.connect(lambda _, t=text: self._on_choose(t))
r = idx // self.cols
c = idx % self.cols
layout.addWidget(btn, r, c)
def updateItems(self, items, cols=None, cell_size=None):
if cols is not None:
self.cols = max(1, cols)
if cell_size is not None:
self.cell_size = cell_size
self.items = items or []
self._build_buttons()
self.adjustSize()
def _on_choose(self, text):
if not self.isVisible():
return
self.itemSelected.emit(text)
self.close()
def showAt(self, widget):
if widget is None:
gw = QPoint(0, 0)
widget_h = 0
else:
try:
gw = widget.mapToGlobal(QPoint(0, 0))
widget_h = widget.height()
except Exception:
gw = QPoint(0, 0)
widget_h = 0
x = gw.x(); y = gw.y() + widget_h + 4
self.adjustSize()
screen = _screen_for_point(gw)
screen_geom = screen.availableGeometry() if screen is not None else QApplication.primaryScreen().availableGeometry()
if x + self.width() > screen_geom.right():
x = max(screen_geom.left(), screen_geom.right() - self.width() - 8)
if y + self.height() > screen_geom.bottom():
alt_y = gw.y() - self.height() - 4
if alt_y >= screen_geom.top(): y = alt_y
try:
self.move(int(x), int(y))
except Exception:
self.move(screen_geom.left(), screen_geom.top())
QTimer.singleShot(0, self.show)
def sizeHint(self):
rows = (len(self.items) + self.cols - 1) // self.cols
w = self.cols * self.cell_size + (self.cols - 1) * 6 + self.margin * 2
h = rows * 36 + (rows - 1) * 6 + self.margin * 2
return QSize(w, h)
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Escape: self.close()
else: super().keyPressEvent(ev)
def focusOutEvent(self, ev):
super().focusOutEvent(ev)
QTimer.singleShot(60, self._maybe_close_on_focus_lost)
def _maybe_close_on_focus_lost(self):
if not self.isActiveWindow() and not any(w.hasFocus() for w in self.findChildren(QWidget)):
self.close()
class CecilPlusTGridBox(QWidget):
submitted = pyqtSignal(str)
def __init__(self, parent=None, placeholder="MergeOperator", grid_items=None):
super().__init__(parent)
self.setFixedWidth(410)
self.setFixedHeight(56)
self.line = QLineEdit(self)
self.ctx_helper = MiniContextMenu(self, fg_color="#484848", bg_color="#89E9EE")
self.ctx_helper.attach(self.line)
self.line.setEchoMode(QLineEdit.Normal)
self.line.setPlaceholderText(placeholder)
self.line.setFrame(False)
self.line.setGeometry(16, 10, 332, 36)
self.line.setFont(QFont("Segoe UI", 11))
self.line.setObjectName("CPGT")
self.line.setStyleSheet("""QLineEdit#CPGT {background: transparent; border: none;
border-bottom: 1px solid #59E9EE;}""")
self.line.returnPressed.connect(self._on_return)
self.grid_btn = QToolButton(self)
self.grid_btn.setCursor(Qt.PointingHandCursor)
self.grid_btn.setGeometry(360, 10, 36, 36)
self._set_grid_icon()
self.grid_btn.clicked.connect(self._on_grid_clicked)
self.grid_btn.setStyleSheet("QToolButton {background: rgba(255,255,255,12); border-radius: 8px;}")
self._focus_progress = 0.0
self._focus_anim = QPropertyAnimation(self, b"focusProgress")
self._focus_anim.setDuration(160)
self.line.installEventFilter(self)
default_items = ["merge_opA", "merge_opB", "merge_opC",
"merge_opD", "merge_opE", "merge_opF",
"merge_opG", "merge_opH", "merge_opI"]
self.grid_items = grid_items or default_items
self.popup = GridPopup(None, items=self.grid_items, cols=3, cell_size=144)
self.popup.itemSelected.connect(self._on_grid_selected)
self.setFocusProxy(self.line)
def _set_grid_icon(self):
app = QApplication.instance()
screen = app.primaryScreen()
dpr = screen.devicePixelRatio() if screen is not None else 1.0
size = int(24 * dpr)
pix = QPixmap(size, size)
pix.setDevicePixelRatio(dpr)
pix.fill(Qt.transparent)
p = QPainter(pix)
p.setRenderHint(QPainter.Antialiasing)
pen = QPen(QColor(180, 180, 180))
pen.setWidthF(1.6)
p.setPen(pen)
s, gap = 4, 4
for row in range(3):
for col in range(3):
rect = QRectF(col*(s+gap)+2, row*(s+gap)+2, s, s)
p.fillRect(rect, QColor(220, 220, 220, 230))
p.end()
self.grid_btn.setIcon(QIcon(pix))
self.grid_btn.setIconSize(QSize(18, 18))
def _on_grid_clicked(self):
if self.popup.isVisible():
self.popup.close()
return
self.popup.updateItems(self.grid_items, cols=self.popup.cols, cell_size=self.popup.cell_size)
self.popup.showAt(self.grid_btn)
def _on_grid_selected(self, text):
self.line.setText(text)
self.line.setEchoMode(QLineEdit.Normal)
self.submitted.emit(text)
def _on_return(self):
self.submitted.emit(self.line.text())
def eventFilter(self, obj, event):
if obj is self.line:
if event.type() == QEvent.FocusIn:
self._focus_anim.stop()
self._focus_anim.setStartValue(self._focus_progress)
self._focus_anim.setEndValue(1.0)
self._focus_anim.start()
elif event.type() == QEvent.FocusOut:
self._focus_anim.stop()
self._focus_anim.setStartValue(self._focus_progress)
self._focus_anim.setEndValue(0.0)
self._focus_anim.start()
return super().eventFilter(obj, event)
def getFocusProgress(self):
return self._focus_progress
def setFocusProgress(self, v):
self._focus_progress = v
self.update()
focusProgress = pyqtProperty(float, getFocusProgress, setFocusProgress)
def paintEvent(self, e):
try:
r = QRectF(0, 0, self.width(), self.height())
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
path = QPainterPath()
path.addRoundedRect(r.adjusted(0.5, 0.5, -0.5, -2.0), 10.0, 10.0)
pen = QPen(QColor(255, 255, 255, 0.05))
pen.setWidthF(1.6)
painter.setPen(pen)
painter.setBrush(QColor(255, 255, 255, 8))
painter.drawPath(path)
base_y = self.height() - 12
base_pen = QPen(QColor(200, 200, 200, 200))
base_pen.setWidthF(1.0)
painter.setPen(base_pen)
if self._focus_progress > 0.001:
accent = QColor(255, 140, 0, int(230 * self._focus_progress))
acc_pen = QPen(accent)
acc_pen.setWidthF(2.8 * self._focus_progress)
acc_pen.setCapStyle(Qt.RoundCap)
painter.setPen(acc_pen)
total_w = (self.width() - 68)
seg_w = total_w * 0.36
cx = 16 + total_w * 0.5
start = cx - seg_w / 2
end = cx + seg_w / 2
painter.end()
except Exception:
pass
class SPSQuantumButton(QPushButton):
UNCHECK, CHECK = "\u2610", "\u2611"
stateChanged = pyqtSignal(bool, bool, bool, int, bool)
xfactorComputed = pyqtSignal(float)
def __init__(self, label, max_value=999, parent=None):
super().__init__(parent)
self.setCheckable(False)
self.setCursor(Qt.PointingHandCursor)
self.left_checked = False; self.right_checked = False; self.third_flag = False
self._label = label; self.value = 0; self.max_value = max_value
self._prev_vector = self._current_vector()
self.line_edit = QLineEdit(self)
fnt_ln_edt = QFont("Segoe UI", 14)
self.line_edit.setFont(fnt_ln_edt)
self.line_edit.setValidator(QIntValidator(0, self.max_value, self))
self.line_edit.setFixedWidth(60)
self.line_edit.setFrame(False)
self.line_edit.setAlignment(Qt.AlignCenter)
self.line_edit.textChanged.connect(self._on_text_changed)
self.line_edit.installEventFilter(self)
self.ctx_helper = MiniContextMenu(self, fg_color="#484848", bg_color="#89E9EE")
self.ctx_helper.attach(self.line_edit)
self.setMinimumHeight(67)
self.color_normal = QColor(170, 170, 170); self.color_checked = QColor("#59E9EE")
self.glow_base_false = QColor("#555555"); self.glow_base_true = QColor("#59E9EE")
self._last_xfactor = 0.0
def sizeHint(self):
return QSize(360, 67)
def _current_vector(self):
return np.array([1.0 if self.left_checked else 0.0, 1.0 if self.right_checked else 0.0,
1.0 if self.third_flag else 0.0, float(self.value) / max(1.0, self.max_value)], dtype=float)
def eventFilter(self, obj, event):
if obj is self.line_edit:
if event.type() in (QEvent.MouseButtonPress, QEvent.MouseButtonDblClick, QEvent.KeyPress):
return False
return super().eventFilter(obj, event)
def _on_text_changed(self, txt):
self.value = int(txt) if txt.isdigit() else 0
self.update()
self._emit_state()
def set_third_flag(self, value: bool):
self.third_flag = bool(value)
self.update()
self._emit_state()
def _emit_state(self):
expr_result = self.evaluate_expression()
self.stateChanged.emit(self.left_checked, self.right_checked, self.third_flag, self.value, expr_result)
def evaluate_expression(self):
base_min, base_max = 10, 20
min_v = base_min + (-5 if self.left_checked else 0)
max_v = base_max + (10 if self.right_checked else 0)
if self.third_flag:
center = (min_v + max_v) / 2.0; half = max(0, (max_v - min_v) / 4.0)
min_v = int(center - half); max_v = int(center + half)
return (self.value >= min_v) and (self.value <= max_v)
def mousePressEvent(self, event):
r = self.rect()
left_rect = QRect(int(r.left() + 8), int(r.top() + 8), 30, 30)
right_rect = QRect(int(r.left() + 46), int(r.top() + 8), 30, 30)
edit_rect = QRect(int(r.left() + 100), int(r.top() + 10), self.line_edit.width(), 30)
if left_rect.contains(event.pos()):
self.left_checked = not self.left_checked
self.update()
self._emit_state()
return
if right_rect.contains(event.pos()):
self.right_checked = not self.right_checked
self.update()
self._emit_state()
return
if edit_rect.contains(event.pos()):
self._place_line_edit(edit_rect)
self.line_edit.setFocus()
return
if event.button() == Qt.LeftButton:
self.left_checked = not self.left_checked
self.right_checked = not self.right_checked
self.update()
self._emit_state()
return
def mouseDoubleClickEvent(self, event):
prev = self._prev_vector.copy()
curr = self._current_vector()
A = np.vstack([prev, curr]).astype(float)
col_std = A.std(axis=0, keepdims=True) + 1e-12
A_norm = A / col_std
try:
u, s, vt = np.linalg.svd(A_norm, full_matrices=False)
ratio = float(s[1] / (s[0] + 1e-12))
except Exception:
ratio = 0.0
diff_norm = float(np.linalg.norm(curr - prev))
xfactor = 0.6 * ratio + 0.4 * (diff_norm / (1.0 + diff_norm))
xfactor = float(max(0.0, min(1.0, xfactor)))
self._last_xfactor = xfactor
self._prev_vector = curr.copy()
self.xfactorComputed.emit(xfactor)
self.update()
def _place_line_edit(self, edit_rect):
self.line_edit.setGeometry(edit_rect)
pal = self.line_edit.palette()
pal.setColor(QPalette.Base, QColor(255, 255, 255, 230))
self.line_edit.setPalette(pal)
def resizeEvent(self, event):
edit_rect = QRect(int(self.rect().left() + 100), int(self.rect().top() + 10), self.line_edit.width(), 30)
self.line_edit.setGeometry(edit_rect)
super().resizeEvent(event)
def _interpolate_color(self, c1: QColor, c2: QColor, t: float):
t = max(0.0, min(1.0, t))
return QColor(int(c1.red() + (c2.red() - c1.red()) * t), int(c1.green() + (c2.green() - c1.green()) * t),
int(c1.blue() + (c2.blue() - c1.blue()) * t), int(c1.alpha() + (c2.alpha() - c1.alpha()) * t),)
def paintEvent(self, event):
try:
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
option = QStyleOptionButton()
option.initFrom(self)
self.style().drawControl(QStyle.CE_PushButtonBevel, option, painter, self)
r = self.rect()
left_rect = QRectF(r.left() + 12, r.top() + 8, 30, 30)
right_rect = QRectF(r.left() + 46, r.top() + 8, 30, 30)
edit_rect = QRectF(r.left() + 100, r.top() + 10, self.line_edit.width(), 30)
text_x = edit_rect.right() + 10
text_rect = QRectF(text_x, r.top(), r.width() - text_x - 10, r.height())
glyph_font = QFont(self.font())
glyph_font.setPointSize(14)
painter.setFont(glyph_font)
painter.setPen(self.color_checked if self.left_checked else self.color_normal)
painter.drawText(left_rect, Qt.AlignCenter, self.CHECK if self.left_checked else self.UNCHECK)
painter.setPen(self.color_checked if self.right_checked else self.color_normal)
painter.drawText(right_rect, Qt.AlignCenter, self.CHECK if self.right_checked else self.UNCHECK)
lbl_fnt = QFont("Segoe UI", 9)
painter.setFont(lbl_fnt)
painter.setPen(self.palette().buttonText().color())
painter.drawText(text_rect, Qt.AlignVCenter | Qt.AlignLeft, self._label)
line_y = left_rect.center().y()
left_end_x = left_rect.left() - 14
painter.setPen(QPen(QColor(150, 150, 150), 2))
start_x = left_end_x + 8
mid_x = left_rect.center().x()
expr_result = self.evaluate_expression()
all_false = (not self.left_checked) and (not self.right_checked) and (not self.third_flag)
any_true = self.left_checked or self.right_checked or self.third_flag
glow_color = None
if all_false and (not expr_result): glow_color = self.glow_base_false
elif any_true or expr_result: glow_color = self.glow_base_true
if glow_color is not None:
t = self._last_xfactor
base = QColor(glow_color)
intense = QColor(min(255, base.red()+80), min(255, base.green()+80), min(255, base.blue()+80))
color = self._interpolate_color(base, intense, t)
glow_rect = QRectF(left_rect.left() - 6, left_rect.bottom() + 4,
(right_rect.right() - left_rect.left()) + 12, 8)
for i in range(5, 0, -1):
alpha = int(30 * i * (0.6 + 0.4 * t))
c = QColor(color)
c.setAlpha(alpha)
painter.setBrush(c)
painter.setPen(Qt.NoPen)
shrink = (7 - i) * 1.2
rect = glow_rect.adjusted(shrink, shrink, -shrink, -shrink)
painter.drawRoundedRect(rect, 1, 1)
except Exception as err:
pass
class CharButton(QPushButton):
def __init__(self, ch, parent=None):
super().__init__(ch, parent)
self.ch = ch
self.setFont(QFont("Segoe UI Symbol"))
self.setFocusPolicy(Qt.StrongFocus)
self.setCursor(QCursor(Qt.PointingHandCursor))
self.setFixedSize(40, 36)
self.setStyleSheet("""QPushButton {border: 1px solid #59E9EE; border-radius: 2px;
background: rgba(201, 201, 201, 0.2); font-size: 20px;}
QPushButton:focus {border: 2px solid #59E9EE; background: #999999;}
QPushButton:hover {background: rgba(122, 122, 122, 0.1);}""")
class CharMapDialog(QDialog):
def __init__(self, parent=None, initial_text=""):
super().__init__(parent)
self.setWindowTitle("Character Map")
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setFixedSize(625, 420)
self.setStyleSheet("QDialog {background: #555555}")
input_fnt = QFont("Segoe UI Symbol", 12)
txt_bx_style = """QLineEdit{border: 1px solid rgba(99,99,99,0.22); border-radius: 5px;
padding: 3px 3px; background: #444444;}"""
self.input = ClearableLineEdit()
self.input.setFixedHeight(38)
self.input.setFont(input_fnt)
self.input.setStyleSheet(txt_bx_style)
self.ctx_helper = MiniContextMenu(self, fg_color="#484848", bg_color="#89E9EE")
self.ctx_helper.attach(self.input)
self.CHRMAP_CFG_PTH = Path(MDL_DIR) / 'Cecil+' / 'ide-settings' / 'chrmap_cfg.cfg'
if os.path.isfile(self.CHRMAP_CFG_PTH):
chr_map_cfg = read_file(self.CHRMAP_CFG_PTH)
if chr_map_cfg != 'null': self.input.setText(chr_map_cfg)
else: write_file(self.CHRMAP_CFG_PTH, 'null')
self.copy_btn = QPushButton(QIcon(ICO_ICO["gray_clipboard"]), "Copy")
self.copy_btn.setFont(QFont("Segoe UI", 9))
self.copy_btn.setDefault(True)
self.copy_btn.setFixedHeight(32)
self.copy_btn.setCursor(QCursor(Qt.PointingHandCursor))
self.copy_btn.clicked.connect(self.copy_and_close_if_single())
top_layout = QHBoxLayout()
top_layout.addWidget(self.input)
top_layout.addWidget(self.copy_btn)
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(8)
self.grid_layout.setContentsMargins(8, 8, 8, 8)
self.char_buttons = []; cols = 12
SPCL_CHARS = ["◑", "◓", "◐", "◒", "∓", "⋂", "⏆", "≁", "⍜", "∫", "ᴦ",
"←", "→", "⋊", "⋉", "ͽ", "⇹", "⇼", "·", "•", "…", "—",
"–", "‹", "›", "«", "»", "※", "§", "¶", "†", "‡", "°", "‰",
"‱", "◊", "↑", "↓", "⇐", "⇒", "⇑", "⇓", "⊂",
"⊃", "⊆", "⊇", "⊕", "⊗", "∅", "∈", "∉", "∀", "∃",
"∧", "∨", "¬", "⇔", "≈", "≠", "≡", "≤", "≥", "√",
"∑", "∏", "∂", "ℓ", "ℵ", "℘", "ƒ", "→=", "←=", "=>",
"<=", "->", "<-", "Λ", "μ", "⊥", "°C", "°F", "±", "×", "÷"]
for idx, ch in enumerate(SPCL_CHARS):
btn = CharButton(ch)
r = idx // cols; c = idx % cols
self.grid_layout.addWidget(btn, r, c)
btn.clicked.connect(self.make_insert_handler(ch))
btn.installEventFilter(self)
btn.setFocusPolicy(Qt.StrongFocus)
btn.setAutoDefault(False)
btn.setDefault(False)
btn.clicked.connect(lambda checked, b=btn: self.focus_button(b))
btn.doubleClicked = False
btn.mouseDoubleClickEvent = self.make_double_click_handler(ch)
self.char_buttons.append(btn)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.grid_widget)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
hint = QLabel(" Click to insert, double-click to copy & close.")
hint.setFont(QFont("Segoe UI Symbol"))
hint.setStyleSheet("color: #111111; font-size: 14px;")
hint.setAlignment(Qt.AlignLeft)
main_layout = QVBoxLayout(self)
main_layout.addLayout(top_layout)
main_layout.addWidget(self.scroll)
main_layout.addWidget(hint)
QShortcut(QKeySequence("Esc"), self, self.reject)
QShortcut(QKeySequence("Ctrl+C"), self, self.copy_text)
self._selected_button = None
if self.char_buttons: self.set_selected(self.char_buttons[0])
self.input.returnPressed.connect(self.copy_text)
def make_insert_handler(self, ch):
def handler():
cur = self.input.cursorPosition()
txt = self.input.text()
new = txt[:cur] + ch + txt[cur:]
self.input.setText(new)
self.input.setCursorPosition(cur + len(ch))
self.input.setFocus()
return handler
def make_double_click_handler(self, ch):
def ev(e):
QApplication.clipboard().setText(ch)
self.accept()
return ev
def focus_button(self, btn):
if self._selected_button:
self._selected_button.setStyleSheet(self._selected_button.styleSheet())
self.set_selected(btn)
btn.setFocus()
def set_selected(self, btn):
self._selected_button = btn
btn.setStyleSheet(btn.styleSheet() + " QPushButton { outline: 1px solid #59E9EE; }")
def copy_and_close_if_single(self):
def slot():
self.copy_text()
return slot
def copy_text(self):
txt = self.input.text()
if not txt:
if self._selected_button:
txt = self._selected_button.ch
else:
return
QApplication.clipboard().setText(txt)
if len(txt) <= 2 and self._selected_button and self._selected_button.ch == txt:
self.accept()
def keyPressEvent(self, event):
if not self._selected_button:
return super().keyPressEvent(event)
idx = self.char_buttons.index(self._selected_button)
cols = 12; key = event.key()
if key in (Qt.Key_Right,):
idx = min(idx + 1, len(self.char_buttons) - 1)
self.set_selected(self.char_buttons[idx])
self.char_buttons[idx].setFocus()
self.ensure_visible(self.char_buttons[idx])
return
if key in (Qt.Key_Left,):
idx = max(idx - 1, 0)
self.set_selected(self.char_buttons[idx])
self.char_buttons[idx].setFocus()
self.ensure_visible(self.char_buttons[idx])
return
if key in (Qt.Key_Down,):
idx = min(idx + cols, len(self.char_buttons) - 1)
self.set_selected(self.char_buttons[idx])
self.char_buttons[idx].setFocus()
self.ensure_visible(self.char_buttons[idx])
return
if key in (Qt.Key_Up,):
idx = max(idx - cols, 0)
self.set_selected(self.char_buttons[idx])
self.char_buttons[idx].setFocus()
self.ensure_visible(self.char_buttons[idx])
return
if key in (Qt.Key_Return, Qt.Key_Enter):
QApplication.clipboard().setText(self._selected_button.ch)
self.accept()
return
super().keyPressEvent(event)
def ensure_visible(self, widget):
rect = self.grid_widget.rect()
widget_rect = widget.geometry()
self.scroll.ensureVisible(widget_rect.x(), widget_rect.y(), widget_rect.width(), widget_rect.height())
def showEvent(self, event):
super().showEvent(event)
def closeEvent(self, event):
try:
super().closeEvent(event)
char_map_txt = self.input.text()
if len(char_map_txt) < 1: char_map_txt = 'null'
write_file(self.CHRMAP_CFG_PTH, char_map_txt)
except Exception:
pass
class CodePageLabel(QLabel):
def __init__(self, text, page_size:QSize, font:QFont, parent=None):
super().__init__(parent)
self.page_size = page_size
self.font = font
self.setStyleSheet("background: #535353; border:1px solid #59E9EE;")
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.render_text_to_pixmap(text)
def render_text_to_pixmap(self, text):
try:
doc = QTextDocument()
doc.setDefaultFont(self.font)
doc.setPlainText(text)
doc.setDocumentMargin(12)
self._cpl_hgh = CecilPlusHighlighter(doc)
self._cpl_hgh.rehighlight()
inner_w = max(10, self.page_size.width() - 24)
doc.setTextWidth(inner_w)
pix = QPixmap(self.page_size)
pix.fill(QColor("#535353"))
painter = QPainter(pix)
painter.setRenderHint(QPainter.TextAntialiasing)
doc.drawContents(painter)
painter.end()
self.setPixmap(pix)
self.setFixedSize(self.page_size)
except Exception as e:
pix = QPixmap(self.page_size)
pix.fill(QColor("#535353"))
painter = QPainter(pix)
painter.drawText(12, 20, f"Render error: {e}")
painter.end()
self.setPixmap(pix)
self.setFixedSize(self.page_size)
class RotatingArrowButton(QToolButton):
def __init__(self, parent=None, size=28):
super().__init__(parent)
self._rotation = 0.0
self.setFixedSize(QSize(size, size))
self.setCursor(Qt.PointingHandCursor)
self.setStyleSheet("background: transparent; border: none;")
self._color = QColor(WHITE)
def paintEvent(self, evt):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
w, h = self.width(), self.height()
painter.translate(w/2, h/2)
painter.rotate(self._rotation)
painter.translate(-w/2, -h/2)
pen = QPen(self._color)
pen.setWidthF(1.5)
painter.setPen(pen)
painter.setBrush(QBrush(self._color))
tri = QPolygonF([self._pt(0.3*w, 0.2*h), self._pt(0.7*w, 0.5*h),
self._pt(0.3*w, 0.8*h)])
painter.drawPolygon(tri)
def _pt(self, x, y):
from PyQt5.QtCore import QPointF
return QPointF(x, y)
def getRotation(self):
return self._rotation
def setRotation(self, v):
self._rotation = v
self.update()
rotation = pyqtProperty(float, getRotation, setRotation)
class BottomBarWidget(QWidget):
def __init__(self, parent=None, terminal_height=800):
super().__init__(parent)
self.terminal_height = terminal_height//0.32
self._terminal_open = False
self._anim_duration = 350
self.vlayout = QVBoxLayout(self)
self.vlayout.setContentsMargins(0, 0, 0, 0)
self.vlayout.setSpacing(0)
self.terminal_panel = QFrame(self)
self.terminal_panel.setObjectName("terminal_panel")
self.terminal_panel.setStyleSheet(f"""QFrame#terminal_panel {{background: {PANEL_GRAY};
border-top: 1px solid rgba(255,255,255,0.04);}}""")
self.terminal_layout = QVBoxLayout(self.terminal_panel)
self.terminal_layout.setContentsMargins(6, 6, 6, 6)
self.terminal_layout.setSpacing(4)
title_bar = QHBoxLayout()
title_bar.setContentsMargins(4, 2, 4, 2)
title_bar.setSpacing(6)
self.menu_btn = QPushButton("Menu")
self.menu_btn.setCursor(Qt.PointingHandCursor)
self.menu_btn.setFixedHeight(22)
self.menu_btn.setStyleSheet(self._mini_button_style())
menu = QMenu(self)
menu.addAction("Clear", self._clear_terminal)
menu.addAction("Copy All", self._copy_terminal)
menu.addSeparator()
menu.addAction("Show Projections", self._show_projections)
menu.addAction("Show Sample Space", self._show_sample_space)
menu.addSeparator()
menu.addAction("Settings", self._show_settings)
menu.addAction("Help", self._show_help)
self.menu_btn.setMenu(menu)
self.menu_btn.clicked.connect(lambda: menu.exec_(self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft())))
title_bar.addWidget(self.menu_btn)
title_bar.addStretch()
self.term_close_btn = QPushButton("✕")
self.term_close_btn.setCursor(Qt.PointingHandCursor)
self.term_close_btn.setFixedSize(24, 22)
self.term_close_btn.setStyleSheet(self._mini_button_style())
self.term_close_btn.clicked.connect(self.hide_terminal)
title_bar.addWidget(self.term_close_btn)
self.terminal_layout.addLayout(title_bar)
self.terminal_edit = QTextEdit()
self.terminal_edit.setStyleSheet(f"""background: #121416; color: {WHITE}; border-radius: 4px;
padding: 6px; font-family: Consolas, "Courier New", monospace;
font-size: 18px;""")
self.terminal_edit.setReadOnly(False)
self.terminal_layout.addWidget(self.terminal_edit)
self.terminal_panel.setMaximumHeight(0)
self.vlayout.addWidget(self.terminal_panel)
self.bar = QFrame(self)
self.bar.setObjectName("bottom_bar")
self.bar.setFixedHeight(36)
self.bar.setStyleSheet(f"""QFrame#bottom_bar {{background: {BG_GRAY};
border-top: 1px solid rgba(199,199,199,0.5);}}""")
self.bar_layout = QHBoxLayout(self.bar)
self.bar_layout.setContentsMargins(8, 4, 8, 4)
self.bar_layout.setSpacing(8)
self.left_label = QLabel("!")
self.left_label.setStyleSheet(f"color: {LIGHT_GRAY};")
self.left_label.setMinimumWidth(200)
self.bar_layout.addWidget(self.left_label)
self.bar_layout.addStretch()
self.btn_a = QPushButton("Process Symbol Defs")
self.btn_b = QPushButton("Del Symbol Contains")
for b in (self.btn_a, self.btn_b):
b.setCursor(Qt.PointingHandCursor)
b.setFixedHeight(26)
b.setStyleSheet(self._toolbar_button_style())
self.bar_layout.addWidget(b)
self.arrow_btn = RotatingArrowButton(size=30)
self.arrow_btn.setCursor(Qt.PointingHandCursor)
self.arrow_btn.clicked.connect(self.toggle_terminal)
self.bar_layout.addWidget(self.arrow_btn)
self.vlayout.addWidget(self.bar)
self._height_anim = QPropertyAnimation(self.terminal_panel, b"maximumHeight", self)
self._height_anim.setEasingCurve(QEasingCurve.InOutCubic)
self._height_anim.setDuration(self._anim_duration)
self._rot_anim = QPropertyAnimation(self.arrow_btn, b"rotation", self)
self._rot_anim.setEasingCurve(QEasingCurve.InOutCubic)
self._rot_anim.setDuration(self._anim_duration)
self.arrow_btn.setRotation(0.0)
self._apply_styles()
def _apply_styles(self):
accent = TURQUOISE
self.btn_a.setStyleSheet(self._toolbar_button_style(accent=True))
self.btn_b.setStyleSheet(self._toolbar_button_style())
def _toolbar_button_style(self, accent=False):
bg = TURQUOISE if accent else "#4a4d50"
return f"""QPushButton {{background: {bg}; color: #0b0b0b; border-radius: 4px;
padding: 4px 8px; font-weight: 600;}} QPushButton:hover {{ opacity: 0.95; }}"""
def _mini_button_style(self):
return f"""QPushButton {{background: rgba(255,255,255,0.04); color: {WHITE};
border: 1px solid rgba(255,255,255,0.04); border-radius: 4px; padding: 0 6px;}}
QPushButton::menu-indicator {{ image: none; }}"""
def toggle_terminal(self):
if self._terminal_open:
self._height_anim.stop()
self._height_anim.setStartValue(self.terminal_panel.maximumHeight())
self._height_anim.setEndValue(0)
self._height_anim.setDuration(self._anim_duration)
self._height_anim.start()
self._rot_anim.stop()
self._rot_anim.setStartValue(self.arrow_btn.rotation)
self._rot_anim.setEndValue(0.0)
self._rot_anim.start()
self._terminal_open = False
else:
self._height_anim.stop()
self._height_anim.setStartValue(self.terminal_panel.maximumHeight())
self._height_anim.setEndValue(self.terminal_height)
self._height_anim.setDuration(self._anim_duration)
self._height_anim.start()
self._rot_anim.stop()
self._rot_anim.setStartValue(self.arrow_btn.rotation)
self._rot_anim.setEndValue(90.0)
self._rot_anim.start()
self._terminal_open = True
def _clear_terminal(self):
self.terminal_edit.clear()
def _copy_terminal(self):
self.terminal_edit.selectAll()
self.terminal_edit.copy()
self.terminal_edit.moveCursor(self.terminal_edit.textCursor().End)
def _show_projections(self):
pass
def _show_sample_space(self):
pass
def _show_settings(self):
pass
def _show_help(self):
pass
def set_status_text(self, text: str):
self.left_label.setText(text)
def append_terminal(self, text: str):
self.terminal_edit.append(text)
def hide_terminal(self):
self.setVisible(False)
class Paginator:
def __init__(self, font:QFont, page_width:int, page_height:int, margin=24):
self.font = font
doc = QTextDocument()
doc.setDefaultFont(self.font)
doc.setPlainText("X\n")
doc.setTextWidth(max(10, page_width - margin))
block = doc.firstBlock()
line_h = 14
try:
if block.isValid():
rb = doc.documentLayout().blockBoundingRect(block)
if rb.height() > 0:
line_h = rb.height()
except Exception:
line_h = 14
usable_h = max(10, page_height - margin)
self.lines_per_page = max(1, int(usable_h // line_h))
def paginate(self, full_text: str):
lines = full_text.splitlines()
if not lines:
return [""]
total_pages = math.ceil(len(lines) / self.lines_per_page)
pages = []
for p in range(total_pages):
start = p * self.lines_per_page
end = start + self.lines_per_page
pages.append("\n".join(lines[start:end]))
return pages or [""]
class IDEOpenDialogHorizontal(QDialog):
def __init__(self, start_dir=None, parent=None):
super().__init__(parent)
self.setFont(QFont('Segoe UI', 9))
self.setStyleSheet(DLG_STYLE_SHEET)
self.setWindowTitle("Open Cecil+ File (*.cplq, *.cplp or *.cpls)")
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setFixedSize(1200, 656)
self.model = TreeIconFileSystemModel()
self.model.setRootPath(QDir.rootPath())
filters = ["*.cplq", "*.cplp", "*.cpls"]
self.model.setNameFilters(filters); self.model.setNameFilterDisables(False)
self.tree = QTreeView(); self.tree.setModel(self.model)
self.tree.setRootIndex(self.model.index(start_dir or QDir.homePath()))
for c in range(1, self.model.columnCount()): self.tree.hideColumn(c)
self.tree.setAnimated(True)
self.page_width = 820; self.page_height = 500
self.font = QFont("Segoe UI", 10)
self.paginator = Paginator(self.font, self.page_width, self.page_height)
self.stack = QStackedWidget()
self.stack.setFixedSize(self.page_width, self.page_height)
nav_bar = QWidget()
nav_h = QHBoxLayout(nav_bar); nav_h.setContentsMargins(0,0,0,0)
txts_fnt = QFont("Segoe UI", 9)
self.btn_prev = QPushButton("\u25C0 Prev Page")
self.btn_prev.setFont(txts_fnt)
self.btn_next = QPushButton("Next Page \u25B6")
self.btn_next.setFont(txts_fnt)
self.page_label = QLabel("Page Image 0 / 0")
self.page_label.setFont(txts_fnt)
self.btn_prev.setFixedHeight(32)
self.btn_prev.setEnabled(False)
self.btn_next.setFixedHeight(32)
self.btn_next.setEnabled(False)
nav_h.addWidget(self.btn_prev); nav_h.addWidget(self.page_label); nav_h.addWidget(self.btn_next)
nav_h.addStretch()
right_container = QWidget()
right_v = QVBoxLayout(right_container); right_v.setContentsMargins(8,8,8,8)
self.meta = QLabel("Preview Page Image(s)"); self.meta.setStyleSheet("font-weight:bold;")
right_v.addWidget(self.meta)
right_v.addWidget(self.stack)
right_v.addWidget(nav_bar)
splitter = QSplitter()
splitter.addWidget(self.tree)
splitter.addWidget(right_container)
splitter.setStretchFactor(0,1); splitter.setStretchFactor(1,3)
splitter.setSizes([360, 840])
self.btn_copy_path = QPushButton(QIcon(ICO_ICO["gray_clipboard"]), "Copy CPL File Path")
self.btn_copy_path.setFont(txts_fnt)
self.btn_copy_path.setEnabled(False)
self.btn_open = QPushButton(QIcon(ICO_ICO["gray_open_folder"]), "Open CPL File")
self.btn_open.setFont(txts_fnt)
self.btn_open.setEnabled(False)
btn_cancel = QPushButton(QIcon(ICO_ICO["gray_exit"]), "Cancel")
btn_cancel.setFont(txts_fnt)
btn_h = QHBoxLayout()
btn_h.addStretch(); btn_h.addWidget(self.btn_copy_path)
btn_h.addWidget(self.btn_open); btn_h.addWidget(btn_cancel)
main = QVBoxLayout(self); main.addWidget(splitter); main.addLayout(btn_h)
self.pages, self.byps_opn = [], False
self.selected_path = None
self.right_container = right_container
self.tree.selectionModel().selectionChanged.connect(self.on_selection_changed)
self.btn_prev.clicked.connect(self.on_prev)
self.btn_next.clicked.connect(self.on_next)
self.btn_copy_path.setFixedHeight(32)
self.btn_copy_path.clicked.connect(self.copy_path)
self.btn_open.setFixedHeight(32)
self.btn_open.clicked.connect(self.on_accept)
btn_cancel.setFixedHeight(32)
btn_cancel.clicked.connect(self.on_cancelled)
def on_selection_changed(self, selected, deselected):
idxs = self.tree.selectionModel().selectedRows()
if not idxs:
self.clear_preview(); return
idx = idxs[0]
if not idx.isValid():
self.clear_preview(); return
path = self.model.filePath(idx)
if os.path.isdir(path):
self.clear_preview(message=f"Directory: {path}"); return
allowed = {".cplq", ".cplp", ".cpls"}
_, ext = os.path.splitext(path); ext = ext.lower()
if ext not in allowed:
self.clear_preview(message=f"Unsupported: {ext}"); return
try:
size = os.path.getsize(path)
max_preview = 900 * 1024
if size > max_preview:
with open(path, "r", errors="replace") as f:
text = f.read(450*1024)
text += "\n\n// Preview truncated (file too large)"
else:
with open(path, "r", errors="replace") as f:
text = f.read()
except Exception as e:
text = f"Could not read file: {e}"
size = 0
self.populate_pages(text, path, size)
def populate_pages(self, text, path, size):
self.pages = self.paginator.paginate(text) or [""]
for i in range(self.stack.count()-1, -1, -1):
w = self.stack.widget(i)
self.stack.removeWidget(w)
w.deleteLater()
for chunk in self.pages:
lbl = CodePageLabel(chunk, QSize(self.page_width, self.page_height), self.font)
self.stack.addWidget(lbl)
self.current_index = 0
self.update_nav()
self.meta.setText(f"{path} — {size} bytes")
self.btn_open.setEnabled(True)
self.btn_copy_path.setEnabled(True)
self.selected_path = path
def clear_preview(self, message="Preview Page Image(s)"):
self.meta.setText(message)
for i in range(self.stack.count()-1, -1, -1):
w = self.stack.widget(i)
self.stack.removeWidget(w)
w.deleteLater()
self.pages = []
self.btn_prev.setEnabled(False); self.btn_next.setEnabled(False)
self.page_label.setText("Page Image 0 / 0")
self.btn_open.setEnabled(False); self.btn_copy_path.setEnabled(False)
self.selected_path = None
def update_nav(self):
total = len(self.pages)
if total == 0:
self.btn_prev.setEnabled(False); self.btn_next.setEnabled(False)
self.page_label.setText("Page Image 0 / 0"); return
if not hasattr(self, "current_index") or self.current_index < 0:
self.current_index = 0
if self.current_index >= total:
self.current_index = total - 1
self.stack.setCurrentIndex(self.current_index)
self.page_label.setText(f"Page Image {self.current_index+1} / {total}")
self.btn_prev.setEnabled(self.current_index > 0)
self.btn_next.setEnabled(self.current_index < total-1)
def on_prev(self):
if getattr(self, "current_index", 0) > 0:
self.current_index -= 1
self.update_nav()
def on_next(self):
if getattr(self, "current_index", 0) < len(self.pages)-1:
self.current_index += 1
self.update_nav()
def copy_path(self):
if not self.selected_path:
return
QApplication.clipboard().setText(self.selected_path)
def on_accept(self):
if not self.selected_path:
return
self.byps_opn = True
self.selected_file = self.selected_path
write_file(OPN_CFG_PTH, f"open:)^(:{self.selected_file}")
self.accept()
def on_cancelled(self):
self.close()
def closeEvent(self, event):
try:
super().closeEvent(event)
if not self.byps_opn: write_file(OPN_CFG_PTH, f"cancel:)^(:null")
except Exception:
pass
class CecilPlusContextMenu:
def __init__(self):
pass
class MiniContextMenu:
def __init__(self, parent=None, fg_color="#454545", bg_color="#59E9EE"):
self.fg = fg_color; self.bg = bg_color
def attach(self, line_edit: QLineEdit):
line_edit.setContextMenuPolicy(Qt.CustomContextMenu)
line_edit.customContextMenuRequested.connect(lambda pos, le=line_edit: self.show_menu(le, pos))
def show_menu(self, line_edit: QLineEdit, pos: QPoint):
menu = QMenu(line_edit)
menu.setFont(QFont('Segoe UI', 10))
menu.setStyleSheet("QMenu {background-color: %s; color: %s;}"
"QMenu::item:selected {background-color: %s; color: %s;}"
% (self.bg, self.fg, self.fg, self.bg))
act_cut = QAction("Cut", menu); act_cut.triggered.connect(line_edit.cut)
act_copy = QAction("Copy", menu); act_copy.triggered.connect(line_edit.copy)
act_paste = QAction("Paste", menu); act_paste.triggered.connect(line_edit.paste)
act_sel_all = QAction("Select Line", menu); act_sel_all.triggered.connect(line_edit.selectAll)
act_cut.setEnabled(not line_edit.isReadOnly() and line_edit.hasSelectedText())
act_copy.setEnabled(line_edit.hasSelectedText())
clipboard = QApplication.clipboard()
act_paste.setEnabled(not line_edit.isReadOnly() and bool(clipboard.text()))
act_sel_all.setEnabled(len(line_edit.text()) > 0)
menu.addAction(act_cut); menu.addAction(act_copy)
menu.addAction(act_paste); menu.addAction(act_sel_all)
menu.exec_(line_edit.mapToGlobal(pos))
class FindDialog(QDialog):
def __init__(self, parent=None):
try:
super().__init__(parent)
self.setFont(QFont('Segoe UI', 10))
self.setStyleSheet(DLG_STYLE_SHEET)
self.setWindowTitle("Find / Replace")
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setFixedSize(600, 285)
main = QVBoxLayout(self)
grid = QGridLayout()
lbl_fnt = QFont("Segoe UI", 10)
fnd_lbl = QLabel("Find:")
fnd_lbl.setFont(lbl_fnt)
grid.addWidget(fnd_lbl, 0, 0)
input_fnt = QFont("Segoe UI", 10)
bttn_fnt = QFont("Segoe UI", 10)
txt_bx_style = "QLineEdit{border: 1px solid rgba(99,99,99,0.22); border-radius: 5px; padding: 3px 3px; background: #444444;}"
self.find_input = ClearableLineEdit()
self.find_input.setFixedHeight(34)
self.find_input.setFont(input_fnt)
self.find_input.setStyleSheet(txt_bx_style)
grid.addWidget(self.find_input, 0, 1, 1, 3)
rplc_lbl = QLabel("Reinsert with:")
rplc_lbl.setFont(lbl_fnt)
grid.addWidget(rplc_lbl, 1, 0)
self.replace_input = ClearableLineEdit()
self.replace_input.setFixedHeight(34)
self.replace_input.setFont(input_fnt)
self.replace_input.setStyleSheet(txt_bx_style)
self.ctx_helper = MiniContextMenu(self, fg_color="#484848", bg_color="#89E9EE")
self.ctx_helper.attach(self.find_input)
self.ctx_helper.attach(self.replace_input)
self.crnt_fnd_txt = ""
self.crnt_fnd_cnt = 1
grid.addWidget(self.replace_input, 1, 1, 1, 3)
main.addLayout(grid)
btns = QHBoxLayout()
btns2 = QHBoxLayout()
spcr_lyt = QHBoxLayout()
self.find_next_btn = QPushButton(QIcon(ICO_ICO["gray_search"]), "Find Next", self)
self.find_next_btn.setFont(bttn_fnt)
self.find_next_btn.setFixedHeight(29)
self.pdl_btn = QPushButton("]|[", self)
self.pdl_btn.setFont(bttn_fnt)
self.pdl_btn.setFixedHeight(29)
self.opr_btn = QPushButton(")^(", self)
self.opr_btn.setFont(bttn_fnt)
self.opr_btn.setFixedHeight(29)
self.replace_btn = QPushButton("Reinsert", self)
self.replace_btn.setFont(bttn_fnt)
self.replace_btn.setFixedHeight(29)
self.replace_all_btn = QPushButton("Reinsert All", self)
self.replace_all_btn.setFont(bttn_fnt)
self.replace_all_btn.setFixedHeight(29)
self.close_btn = QPushButton(QIcon(ICO_ICO["gray_exit"]), "Close", self)
self.close_btn.setFont(bttn_fnt)
self.close_btn.setFixedHeight(29)
self.crct_btn = QPushButton("//|_CRCT_|:")
self.crct_btn.setFont(bttn_fnt)
self.crct_btn.setFixedHeight(29)
self.undo_btn = QPushButton(QIcon(ICO_ICO["gray_undo"]), "Undo Reinsert All")
self.undo_btn.setFont(bttn_fnt)
self.undo_btn.setFixedHeight(28)
self.redo_btn = QPushButton(QIcon(ICO_ICO["gray_redo"]), "Redo Reinsert All")
self.redo_btn.setFont(bttn_fnt)
self.redo_btn.setFixedHeight(28)
btns.addWidget(self.find_next_btn)
self.find_next_btn.clicked.connect(self.find)
btns.addWidget(self.pdl_btn)
self.pdl_btn.clicked.connect(self.pipe_find)
btns.addWidget(self.opr_btn)
self.opr_btn.clicked.connect(self.operator_find)
btns.addWidget(self.replace_btn)
self.replace_btn.clicked.connect(self.replace)
btns.addWidget(self.replace_all_btn)
self.replace_all_btn.clicked.connect(self.replace_all_btn_act)
spacer = QSpacerItem(20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
btns.addItem(spacer)
btns.addWidget(self.close_btn)
self.close_btn.clicked.connect(self.close)
self.crct_btn.clicked.connect(self.crct_cmmnt)
btns2.addWidget(self.crct_btn)
self.undo_btn.clicked.connect(self.replace_undo)
btns2.addWidget(self.undo_btn)
self.redo_btn.clicked.connect(self.replace_redo)
btns2.addWidget(self.redo_btn)
main.addLayout(btns)
spcr_lbl = QLabel(" ")
spcr_lbl.setFixedHeight(10)
spcr_lyt.addWidget(spcr_lbl)
main.addLayout(spcr_lyt)
main.addLayout(btns2)
info_lbl = QHBoxLayout()
fdbck_lbl_fnt = QFont("Segoe UI", 9)
self.info_lbl_txt = QLabel(f' {self.format_time("Indian/Mahe")}', self)
self.info_lbl_txt.setFont(fdbck_lbl_fnt)
self.info_lbl_txt.setStyleSheet("color: #777777")
feedback_lbl = QHBoxLayout()
self.feedback_lbl_txt = QLabel(" > ready", self)
self.feedback_lbl_txt.setFont(fdbck_lbl_fnt)
self.feedback_lbl_txt.setStyleSheet("color: #777777")
info_lbl.addWidget(self.info_lbl_txt)
main.addLayout(info_lbl)
feedback_lbl.addWidget(self.feedback_lbl_txt)
main.addLayout(feedback_lbl)
bttn_recent = QVBoxLayout(self)
self.bttn_recent_find = QPushButton("\u25BC Stored recent search/find list item choices \u25BC", self)
self.bttn_recent_find.setFont(bttn_fnt)
self.bttn_recent_find.setFixedHeight(28)
bttn_recent.addWidget(self.bttn_recent_find)
self.popup = QFrame(self, Qt.Popup | Qt.FramelessWindowHint)
popup_layout = QVBoxLayout(self.popup)
self.list_view = QListView(self.popup)
lst_vw_f = QFont()
lst_vw_f.setPointSize(10)
lst_vw_f.setBold(False)
self.list_view.setFont(lst_vw_f)
popup_layout.addWidget(self.list_view)
if os.path.isfile(FND_LST_PTH):
fnd_lst = read_file(FND_LST_PTH)
fnd_lst = fnd_lst.split(':)^(:')
if len(fnd_lst) > 0: self.model = QStringListModel(fnd_lst, self.list_view)
else: self.model = QStringListModel(['member'], self.list_view)
else: self.model = QStringListModel(['member'], self.list_view)
self.list_view.setModel(self.model)
self.list_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.list_view.setSelectionMode(QAbstractItemView.SingleSelection)
self.bttn_recent_find.clicked.connect(self.show_list_popup)
self.list_view.clicked.connect(self.on_recent_clicked)
main.addLayout(bttn_recent)
if os.path.isfile(FND_CFG_PTH):
fnd_cfg = read_file(FND_CFG_PTH).split(':)^(:')
if fnd_cfg[1] != 'null': self.find_input.setText(fnd_cfg[1])
if fnd_cfg[2] != 'null': self.replace_input.setText(fnd_cfg[2])
write_file(FND_CFG_PTH, f'open:)^(:{fnd_cfg[1]}:)^(:{fnd_cfg[2]}')
else: write_file(FND_CFG_PTH, f'open:)^(:null:)^(:null')
write_file(RPL_CFG_PTH, "null")
self._rplc_undo, self._rplc_lmtd, self._rplc_byps = False, False, False
self._rplc_luae, self._rplc_hlt = False, False
self._rplc_ucnt, self._rplc_rcnt, self._rplc_lcnt = -1, -1, 0
self.editor = None
except Exception:
pass
def find(self, isPipeInstances=False, isOperatorInstances=False, isCircuitComments=False):
try:
if not self.editor:
self.feedback_lbl_txt.setText(f" > no editor open..")
return
else: self.editor.setExtraSelections([])
if isPipeInstances: text = "pipe"
elif isOperatorInstances: text = "operator"
elif isCircuitComments: text = "//|_CRCT_|:"
else: text = self.find_input.text()
if len(text) < 1:
return
cursor = self.editor.textCursor()
pos = cursor.selectionEnd()
doc = self.editor.document()
found = doc.find(text, pos)
fnd_cnd = False
if not found.isNull(): fnd_cnd = True
else:
found = doc.find(text, 0)
if not found.isNull(): fnd_cnd = True
if fnd_cnd:
self.editor.setTextCursor(found)
start_pos = min(cursor.selectionStart(), cursor.selectionEnd())
if text == self.crnt_fnd_txt:
self.crnt_fnd_cnt+=1
self.feedback_lbl_txt.setText(f" > found '{text}' ({self.crnt_fnd_cnt})")
else:
self.crnt_fnd_cnt = 1
self.crnt_fnd_txt = text
self.feedback_lbl_txt.setText(f" > found '{text}' (1)")
if not isPipeInstances and not isOperatorInstances and not isCircuitComments:
fnd_items = self.model.stringList()
if text not in fnd_items: fnd_items = [text]+fnd_items
if len(fnd_items) > 66: fnd_items = fnd_items[:66]
self.model.setStringList(fnd_items)
else: self.feedback_lbl_txt.setText(f" > not found '{text}'")
except Exception:
return
def pipe_find(self):
self.find(True, False, False)
def operator_find(self):
self.find(False, True, False)
def crct_cmmnt(self):
self.find(False, False, True)
def show_list_popup(self):
try:
btn_pos = self.bttn_recent_find.mapToGlobal(QPoint(0, self.bttn_recent_find.height()))
self.popup.setFixedWidth(self.bttn_recent_find.width())
self.popup.move(btn_pos)
self.popup.show()
self.list_view.setFocus()
except Exception:
pass
def on_recent_clicked(self, index: QModelIndex):
try:
txt = index.data(Qt.DisplayRole)
self.find_input.setText(txt)
self.popup.hide()
except Exception:
pass
def replace(self):
try:
if not self.editor:
self.feedback_lbl_txt.setText(f" > no editor open..")
return
rplc_txt = self.replace_input.text()
fnd_txt = self.find_input.text()
if len(rplc_txt) < 1:
return
cursor = self.editor.textCursor()
selection = cursor.selectedText()
match_current = False
if cursor.hasSelection():
match_current = selection == fnd_txt
if not match_current:
self.find()
cursor = self.editor.textCursor()
if not cursor.hasSelection():
return
cursor.beginEditBlock()
cursor.insertText(rplc_txt)
self.feedback_lbl_txt.setText(f" > total reinserted (1)")
cursor.endEditBlock()
except Exception:
pass
def replace_all_btn_act(self):
if self._rplc_ucnt == -1: self.replace_all()
else: self.feedback_lbl_txt.setText(f" > no reinsert all can be done(an undo action was called)")
def replace_all(self):
if not self.editor:
self.feedback_lbl_txt.setText(f" > no editor open..")
return
count = None
fnd_txt = self.find_input.text()
rplc_txt = self.replace_input.text()
if len(fnd_txt) < 1:
return
try:
doc = self.editor.document()
cursor, count = QTextCursor(doc), None
cursor.beginEditBlock()
try:
src = doc.toPlainText()
src_lenA = len(src)
count = src.count(fnd_txt)
new_text = src.replace(fnd_txt, rplc_txt)
doc.setPlainText(new_text)
src = doc.toPlainText()
src_lenB = len(src)
delta = src_lenB-src_lenA
pct, cnt_prt = None, None
if src_lenA > 0: pct = (delta/src_lenA)*100.0
if count == 0: cnt_prt = " > No reinserted"
else:
if not self._rplc_byps:
rpl_cfg = read_file(RPL_CFG_PTH)
if rpl_cfg == "null": write_file(RPL_CFG_PTH, f"{fnd_txt}:)^(:{rplc_txt}")
else: write_file(RPL_CFG_PTH, f"{rpl_cfg}]^:^[{fnd_txt}:)^(:{rplc_txt}")
cnt_prt = f" > Reinserted {count} occurrences"
if not self._rplc_byps:
sign = "+" if delta >= 0 else "-"
delta = abs(delta)
pct_prt = f" - net {sign}{abs(pct):.1f}%" if pct is not None else ""
self.feedback_lbl_txt.setText(f"{cnt_prt} · (∆chars: {sign}{delta}{pct_prt})")
except Exception:
cursor.endEditBlock()
return
cursor.endEditBlock()
except Exception:
return
def set_uri_cfg(self, isUndo):
if isUndo:
if self._rplc_ucnt < 0:
if self._rplc_lcnt == 1: self._rplc_ucnt = 0
else: self._rplc_ucnt = self._rplc_lcnt-1
self._rplc_rcnt = self._rplc_ucnt
self._rplc_undo = True
return
if self._rplc_luae:
if self._rplc_ucnt-1 < 0:
self.feedback_lbl_txt.setText(" > no more undo reinsert all available")
self._rplc_hlt = True
else:
self._rplc_ucnt -= 1
self._rplc_rcnt = self._rplc_ucnt
else:
if not self._rplc_undo:
self.feedback_lbl_txt.setText(" > no redo reinsert all to apply(no undo done)")
self._rplc_hlt = True
return
if not self._rplc_luae:
if self._rplc_rcnt+1 == self._rplc_lcnt:
self.feedback_lbl_txt.setText(" > no more redo reinsert all available")
self._rplc_hlt = True
else:
self._rplc_rcnt += 1
self._rplc_ucnt = self._rplc_rcnt
def rpt_rpl(self, isUndo, rpl_cfg):
self.set_uri_cfg(isUndo)
if isUndo:
cnt_idx, rpl_t, prv_c = self._rplc_ucnt, (1, 0), True
else:
cnt_idx, rpl_t, prv_c = self._rplc_rcnt, (0, 1), False
if not self._rplc_hlt:
if self._rplc_lcnt > 1: rpl_cfg = rpl_cfg[cnt_idx]
rpl_cfg = rpl_cfg.split(":)^(:")
self.find_input.setText(rpl_cfg[rpl_t[0]])
self.replace_input.setText(rpl_cfg[rpl_t[1]])
self._rplc_byps = True
self.replace_all()
self._rplc_byps = False
self._rplc_luae = prv_c
if isUndo:
self.feedback_lbl_txt.setText(f" > undo reinsert all done(reinsert index [{cnt_idx}] used)")
else:
self.feedback_lbl_txt.setText(f" > redo reinsert all done(reinsert index [{cnt_idx}] used)")
def set_rpl_cfg(self, isUndo):
try:
if not self.editor:
self.feedback_lbl_txt.setText(f" > no editor open..")
return
self._rplc_hlt = False
rpl_cfg = read_file(RPL_CFG_PTH)
if rpl_cfg != "null":
if rpl_cfg.find("]^:^[") > -1:
rpl_cfg = rpl_cfg.split("]^:^[")
self._rplc_lcnt = len(rpl_cfg)
else: self._rplc_lcnt = 1
else:
if isUndo: self.feedback_lbl_txt.setText(" > no undo reinsert all available")
else: self.feedback_lbl_txt.setText(" > no redo reinsert all available")
return
self.rpt_rpl(isUndo, rpl_cfg)
except Exception:
pass
def replace_undo(self):
self.set_rpl_cfg(True)
def replace_redo(self):
self.set_rpl_cfg(False)
def format_time(self, offset_hours):
try:
tz = zoneinfo.ZoneInfo(offset_hours)
now = datetime.now(tz)
hour_min = f"{now.hour:02d}:{now.minute:02d}"
weekday = now.strftime("%a")
month = str(now.month)
year = str(now.year)
off = now.utcoffset() or timedelta(0)
total_minutes = int(off.total_seconds()//60)
sign = "+" if total_minutes >= 0 else "-"
hh = abs(total_minutes)//60
mm = abs(total_minutes)%60
offset_str = f"UTC{sign}{hh:02d}:{mm:02d}"
week = now.isocalendar()[1]
return f"{hour_min} {weekday}/{month}/{year} {offset_str} Week {week}"
except Exception:
return "error @format_time(), couldn't resolve valid UTC Time Format"
def showEvent(self, event):
super().showEvent(event)
def closeEvent(self, event):
try:
super().closeEvent(event)
fnd_txt = self.find_input.text()
if len(fnd_txt) < 1: fnd_txt = 'null'
rpl_txt = self.replace_input.text()
if len(rpl_txt) < 1: rpl_txt = 'null'
write_file(FND_CFG_PTH, f'closed:)^(:{fnd_txt}:)^(:{rpl_txt}')
fnd_items = self.model.stringList()
fnd_items = ":)^(:".join(fnd_items)
write_file(FND_LST_PTH, fnd_items)
except Exception:
pass
class AboutDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet(DLG_STYLE_SHEET)
self.setWindowTitle("About")
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setModal(True)
icon_label = QLabel(self)
qicon = CPL_ICO.get("ki_resolver")
pix = qicon.pixmap(48, 48)
icon_label.setPixmap(pix)
icon_label.setFixedSize(QSize(48, 48))
title_label = QLabel("<b>Cecil+</b> <span style='font-weight:normal'>(v1.001.039)</span>", self)
title_font = title_label.font()
title_font.setBold(True)
title_font.setPointSize(title_font.pointSize() + 2)
title_label.setFont(title_font)
line1 = QLabel("Build Date: ---------------", self)
line2 = QLabel("Linker Libraries: ---------------", self)
line3 = QLabel("Runtime CMUT Libraries: ---------------", self)
line4 = QLabel("Spectral PMQ Libraries: ---------------", self)
small_font = line1.font()
small_font.setPointSize(max(8, small_font.pointSize() - 1))
small_font.setBold(False)
line1.setFont(small_font); line2.setFont(small_font)
line3.setFont(small_font); line4.setFont(small_font)
ok_btn = QPushButton("ACCEPT", self)
ok_btn.setDefault(True)
ok_btn.clicked.connect(self.accept)
text_v = QVBoxLayout()
text_v.setContentsMargins(0, 0, 0, 0)
text_v.addWidget(title_label)
text_v.addWidget(line1); text_v.addWidget(line2)
text_v.addWidget(line3); text_v.addWidget(line4)
text_v.addStretch()
bottom_row = QHBoxLayout()
bottom_row.addStretch()
bottom_row.addWidget(ok_btn)
text_v.addLayout(bottom_row)
main = QHBoxLayout(self)
main.addWidget(icon_label, 0, Qt.AlignTop)
main.addLayout(text_v)
self.setLayout(main)
self.setFixedSize(self.sizeHint().expandedTo(QSize(360, 140)))
class TreeIconFileSystemModel(QFileSystemModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
lst_page_icons = create_cecil_plus_page_icons()
self.page_icons = {'folder': QIcon(ICO_ICO["folder"]), 'file': lst_page_icons[0], '.cplq': lst_page_icons[1],
'.cplp': lst_page_icons[2], '.cpls': lst_page_icons[3]}
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DecorationRole and index.column() == 0:
if self.isDir(index):
return self.page_icons['folder']
path = self.filePath(index)
_, ext = os.path.splitext(path)
ext = ext.lower()
if ext == '.cplq' or ext == '.cplp' or ext == '.cpls':
return self.page_icons[ext]
else:
return self.page_icons['file']
return super().data(index, role)
class FileExplorer(QTreeView):
fileOpened = pyqtSignal(str)
def __init__(self, parent=None, rootPath=""):
super().__init__(parent)
self.model = TreeIconFileSystemModel()
self.model.setRootPath(rootPath or os.path.expanduser("~"))
self.setModel(self.model)
self.setRootIndex(self.model.index(rootPath or os.path.expanduser("~")))
self.doubleClicked.connect(self.onDoubleClick)
self.setColumnHidden(0, False)
self.setColumnHidden(1, True)
self.setColumnHidden(2, True)
self.setColumnHidden(3, True)
def onDoubleClick(self, idx):
if not idx.isValid():
return
path = self.model.filePath(idx)
if QFileInfo(path).isFile(): self.fileOpened.emit(path)
class StatusBar(QStatusBar):
def __init__(self, parent=None):
super().__init__(parent)
container = QWidget(self)
self._layout = QHBoxLayout(container)
self._layout.setContentsMargins(4, 0, 4, 0)
self._layout.setSpacing(6)
self.left_btns = QWidget(container)
left_layout = QHBoxLayout(self.left_btns)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.setSpacing(4)
btn_style = f"""QPushButton {{color: rgba(68, 68, 68, 1.0); background: rgba(78, 78, 78, 0.5);
padding: 10px 16px}} QPushButton::hover {{color: rgba(42, 42, 42, 1.8);
background: rgba(84, 84, 84, 0.3)}}"""
self.btn_krknB = QPushButton(QIcon(ICO_ICO["tag"]), "", self.left_btns)
self.btn_krknB.setStyleSheet(btn_style)
self.btn_krknB.setFixedSize(68, 32)
self.btn_krknF = QPushButton(QIcon(ICO_ICO["link"]), "", self.left_btns)
self.btn_krknF.setStyleSheet(btn_style)
self.btn_krknF.setFixedSize(68, 32)
for b in(self.btn_krknB, self.btn_krknF):
b.setFocusPolicy(Qt.NoFocus); left_layout.addWidget(b)
self._layout.addWidget(self.left_btns, 0, Qt.AlignLeft)
self.status_label = QLabel(" > Ready", container)
self.status_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self._layout.addWidget(self.status_label, 1)
self.right_btns = QWidget(container)
right_layout = QHBoxLayout(self.right_btns)
right_layout.setContentsMargins(8, 0, 7, 11)
right_layout.setSpacing(4)
btn_style2 = f"QPushButton::hover {{ color: rgba(22, 22, 22, 0.8) }}"
self.btn_a = QPushButton("BTTN-A", self.right_btns); self.btn_a.setStyleSheet(btn_style2)
self.btn_b = QPushButton("BTTN-B", self.right_btns); self.btn_b.setStyleSheet(btn_style2)
self.btn_c = QPushButton("BTTN-C", self.right_btns); self.btn_c.setStyleSheet(btn_style2)
self.btn_d = QPushButton("BTTN-D", self.right_btns); self.btn_d.setStyleSheet(btn_style2)
for b in(self.btn_a, self.btn_b, self.btn_c, self.btn_d):
b.setFocusPolicy(Qt.NoFocus); right_layout.addWidget(b)
self._layout.addWidget(self.right_btns, 0, Qt.AlignRight)
self.addPermanentWidget(container, 1)
self._busy = False
self._pulse_alpha = 0.0
self._pulse_dir = 1
self._pulse_timer = QTimer(self)
self._pulse_timer.setInterval(42)
self._pulse_timer.timeout.connect(self._on_pulse_timer)
self.btn_krknB.clicked.connect(self._on_pattern_clicked)
self.btn_krknF.clicked.connect(self._on_build_clicked)
def showMessage(self, message, timeout=0):
self.status_label.setText(message)
if timeout > 0:
QTimer.singleShot(timeout, lambda: self.status_label.setText(""))
def clearMessage(self):
self.status_label.setText("")
def setBusy(self, busy: bool):
busy = bool(busy)
if busy == self._busy:
return
self._busy = busy
if busy:
self._pulse_alpha = 0.0
self._pulse_dir = 1
self._pulse_timer.start()
else:
self._pulse_timer.stop()
self._pulse_alpha = 0.0
self.update()
def setLeftButtonsVisible(self, visible: bool):
self.left_btns.setVisible(visible)
def setRightButtonsVisible(self, visible: bool):
self.right_btns.setVisible(visible)
def _on_pulse_timer(self):
step = 0.08
self._pulse_alpha += self._pulse_dir * step
if self._pulse_alpha >= 1.0:
self._pulse_alpha = 1.0
self._pulse_dir = -1
elif self._pulse_alpha <= 0.0:
self._pulse_alpha = 0.0
self._pulse_dir = 1
self.update()
def paintEvent(self, event: QPaintEvent):
super().paintEvent(event)
if not self._busy:
return
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing, True)
base = QColor(255, 200, 0)
alpha = int(180 * (0.35 + 0.65 * self._pulse_alpha))
base.setAlpha(alpha)
p.setPen(base)
p.setBrush(base)
height = 3
rect = QRectF(0, self.height() - height, self.width(), height)
p.drawRect(rect)
p.end()
@pyqtSlot()
def _on_pattern_clicked(self):
self.showMessage(" > Kraken AI Process - Spectral Object(s) Patternize", 2000)
@pyqtSlot()
def _on_build_clicked(self):
self.showMessage(" > Kraken AI Process - Spectral Object(s) Building...")
self.setBusy(True)
QTimer.singleShot(3000, lambda: (self.showMessage(" > Spectral Object(s) build finished", 2000), self.setBusy(False)))
class CustomTabBar(QTabBar):
tabClosedRequested = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent=None)
pass
def tabInserted(self, index):
try:
super().tabInserted(index)
btn = QPushButton("X")
btn.setCursor(Qt.PointingHandCursor)
btn.setFixedSize(20, 20)
btn.setFlat(True)
btn.setStyleSheet("QPushButton {background: #888888; color: #303030; border: 1px solid #505050;}")
self.setTabButton(index, QTabBar.RightSide, btn)
btn.clicked.connect(lambda _checked, i=index: self.tabCloseRequested.emit(i))
except Exception:
pass
class MenuRegistry(QObject):
def __init__(self):
super().__init__()
self.items = []
def register_menu_item(self, menu_path, descriptor):
self.items.append((menu_path, descriptor))
MENU_REGISTRY = MenuRegistry()
class CecilIDE(QMainWindow):
def __init__(self):
super().__init__()
preload_cpl_icons()
ico_pth = Path(f"{MDL_DIR}/Cecil+/ide-settings/ico-ico")
if not os.path.isdir(ico_pth): build_unicode_icon_pngs()
for ico_png in ico_pth.glob("*.png"):
ico_key = ico_png.stem
ICO_ICO[ico_key] = QPixmap(str(ico_png))
self.setWindowTitle('Cecil+')
self.setGeometry(100, 100, 1100, 760)
self._currentFiles, self._actions = {}, {}
self._ut_cnt = 0
self.menu_structure = self.default_menu_structure()
self._doc = None
self._cur = None
self._block = None
self._start = None
self._length = None
self._end = None
self.initUI()
def default_menu_structure(self):
return {
"File": [{"id": "new", "label": "New", "shortcut": "Ctrl+Shift+N"},
{"id": "open", "label": "Open", "shortcut": "Ctrl+O"},
{"id": "open_template", "label": "Open Template", "shortcut": ""},
{"id": "open_linker_xml", "label": "Open Linker XML", "shortcut": ""},
{"id": "open_kraken_sqtpp", "label": "Open Kraken SQTPP", "shortcut": ""},
{"type": "separator"},
{"id": "new_project", "label": "New Project", "shortcut": ""},
{"id": "open_project", "label": "Open Project", "shortcut": ""},
{"id": "export", "label": "Export Project", "shortcut": ""},
{"id": "project_settings", "label": "Project Settings", "shortcut": ""},
{"id": "close_project", "label": "Close Project", "shortcut": ""},
{"type": "separator"},
{"id": "save", "label": "Save", "shortcut": "Ctrl+S"},
{"id": "save_as", "label": "Save As", "shortcut": ""},
{"id": "save_as_template", "label": "Save As Template", "shortcut": ""},
{"id": "save_all", "label": "Save All", "shortcut": "Ctrl+Shift+S"},
{"type": "separator"},
{"id": "apply_circuit_comments", "label": "Apply Circuit Comments", "shortcut": ""},
{"id": "remove_circuit_comments", "label": "Remove Circuit Comments", "shortcut": ""},
{"id": "refresh_circuit_comments", "label": "Refresh Circuit Comments", "shortcut": ""},
{"type": "separator"},
{"id": "settings", "label": "Settings", "shortcut": "Ctrl+Alt+S"},
{"id": "kraken_ai_settings", "label": "Kraken AI Settings", "shortcut": "Ctrl+Alt+K"},
{"id": "expression_unit_settings", "label": "Expression Unit Settings", "shortcut": "Ctrl+Alt+U"},
{"id": "linear_commutator_settings", "label": "Linear Commutator Settings", "shortcut": "Ctrl+Alt+L"},
{"type": "separator"},
{"id": "linker_properties", "label": "Linker Properties", "shortcut": "Ctrl+Alt+P"},
{"id": "linker_symbols", "label": "Linker Symbols", "shortcut": "Ctrl+Alt+O"},
{"id": "linker_methods_pmq", "label": "Linker Methods PMQ", "shortcut": "Ctrl+Alt+Q"}],
"Edit": [
{"id": "undo", "label": "Undo", "shortcut": "Ctrl+Z"},
{"id": "redo", "label": "Redo", "shortcut": "Ctrl+Y"},
{"type": "separator"},
{"id": "cut", "label": "Cut", "shortcut": "Ctrl+X"},
{"id": "copy", "label": "Copy", "shortcut": "Ctrl+C"},
{"id": "paste", "label": "Paste", "shortcut": "Ctrl+V"},
{"id": "select_all", "label": "Select All", "shortcut": "Ctrl+A"},
{"type": "separator"},
{"id": "find", "label": "Find", "shortcut": "Ctrl+F"},
{"id": "find_in_files", "label": "Find in Files", "shortcut": "Ctrl+Shift+F"},
{"id": "copy_file_path", "label": "Copy Tab File Path", "shortcut": "Ctrl+Shift+C"},
{"id": "format", "label": "Format Document", "shortcut": "Shift+Alt+F"},
{"type": "separator"},
{"id": "insert_comment", "label": "Insert Comment", "shortcut": "Ctrl+/"},
{"id": "combine_strict_types_for_operator", "label": " Join Strict Types for Operator", "shortcut": ""},
{"type": "separator"},
{"id": "select_line", "label": "Select Line", "shortcut": "Ctrl+L"},
{"id": "indent_line", "label": "Indent Line", "shortcut": "Ctrl+["},
{"id": "unindent_line", "label": "Unindent Line", "shortcut": "Ctrl+]"},
{"id": "move_line_up", "label": "Move Line Up", "shortcut": "Ctrl+Shift+Up"},
{"id": "move_line_down", "label": "Move Line Down", "shortcut": "Ctrl+Shift+Down"},
{"id": "insert_blank_line_above", "label": "Insert Blank Line Above", "shortcut": "Ctrl+Alt+Enter"},
{"id": "insert_blank_line_below", "label": "Insert Blank Line Below", "shortcut": "Ctrl+Alt+Shift+Enter"},
{"id": "duplicate_line", "label": "Duplicate Line", "shortcut": "Ctrl+Shift+D"},
{"id": "del_trail_space", "label": "Del Trailing Line Space", "shortcut": "Ctrl+Backspace"},
{"id": "delete_line", "label": "Delete Line", "shortcut": "Ctrl+Shift+K"},
{"type": "separator"},
{"id": "char_map", "label": "Char Map", "shortcut": ""}],
"View": [
{"id": "explorer", "label": "Explorer", "shortcut": "Ctrl+Shift+E"},
{"id": "terminal", "label": "Terminal", "shortcut": "Ctrl+`"},
{"type": "separator"},
{"id": "fold_all_functions", "label": "Fold All", "shortcut": "Alt+0"},
{"id": "unfold_all_functions", "label": "Unfold All", "shortcut": "Alt+Shift+0"},
{"id": "fold_current_function", "label": "Fold Current", "shortcut": "Ctrl+Alt+F"},
{"id": "unfold_current_function", "label": "Unfold Current", "shortcut": "Ctrl+Alt+Shift+F"},
{"type": "separator"},
{"id": "zen", "label": "Toggle Zen Mode", "shortcut": "F11"}
],
"Navigate": [
{"id": "back", "label": "Back", "shortcut": "Alt+Left"},
{"id": "forward", "label": "Forward", "shortcut": "Alt+Right"},
{"id": "goto_def", "label": "Go to Definition", "shortcut": "F12"}
],
"Build": [
{"id": "build", "label": "Build Project", "shortcut": "Ctrl+Shift+B"},
{"id": "rebuild", "label": "Rebuild", "shortcut": ""},
{"id": "clean", "label": "Clean", "shortcut": ""}
],
"Run": [
{"id": "run", "label": "Run", "shortcut": "F5"},
{"id": "run_no_debug", "label": "Run Without Debugging", "shortcut": "Ctrl+F5"},
{"id": "run_tests", "label": "Run Tests", "submenu": [
{"id": "run_all_tests", "label": "Run All Tests"},
{"id": "run_current_test", "label": "Run Current Test"}
]}
],
"Debug": [
{"id": "start_debug", "label": "Start Debugging", "shortcut": "F5"},
{"id": "stop_debug", "label": "Stop Debugging", "shortcut": "Shift+F5"},
{"id": "step_over", "label": "Step Over", "shortcut": "F10"}
],
"Tools": [
{"id": "palette", "label": "Command Palette", "shortcut": "Ctrl+Shift+P"},
{"id": "extensions", "label": "Extensions...", "shortcut": ""}
],
"Window": [
{"id": "next_editor", "label": "Next Editor", "shortcut": "Ctrl+Tab"},
{"id": "prev_editor", "label": "Previous Editor", "shortcut": "Ctrl+Shift+Tab"},
{"id": "reset_layout", "label": "Reset Layout", "shortcut": ""}
],
"Help": [
{"id": "docs", "label": "Cecil+ Docs", "shortcut": "F1"},
{"id": "shortcuts", "label": "Keyboard Shortcuts", "shortcut": ""},
{"id": "about", "label": "About", "shortcut": ""}
]
}
def initUI(self):
central = QWidget()
self.setCentralWidget(central)
hl = QHBoxLayout(central)
# file explorer
self.fileExplorer = FileExplorer(rootPath=os.path.expanduser("~"))
self.fileExplorer.setMaximumWidth(320)
self.fileExplorer.fileOpened.connect(self.openFile)
# editor tabs
self.tabs = QTabWidget()
tab_bar = CustomTabBar()
self.tabs.setTabBar(tab_bar)
tab_bar.tabCloseRequested.connect(self.closeTab)
hl.addWidget(self.fileExplorer)
hl.addWidget(self.tabs)
# terminal bar
size = self.window()
self.terminalBar = BottomBarWidget(None, size.height())
self.terminalBar.setVisible(False)
hl.addWidget(self.terminalBar)
self.terminalBar.set_status_text(":: Cecil+ terminal ready")
self.terminalBar.append_terminal("/CMUT/v1.04 >>")
self.sb = StatusBar(self)
self.setStatusBar(self.sb)
# toolbar & menu
self.createActions()
self.buildMenusFromStructure()
self.createToolBar()
self.createVertToolBar()
# process any registered menu extensions
self.apply_menu_registry()
def createActions(self):
a = self._actions
# ---- FILE ----
a['new'] = QAction(QIcon(ICO_ICO["page"]), " New", self); a['new'].triggered.connect(self.newFile)
a['open'] = QAction(QIcon(ICO_ICO["open_folder"]), " Open", self); a['open'].triggered.connect(self.cecilPlusOpenFile)
a['open_template'] = QAction(QIcon(ICO_ICO["keyboard"]), " Open Template", self); a['open_template'].triggered.connect(self.openTemplate)
a['open_linker_xml'] = QAction(QIcon(ICO_ICO["link"]), " Open Linker XML", self); a['open_linker_xml'].triggered.connect(self.openLinkerXML)
a['open_kraken_sqtpp'] = QAction(QIcon(ICO_ICO["microscope"]), " Open Kraken SQTPP", self); a['open_kraken_sqtpp'].triggered.connect(self.openKrakenSQTPP)
a['new_project'] = QAction(QIcon(ICO_ICO["package"]), " New Project", self); a['new_project'].triggered.connect(self.newProjectDialog)
a['open_project'] = QAction(QIcon(ICO_ICO["user"]), " Open Project", self); a['open_project'].triggered.connect(self.openProjectDialog)
a['export'] = QAction(QIcon(ICO_ICO["grid"]), " Export Project", self); a['export'].triggered.connect(self.exportProject)
a['project_settings'] = QAction(QIcon(ICO_ICO["hammer_wrench"]), " Project Settings", self); a['project_settings'].triggered.connect(self.projectSettings)
a['close_project'] = QAction(QIcon(ICO_ICO["close"]), " Close Project", self); a['close_project'].triggered.connect(self.closeProject)
a['save'] = QAction(QIcon(ICO_ICO["save"]), " Save", self); a['save'].triggered.connect(self.saveFile)
a['save_all'] = QAction(" Save All", self); a['save_all'].triggered.connect(self.saveAll)
a['save_as'] = QAction(" Save As", self); a['save_as'].triggered.connect(self.saveFileAs)
a['save_as_template'] = QAction(" Save As Template", self); a['save_as_template'].triggered.connect(self.saveAsTemplate)
a['apply_circuit_comments'] = QAction(QIcon(ICO_ICO["add"]), " Apply Circuit Comments", self); a['apply_circuit_comments'].triggered.connect(self.applyCircuitComments)
a['remove_circuit_comments'] = QAction(QIcon(ICO_ICO["remove"]), " Remove Circuit Comments", self); a['remove_circuit_comments'].triggered.connect(self.removeCircuitComments)
a['refresh_circuit_comments'] = QAction(" Refresh Circuit Comments", self); a['refresh_circuit_comments'].triggered.connect(self.refreshCircuitComments)
a['settings'] = QAction(QIcon(ICO_ICO["gear"]), " Settings", self); a['settings'].triggered.connect(self.settings)
a['kraken_ai_settings'] = QAction(" Kraken AI Settings", self); a['kraken_ai_settings'].triggered.connect(self.krakenAiSettings)
a['expression_unit_settings'] = QAction(" Expression Unit Settings", self); a['expression_unit_settings'].triggered.connect(self.expressionUnitSettings)
a['linear_commutator_settings'] = QAction(" Linear CMUT Settings", self); a['linear_commutator_settings'].triggered.connect(self.linearCommutatorSettings)
a['linker_properties'] = QAction(QIcon(ICO_ICO["info"]), " Linker Properties", self); a['linker_properties'].triggered.connect(self.linkerProperties)
a['linker_symbols'] = QAction(" Linker Symbols", self); a['linker_symbols'].triggered.connect(self.linkerSymbols)
a['linker_methods_pmq'] = QAction(" Linker Methods PMQ", self); a['linker_methods_pmq'].triggered.connect(self.linkerMethodsPMQ)
# ---- EDIT ----
a['undo'] = QAction(QIcon(ICO_ICO["undo"]), " Undo", self); a['undo'].triggered.connect(self.undo)
a['redo'] = QAction(QIcon(ICO_ICO["redo"]), " Redo", self); a['redo'].triggered.connect(self.redo)
a['cut'] = QAction(QIcon(ICO_ICO["cut"]), " Cut", self); a['cut'].triggered.connect(self.cut)
a['copy'] = QAction(QIcon(ICO_ICO["clipboard"]), " Copy", self); a['copy'].triggered.connect(self.copy)
a['paste'] = QAction(QIcon(ICO_ICO["paste"]), " Paste", self); a['paste'].triggered.connect(self.paste)
a['select_all'] = QAction(" Select All", self); a['select_all'].triggered.connect(self.select_all)
a['find'] = QAction(QIcon(ICO_ICO["search"]), " Find", self); a['find'].triggered.connect(self.openFind)
a['find_in_files'] = QAction(" Find in Files", self); a['find_in_files'].triggered.connect(self.findInFiles)
a['copy_file_path'] = QAction(" Copy Tab File Path", self); a['copy_file_path'].triggered.connect(self.copyFilePath)
a['format'] = QAction(" Format Document", self); a['format'].triggered.connect(self.formatDocument)
a['insert_comment'] = QAction(QIcon(ICO_ICO["ellipsis"]), " Insert Comment", self); a['insert_comment'].triggered.connect(self.insert_comment)
a['combine_strict_types_for_operator'] = QAction(" Join Strict Types for Operator", self); a['combine_strict_types_for_operator'].triggered.connect(self.join_strict_operator)
a['select_line'] = QAction(QIcon(ICO_ICO["pencil"]), " Select Line", self); a['select_line'].triggered.connect(self.select_line)
a['indent_line'] = QAction(" Indent Line", self); a['indent_line'].triggered.connect(self.indent_line)
a['unindent_line'] = QAction(" Unindent Line", self); a['unindent_line'].triggered.connect(self.unindent_line)
a['move_line_up'] = QAction(" Move Line Up", self); a['move_line_up'].triggered.connect(self.move_line_up)
a['move_line_down'] = QAction(" Move Line Down", self); a['move_line_down'].triggered.connect(self.move_line_down)
a['insert_blank_line_above'] = QAction(" Insert Blank Line Above", self); a['insert_blank_line_above'].triggered.connect(self.insert_blank_line_above)
a['insert_blank_line_below'] = QAction(" Insert Blank Line Below", self); a['insert_blank_line_below'].triggered.connect(self.insert_blank_line_below)
a['duplicate_line'] = QAction(" Duplicate Line", self); a['duplicate_line'].triggered.connect(self.duplicate_line)
a['del_trail_space'] = QAction(" Del Trailing Line Space", self); a['del_trail_space'].triggered.connect(self.del_line_whitespace)
a['delete_line'] = QAction(" Delete Line", self); a['delete_line'].triggered.connect(self.delete_line)
a['char_map'] = QAction(QIcon(ICO_ICO["memo"]), " Character Map", self); a['char_map'].triggered.connect(self.char_map)
# View
a['explorer'] = QAction(QIcon(ICO_ICO["folder"]), " Explorer", self); a['explorer'].triggered.connect(self.toggleExplorer)
a['terminal'] = QAction(QIcon(ICO_ICO["laptop"]), " Terminal", self); a['terminal'].triggered.connect(self.toggleTerminal)
a['fold_all_functions'] = QAction(" Fold All", self); a['fold_all_functions'].triggered.connect(self.fold_all_func)
a['unfold_all_functions'] = QAction(" Unfold All", self); a['unfold_all_functions'].triggered.connect(self.unfold_all_func)
a['fold_current_function'] = QAction(" Fold Current", self); a['fold_current_function'].triggered.connect(self.fold_current_func)
a['unfold_current_function'] = QAction(" Unfold Current", self); a['unfold_current_function'].triggered.connect(self.unfold_current_func)
a['zen'] = QAction(" Full Screen", self); a['zen'].triggered.connect(self.toggleZen)
# Build/Run/Debug
a['build'] = QAction("Build", self); a['build'].triggered.connect(self.buildProject)
a['rebuild'] = QAction("Rebuild", self); a['rebuild'].triggered.connect(self.rebuildProject)
a['clean'] = QAction("Clean", self); a['clean'].triggered.connect(self.cleanProject)
a['run'] = QAction("Run", self); a['run'].triggered.connect(self.runCurrent)
a['run_no_debug'] = QAction("Run Without Debugging", self); a['run_no_debug'].triggered.connect(self.runWithoutDebug)
a['start_debug'] = QAction("Start Debugging", self); a['start_debug'].triggered.connect(self.startDebugging)
a['stop_debug'] = QAction("Stop Debugging", self); a['stop_debug'].triggered.connect(self.stopDebugging)
# Tools/Git/Help
a['palette'] = QAction("Command Palette", self); a['palette'].triggered.connect(self.openCommandPalette)
a['extensions'] = QAction("Extensions...", self); a['extensions'].triggered.connect(self.openExtensions)
a['docs'] = QAction("Cecil+ Docs", self); a['docs'].triggered.connect(self.openDocs)
a['about'] = QAction("About", self); a['about'].triggered.connect(self.showAbout)
def buildMenusFromStructure(self):
sep_ucd = '\u2014'
menu_sep = ''.join([sep_ucd for _ in range(13)])
menubar = self.menuBar()
menubar.clear()
for menuName, items in self.menu_structure.items():
menu = menubar.addMenu(menuName)
menu.setStyleSheet(MENU_STYLESHEET)
for it in items:
if it.get('type') == 'separator':
addMenuDivider(menu, it.get('text', ' ' + menu_sep))
else:
if 'submenu' in it:
sub = menu.addMenu(it['label'])
for sit in it.get('submenu', []):
act = self._actions.get(sit['id']) or QAction(sit['label'], self)
sub.addAction(act)
else:
act = self._actions.get(it['id']) or QAction(it['label'], self)
if it.get('shortcut'):
act.setShortcut(it['shortcut'])
menu.addAction(act)
def apply_menu_registry(self):
for path, desc in MENU_REGISTRY.items:
parts = path.split('/')
if not parts:
continue
menu = self.menuBar().findChild(type(self.menuBar()), parts[0])
targetMenu = None
for m in self.menuBar().children():
if getattr(m, 'title', None) == parts[0] or (hasattr(m, 'windowTitle') and m.windowTitle() == parts[0]):
targetMenu = m
break
if not targetMenu: targetMenu = self.menuBar().addMenu(parts[0])
label = desc.get('label', 'Plugin Item')
act = QAction(label, self)
if 'shortcut' in desc and desc['shortcut']: act.setShortcut(desc['shortcut'])
if 'callback' in desc and callable(desc['callback']): act.triggered.connect(desc['callback'])
targetMenu.addAction(act)
def createToolBar(self):
tb = QToolBar('Main')
tb.setStyleSheet(f"QToolBar {{ background: {PALETTE['tab_active']}; }}")
fnt = QFont('Segoe UI', 10)
fnt.setBold(False)
tb.setFont(fnt)
tb.setFixedHeight(62)
tb.setMovable(False)
tb.setObjectName("mainToolBar")
tb.setStyleSheet(""" #mainToolBar QToolBar {spacing: 1px; padding: 1px;}
QToolBar::separator {background: rgba(222, 222, 222, 0.28); width: 2px; height 2px;
margin-top: 10px; margin-left: 8px; margin-right: 8px; margin-bottom: 10px}
QToolButton {min-height: 21; padding: 0px 10px; font-size: 10pt;}
QToolButton:hover { background: rgba(48, 48, 48, 0.22);}""")
self.addToolBar(tb)
for key in ['new', 'new_project', 'open', 'open_project', 'open_template', 'save', 'cut', 'copy', 'paste', 'undo', 'redo', 'find', 'terminal', 'run', 'build']:
if key in self._actions: tb.addAction(self._actions[key])
if key == 'save' or key == 'redo' or key == 'terminal': tb.addSeparator()
sp = QWidget()
sp.setFixedWidth(8)
sp.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
tb.addWidget(sp)
textbox = CecilPlusTGridBox(parent=None, placeholder="Merge Operator+Enter | Grid Select")
textbox.submitted.connect(lambda text: print("M.O. on Enter:", text))
textbox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
cptgb_action = QWidgetAction(tb)
cptgb_action.setDefaultWidget(textbox)
tb.addAction(cptgb_action)
sp2 = QWidget()
sp2.setFixedWidth(8)
sp2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
tb.addWidget(sp2)
sps_btn = SPSQuantumButton("Set Superlink Priority State", max_value=999)
sps_btn.stateChanged.connect(lambda l, r, t, v, e: print(
f"L={'T' if l else 'F'} R={'T' if r else 'F'} T={'T' if t else 'F'} V={v} E={'T' if e else 'F'}"))
sps_btn.setObjectName("SpsButton")
sps_btn.setStyleSheet("""QPushButton#SpsButton {background: transparent;
border: 1px solid rgba(122,122,122,0.00);
border-radius: 0px; color: #999999; padding: 8px 12px;} QPushButton#SpsButton:hover {
border: 1px solid #59E9EE; background: qlineargradient(
x1:0, y1:0, x2:0, y2:1, stop:0 rgba(222,222,222,0.03), stop:1 rgba(255,255,255,0.03));}
QPushButton#SpsButton:pressed {background-color: rgba(111,111,111,0.35);
padding-top: 10px;} QPushButton#SpsButton QLineEdit {
background: rgba(255,255,255,0.25); border: 1px solid #636363;
color: rgba(230, 250, 250, 0.98); border-radius: 6px; padding: 3px 6px;
selection-background-color: rgba(89,233,238,0.28);min-height: 20px;}
QPushButton#SpsButton QLineEdit:focus {border: 1px solid rgba(89,233,238,0.9);
background: rgba(89,233,238,0.10); color: #59E9EE;}""")
sps_btn.setFixedHeight(52); sps_btn.setFixedWidth(320)
tb.addWidget(sps_btn)
def _iconWithLetter(self, ico, letter, pix_size=28, font_family='Segoe UI',
font_point=7, letter_color=QColor(201, 201, 201)):
pix = ico.pixmap(pix_size, pix_size)
painter = QPainter(pix)
painter.setRenderHint(QPainter.Antialiasing)
font = QFont(font_family, font_point)
font.setBold(True)
painter.setFont(font)
fm = painter.fontMetrics()
txt = letter.upper()
x = (pix.width() - fm.horizontalAdvance(txt)) // 4
y = (pix.height() + fm.ascent()) // 4 - 1
painter.setPen(letter_color)
painter.drawText(x, y, txt)
painter.end()
return QIcon(pix)
def createVertToolBar(self):
tb_vert = QToolBar('Vert')
tb_vert.setOrientation(Qt.Vertical)
tb_vert.setMovable(False)
tb_vert.setIconSize(QSize(32, 32))
tb_vert.setObjectName("vert1ToolBar")
tb_vert.setStyleSheet(""" #vert1ToolBar QToolBar {spacing: 1px; padding: 1px;}
QToolButton {min-height: 21; padding: 0px 10px; font-size: 10pt;}
QToolButton:hover { background: rgba(48, 48, 48, 0.22); }""")
self.addToolBar(Qt.LeftToolBarArea, tb_vert)
icons = [QIcon(ICO_ICO['gray_folder']), QIcon(ICO_ICO['gray_fast_forward'])]
for i, icon in enumerate(icons, start=1):
if i == 1: act = QAction(icon, 'Store Multi-Spectral Span Member Object', self)
else: act = QAction(icon, 'Record Multi-Spectral Span Member Object', self)
act.triggered.connect(lambda checked=False, n=i: print(f'vert toolbar1 action {n}'))
tb_vert.addAction(act)
for ch in ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'):
ic = self._iconWithLetter(icons[1], ch)
act = QAction(ic, f'Procedure Multi-Spectral Span Member Object {ch}', self)
act.triggered.connect(lambda checked=False, c=ch: print(f'spectral multidispatch pre-link', c))
tb_vert.addAction(act)
def currentTabPath(self):
try:
idx = self.tabs.currentIndex()
pth = self._currentFiles[idx]
if pth: return pth
else: return None
except Exception:
return None
def copyFilePath(self):
pth = self.currentTabPath()
if pth: QApplication.clipboard().setText(pth)
def newFile(self):
editor = CodeEditor()
editor.modifiedChanged.connect(self.onModifiedChanged)
self._ut_cnt+=1
idx = self.tabs.addTab(editor, f"untitled({self._ut_cnt})")
self.tabs.setCurrentIndex(idx)
self._currentFiles[idx] = None
def cecilPlusOpenFile(self):
cecil_plus_open_files(self)
f_pth = read_file(OPN_CFG_PTH).split(':)^(:')
if f_pth[0] != 'cancel': self.openFile(f_pth[1])
def openFile(self, path):
for i in range(self.tabs.count()):
if self._currentFiles.get(i) == path:
self.tabs.setCurrentIndex(i)
return
try:
with open(path, 'r', encoding='utf-8') as f: text = f.read()
except Exception as e:
QMessageBox.warning(self, "Open Failed", str(e))
return
editor = CodeEditor()
editor.setPlainText(text)
editor.markSaved()
editor.modifiedChanged.connect(self.onModifiedChanged)
idx = self.tabs.addTab(editor, os.path.basename(path))
self.tabs.setCurrentIndex(idx)
self._currentFiles[idx] = path
self.sb.showMessage(f" > Opened {path}", 3000)
def openTemplate(self):
QMessageBox.information(self, "Open Template", "Open Template")
def openLinkerXML(self):
QMessageBox.information(self, "Open Linker XML", "Open Linker XML")
def openKrakenSQTPP(self):
QMessageBox.information(self, "Open Kraken SQTPP", "Open Kraken SQTPP")
def newProjectDialog(self):
QMessageBox.information(self, "New Project", "New Cecil+ Project...")
def openProjectDialog(self):
QMessageBox.information(self, "Open Project", "Open Cecil+ Project...")
def exportProject(self):
QMessageBox.information(self, "Export Project", "Export Cecil+ Project")
def projectSettings(self):
QMessageBox.information(self, "Project Settings", "Cecil+ Project Settings")
def closeProject(self):
QMessageBox.information(self, "Close Project", "Close Cecil+ Project")
def saveFile(self):
editor = self.currentEditor()
if not editor:
return
idx = self.tabs.currentIndex()
path = self._currentFiles.get(idx)
if not path:
return self.saveFileAs()
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(editor.toPlainText())
editor.markSaved()
self.tabs.setTabText(idx, os.path.basename(path))
self.sb.showMessage(f" > Saved {path}", 3000)
except Exception as e:
QMessageBox.warning(self, "Save Failed", str(e))
def saveFileAs(self):
editor = self.currentEditor()
if not editor:
return
path, _ = QFileDialog.getSaveFileName(self, "Save As", os.path.expanduser("~"))
if not path:
return
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(editor.toPlainText())
idx = self.tabs.currentIndex()
self._currentFiles[idx] = path
editor.markSaved()
self.tabs.setTabText(idx, os.path.basename(path))
self.sb.showMessage(f" > Saved {path}", 3000)
except Exception as e:
QMessageBox.warning(self, "Save Failed", str(e))
def saveAsTemplate(self):
QMessageBox.information(self, "Save As Template", "Save As Template")
def saveAll(self):
for i in range(self.tabs.count()):
editor = self.tabs.widget(i)
if isinstance(editor, CodeEditor) and not editor.isSaved():
self.tabs.setCurrentIndex(i)
self.saveFile()
def applyCircuitComments(self):
print('apply circuit comments executed')
def refreshCircuitComments(self):
print('refresh circuit comments executed')
def removeCircuitComments(self):
print('remove circuit comments executed')
def settings(self):
QMessageBox.information(self, "Settings", "Cecil+ Settings")
def krakenAiSettings(self):
QMessageBox.information(self, "Kraken AI Settings", "Cecil+ Kraken AI Settings")
def expressionUnitSettings(self):
QMessageBox.information(self, "Expression Unit Settings", "Cecil+ Expression Unit Settings(KRAKEN AI)")
def linearCommutatorSettings(self):
QMessageBox.information(self, "Linear Commutator Settings", "Cecil+ Linear Commutator Settings")
def linkerProperties(self):
QMessageBox.information(self, "Linker Properties", "Cecil+ Linker Properties")
def linkerSymbols(self):
QMessageBox.information(self, "Linker Symbols", "Cecil+ Linker Symbols")
def linkerMethodsPMQ(self):
QMessageBox.information(self, "Linker Methods PMQ", "Cecil+ Linker Methods PMQ")
def currentEditor(self):
w = self.tabs.currentWidget()
if isinstance(w, CodeEditor):
return w
return None
def closeTab(self, idx):
editor, bypass, newMap = self.tabs.widget(idx), False, {}
if isinstance(editor, CodeEditor) and not editor.isSaved():
resp = QMessageBox.question(self, "Unsaved", "Save changes before closing?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if resp == QMessageBox.Cancel: bypass = True
if resp == QMessageBox.Yes and not bypass:
self.tabs.setCurrentIndex(idx)
self.saveFile()
if not bypass:
t_cnt = self.tabs.count()
if t_cnt == 0: self.tabs.removeTab(0)
else: self.tabs.removeTab(idx)
for i in range(t_cnt): newMap[i] = self._currentFiles.get(i if i < idx else i + 1)
self._currentFiles = newMap
def onModifiedChanged(self, modified):
idx = self.tabs.currentIndex()
if idx < 0:
return
if self._currentFiles.get(idx): base = os.path.basename(self._currentFiles.get(idx))
else: base = self.tabs.tabText(idx)
if modified:
if not self.tabs.tabText(idx).endswith('*'): self.tabs.setTabText(idx, base + "*")
else: self.tabs.setTabText(idx, base)
def openFind(self):
if os.path.isfile(FND_CFG_PTH):
fnd_cfg = read_file(FND_CFG_PTH)
if fnd_cfg.find('closed:') > -1:
fd = FindDialog(self)
fd.editor = self.currentEditor()
fd.show()
else:
fd = FindDialog(self)
fd.editor = self.currentEditor()
fd.show()
def findInFiles(self):
pass
def formatDocument(self):
editor = self.currentEditor()
if not editor:
return
pass
def _is_readonly(self, editor):
if not editor:
return True
if hasattr(editor, "isReadOnly"):
try:
return editor.isReadOnly()
except Exception:
pass
return False
def char_map(self):
char_map_dlg = CharMapDialog(initial_text="")
char_map_dlg.show()
def copy(self):
editor = self.currentEditor()
if not editor:
return
if hasattr(editor, "copy"):
try:
editor.copy()
return
except Exception:
pass
try:
cursor = editor.textCursor()
text = cursor.selectedText()
if text:
QApplication.clipboard().setText(text)
except Exception:
pass
def cut(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
if hasattr(editor, "cut"):
try:
editor.cut()
return
except Exception:
pass
try:
cursor = editor.textCursor()
text = cursor.selectedText()
if text:
QApplication.clipboard().setText(text)
cursor.removeSelectedText()
editor.setTextCursor(cursor)
except Exception:
pass
def paste(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
if hasattr(editor, "paste"):
try:
editor.paste()
return
except Exception:
pass
try:
clip = QApplication.clipboard().text()
if not clip:
return
if hasattr(editor, "insertPlainText"):
cursor = editor.textCursor()
cursor.insertText(clip)
editor.setTextCursor(cursor)
else:
if hasattr(editor, "toPlainText") and hasattr(editor, "setPlainText"):
txt = editor.toPlainText()
cursor = editor.textCursor()
pos = cursor.position()
new = txt[:pos] + clip + txt[pos:]
editor.setPlainText(new)
c = editor.textCursor()
c.setPosition(pos + len(clip))
editor.setTextCursor(c)
except Exception:
pass
def select_all(self):
editor = self.currentEditor()
if not editor:
return
editor.selectAll()
def undo(self):
editor = self.currentEditor()
if not editor:
return
if hasattr(editor, "undo"):
try:
editor.undo()
return
except Exception:
pass
def redo(self):
editor = self.currentEditor()
if not editor:
return
if hasattr(editor, "redo"):
try:
editor.redo()
return
except Exception:
pass
def _current_line_cursor(self, editor):
cursor = editor.textCursor()
cursor.beginEditBlock() if hasattr(cursor, "beginEditBlock") else None
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
return cursor
def insert_comment(self):
try:
editor = self.currentEditor()
if not editor:
return
read_cur = editor.textCursor()
read_cur.movePosition(QTextCursor.StartOfLine)
read_cur.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor)
line_start = read_cur.selectionStart()
line_end = read_cur.selectionEnd()
line = read_cur.selectedText()
m = re.match(r'(\s*)(//)(.*)', line)
if m:
leading = m.group(1)
caret_pos = line_start+len(leading)+2
new_cursor = editor.textCursor
new_cursor.setPosition(caret_pos)
new_cursor.clearSelection()
editor.setTextCursor(new_cursor)
return
m2 = re.match(r'(\s*)(.*)', line)
leading, rest = m2.groups()
new_line = leading + '//' + rest
edit_cur = editor.textCursor()
editor.blockSignals(True)
try:
edit_cur.beginEditBlock()
edit_cur.setPosition(line_start)
edit_cur.setPosition(line_end, QTextCursor.KeepAnchor)
edit_cur.insertText(new_line)
caret_pos = line_start+len(leading)+2
edit_cur.setPosition(caret_pos)
edit_cur.clearSelection()
edit_cur.endEditBlock()
except Exception:
try:
edit_cur.endEditBlock()
except Exception:
pass
finally:
editor.blockSignals(False)
editor.setTextCursor(edit_cur)
except Exception:
pass
def combine_strict_modifier(self, line):
r_lst, rc_lst = [r'Int\(.*?\)', r'Float\(.*?\)', r'Complex\(.*?\)', r'Bool\(.*?\)'], []
for rl in r_lst:
fnd_lst = re.findall(rl, line)
if len(fnd_lst) > 0: rc_lst.append(fnd_lst)
if len(rc_lst) > 0:
r_lst = []
for rc in rc_lst:
if len(rc) == 1:
line = line.replace(rc[0], '')
r_lst.append(rc[0])
else:
for rc_c in rc:
line = line.replace(rc_c, '')
r_lst.append(rc_c)
return f"{line}operator[?]::<{','.join(r_lst)}>"
else:
return None
def _get_editor_line(self):
try:
editor = self.currentEditor()
if not editor:
return
self._doc = editor.document()
self._cur = editor.textCursor()
self._block = self._cur.block()
self._start = self._block.position()
self._length = max(0, self._block.length() - 1)
self._end = self._start + self._length
tc = QTextCursor(self._doc)
tc.setPosition(self._start)
tc.setPosition(self._end, QTextCursor.KeepAnchor)
line = tc.selectedText()
return line
except Exception:
return None
def _set_editor_line(self, new_line):
try:
rc = QTextCursor(self._doc)
rc.beginEditBlock()
try:
rc.setPosition(self._start)
rc.setPosition(self._end, QTextCursor.KeepAnchor)
rc.insertText(new_line)
finally:
rc.endEditBlock()
orig_col = self._cur.position() - self._start
new_col = max(0, min(len(new_line), orig_col))
nc = QTextCursor(self._doc)
nc.setPosition(self._start + new_col)
editor = self.currentEditor()
editor.setTextCursor(nc)
self._doc = None
except Exception:
return
def join_strict_operator(self):
line, new_line = self._get_editor_line(), None
if line:
new_line = self.combine_strict_modifier(line)
else:
return
if not new_line:
return
self._set_editor_line(new_line)
def select_line(self):
editor = self.currentEditor()
if not editor:
return
try:
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
editor.setTextCursor(cursor)
except Exception:
pass
def del_line_whitespace(self):
editor = self.currentEditor()
if not editor:
return
try:
cursor = editor.textCursor()
cursor.movePosition(cursor.StartOfLine)
cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor)
line_text = cursor.selectedText()
new_text = re.sub(r'[ \t]+$', '', line_text)
if new_text != line_text:
cursor.insertText(new_text)
editor.setTextCursor(cursor)
except Exception:
pass
def delete_line(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = getattr(editor, "document", None)
if callable(doc):
doc = editor.document()
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor)
cursor.removeSelectedText()
editor.setTextCursor(cursor)
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def duplicate_line(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = getattr(editor, "document", None)
if callable(doc):
doc = editor.document()
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
line_text = cursor.selectedText()
end_pos = cursor.selectionEnd()
insert_cursor = QTextCursor(editor.document())
insert_cursor.setPosition(end_pos)
insert_cursor.insertText("\n" + line_text)
cursor.setPosition(end_pos + 1)
editor.setTextCursor(cursor)
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def move_line_up(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = getattr(editor, "document", None)
if callable(doc):
doc = editor.document()
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfBlock)
start = cursor.position()
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
end = cursor.selectionEnd()
line_text = cursor.selectedText()
if start == 0:
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
return
prev_cursor = QTextCursor(editor.document())
prev_cursor.setPosition(start - 1)
prev_cursor.movePosition(QTextCursor.StartOfBlock)
prev_cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
prev_text = prev_cursor.selectedText()
prev_start = prev_cursor.selectionStart()
prev_end = prev_cursor.selectionEnd()
replace_cursor = QTextCursor(editor.document())
replace_cursor.setPosition(prev_start)
replace_cursor.setPosition(end, QTextCursor.KeepAnchor)
replace_cursor.insertText(line_text + "\n" + prev_text)
new_pos = prev_start
c = QTextCursor(editor.document())
c.setPosition(new_pos)
editor.setTextCursor(c)
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def move_line_down(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = getattr(editor, "document", None)
if callable(doc):
doc = editor.document()
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
cursor.movePosition(QTextCursor.StartOfBlock)
start = cursor.position()
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
end = cursor.selectionEnd()
line_text = cursor.selectedText()
doc_len = editor.document().characterCount()
if end >= doc_len - 1:
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
return
next_cursor = QTextCursor(editor.document())
next_cursor.setPosition(end + 1)
next_cursor.movePosition(QTextCursor.StartOfBlock)
next_cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
next_text = next_cursor.selectedText()
next_start = next_cursor.selectionStart()
next_end = next_cursor.selectionEnd()
replace_cursor = QTextCursor(editor.document())
replace_cursor.setPosition(start)
replace_cursor.setPosition(next_end, QTextCursor.KeepAnchor)
replace_cursor.insertText(next_text + "\n" + line_text)
c = QTextCursor(editor.document())
c.setPosition(next_start)
editor.setTextCursor(c)
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def insert_blank_line_above(self):
editor = self.currentEditor()
if not editor:
return
try:
cursor = editor.textCursor()
if cursor.hasSelection():
cursor.setPosition(cursor.selectionStart())
cursor.movePosition(QTextCursor.StartOfLine)
cursor.insertBlock()
cursor.movePosition(QTextCursor.Up)
editor.setTextCursor(cursor)
except Exception:
pass
def insert_blank_line_below(self):
editor = self.currentEditor()
if not editor:
return
try:
cursor = editor.textCursor()
if cursor.hasSelection():
cursor.setPosition(cursor.selectionEnd())
cursor.movePosition(QTextCursor.EndOfLine)
cursor.insertBlock()
cursor.movePosition(QTextCursor.Up)
editor.setTextCursor(cursor)
except Exception:
pass
def indent_line(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = editor.document() if callable(getattr(editor, "document", None)) else None
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
if not cursor.hasSelection():
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.insertText(INDENT)
editor.setTextCursor(cursor)
else:
start = cursor.selectionStart()
end = cursor.selectionEnd()
c = QTextCursor(editor.document())
c.setPosition(start)
c.movePosition(QTextCursor.StartOfBlock)
while c.position() <= end:
c.insertText(INDENT)
c.movePosition(QTextCursor.EndOfBlock)
if c.atEnd():
break
c.movePosition(QTextCursor.Right)
if c.position() > end:
break
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def unindent_line(self):
editor = self.currentEditor()
if not editor or self._is_readonly(editor):
return
try:
doc = editor.document() if callable(getattr(editor, "document", None)) else None
if doc and hasattr(doc, "beginEditBlock"):
doc.beginEditBlock()
cursor = editor.textCursor()
has_sel = cursor.hasSelection()
start = cursor.selectionStart() if has_sel else None
end = cursor.selectionEnd() if has_sel else None
c = QTextCursor(editor.document())
if has_sel:
c.setPosition(start)
else:
c = editor.textCursor()
c.movePosition(QTextCursor.StartOfBlock)
while True:
c.movePosition(QTextCursor.StartOfBlock)
r = QTextCursor(editor.document())
r.setPosition(c.position())
r.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(INDENT))
s = r.selectedText()
if s == INDENT:
r.removeSelectedText()
else:
to_remove = 0
for ch in s:
if ch == " ":
to_remove += 1
else:
break
if to_remove:
r = QTextCursor(editor.document())
r.setPosition(c.position())
r.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, to_remove)
r.removeSelectedText()
if has_sel:
c.movePosition(QTextCursor.EndOfBlock)
if c.atEnd():
break
c.movePosition(QTextCursor.Right)
if c.position() > end:
break
else:
break
if doc and hasattr(doc, "endEditBlock"):
doc.endEditBlock()
except Exception:
pass
def has_selection(self):
editor = self.currentEditor()
if not editor:
return False
try:
if hasattr(editor, "textCursor"):
return bool(editor.textCursor().hasSelection())
if hasattr(editor, "hasSelection"):
return editor.hasSelection()
except Exception:
pass
return False
def clipboard_has_text(self):
return bool(QApplication.clipboard().text())
def toggleExplorer(self):
visible = not self.fileExplorer.isVisible()
self.fileExplorer.setVisible(visible)
def toggleTerminal(self):
visible = not self.terminalBar.isVisible()
self.terminalBar.setVisible(visible)
def openTerminal(self):
pass
def fold_current_func(self):
editor = self.currentEditor()
if not editor:
return
try:
editor._fold_current(False)
except Exception as err:
print(err)
def unfold_current_func(self):
editor = self.currentEditor()
if not editor:
return
try:
editor._fold_current(True)
except Exception as err:
print(err)
def fold_all_func(self):
editor = self.currentEditor()
if not editor:
return
try:
editor._fold_all()
except Exception:
return
def unfold_all_func(self):
editor = self.currentEditor()
if not editor:
return
try:
editor._unfold_all()
except Exception as err:
return
def toggleZen(self):
is_full = self.isFullScreen()
self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) if not is_full else self.showNormal()
def buildProject(self):
pass
def rebuildProject(self):
pass
def cleanProject(self):
pass
def runWithoutDebug(self):
self.runCurrent()
def startDebugging(self):
pass
def stopDebugging(self):
pass
def openCommandPalette(self):
pass
def openExtensions(self):
pass
def openDocs(self):
pass
def showAbout(self):
ad = AboutDialog(self)
ad.show()
def runCurrent(self):
editor = self.currentEditor()
if not editor:
return
idx = self.tabs.currentIndex()
path = self._currentFiles.get(idx)
if not path:
resp = QMessageBox.question(self, "Run", "Save file before running?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if resp == QMessageBox.Cancel:
return
if resp == QMessageBox.Yes:
self.saveFileAs()
path = self._currentFiles.get(self.tabs.currentIndex())
else:
return
QMessageBox.information(self, "Run", f"Run hook called for: {path}\nCecil+ Compiler.")
self.sb.showMessage(f" > Run invoked for {path}", 3000)
class WorkerSignals(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(object)
class Worker(QRunnable):
def __init__(self, tasks=10):
super().__init__()
self.signals = WorkerSignals()
self.tasks = tasks
self.setAutoDelete(True)
@pyqtSlot()
def run(self):
for i in range(1, self.tasks + 1):
time.sleep(0.11)
pct = int(i / self.tasks * 100)
self.signals.progress.emit(pct)
result = {"status": "ok"}
self.signals.finished.emit(result)
class SplashWindow(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.resize(480, 220)
qr = self.frameGeometry()
cp = QApplication.primaryScreen().availableGeometry().center()
qr.moveCenter(cp); self.move(qr.topLeft())
panel = QFrame(self)
panel.setObjectName("panel")
panel.setGeometry(0, 0, 480, 220)
panel.setStyleSheet("""QFrame#panel {background: #59E9EE; border-radius: 5px;}
QLabel#title {color: #FFFFFF; font-size: 56px;}
QLabel#sub {color: #FFFFFF; font-size: 16px;}
QProgressBar {height: 18px; border-radius: 5px; background: #535353; color:#FFFFFF;}
QProgressBar::chunk {background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #59E9EE, stop:1 #FFFFFF);
border-radius:5px;}""")
v = QVBoxLayout(panel)
v.setContentsMargins(20, 18, 20, 18)
label_title = QLabel("Cecil+")
label_title.setObjectName("title")
label_sub = QLabel(" Loading...")
label_sub.setObjectName("sub")
v.addWidget(label_title)
v.addWidget(label_sub)
v.addStretch(1)
self.progress = QProgressBar()
self.progress.setRange(0, 100)
self.progress.setValue(0)
v.addWidget(self.progress)
self._drag_pos = None
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle('Fusion')
_palette = QPalette()
_palette.setColor(QPalette.Window, QColor(53, 53, 53))
_palette.setColor(QPalette.WindowText, Qt.white)
_palette.setColor(QPalette.Base, QColor(51, 51, 51))
_palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
_palette.setColor(QPalette.ToolTipBase, Qt.black)
_palette.setColor(QPalette.ToolTipText, Qt.white)
_palette.setColor(QPalette.Text, Qt.white)
_palette.setColor(QPalette.Button, QColor(66, 177, 177))
_palette.setColor(QPalette.ButtonText, Qt.white)
_palette.setColor(QPalette.BrightText, Qt.red)
_palette.setColor(QPalette.Link, QColor(42, 130, 218))
_palette.setColor(QPalette.Highlight, QColor(67, 67, 67))
_palette.setColor(QPalette.HighlightedText, QColor(67, 177, 199))
app.setPalette(_palette)
splash = SplashWindow()
splash.show()
threadpool = QThreadPool.globalInstance()
worker = Worker(tasks=12)
worker.signals.progress.connect(lambda p: splash.progress.setValue(p))
def on_finished(result):
main_win = CecilIDE()
main_win.showMaximized()
splash.close()
app.main_win = main_win
worker.signals.finished.connect(on_finished)
threadpool.start(worker)
sys.exit(app.exec_())
@lastforkbender
Copy link
Author

Cecil+ will include a extremely advanced B-Spline NN

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment