227 lines
7.3 KiB
Python
227 lines
7.3 KiB
Python
from PySide6.QtCore import QSize, Qt, Slot, QRect, QFileSystemWatcher, Signal, QTime
|
|
from PySide6.QtGui import QIcon, QAction, QShortcut, QKeySequence, QPainter, QColor, QTextFormat, QPixmap, QFontMetrics
|
|
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QLabel, QVBoxLayout, QHBoxLayout, \
|
|
QGroupBox, QLineEdit, QCheckBox, QSpinBox, QPlainTextEdit, QSizePolicy, QGridLayout, QTextEdit, QScrollArea, QFrame, \
|
|
QDoubleSpinBox, QFormLayout, QTimeEdit
|
|
|
|
import webbrowser
|
|
import validators
|
|
import datetime
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
class LineNumberArea(QWidget):
|
|
def __init__(self, editor):
|
|
QWidget.__init__(self, editor)
|
|
self._code_editor = editor
|
|
|
|
def sizeHint(self):
|
|
return QSize(self._code_editor.line_number_area_width(), 0)
|
|
|
|
def paintEvent(self, event):
|
|
self._code_editor.line_number_area_paint_event(event)
|
|
|
|
|
|
class CodeEditor(QPlainTextEdit):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.line_number_area = LineNumberArea(self)
|
|
|
|
self.blockCountChanged[int].connect(self.update_line_number_area_width)
|
|
self.updateRequest[QRect, int].connect(self.update_line_number_area)
|
|
self.cursorPositionChanged.connect(self.highlight_current_line)
|
|
|
|
def line_number_area_width(self):
|
|
digits = len(str(self.blockCount()))
|
|
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
|
|
return space
|
|
|
|
def resizeEvent(self, e):
|
|
super().resizeEvent(e)
|
|
cr = self.contentsRect()
|
|
self.line_number_area.setGeometry(QRect(cr.left(),
|
|
cr.top(),
|
|
self.line_number_area_width(),
|
|
cr.height()))
|
|
|
|
def line_number_area_paint_event(self, event):
|
|
with QPainter(self.line_number_area) as painter:
|
|
painter.fillRect(event.rect(), Qt.white)
|
|
painter.setFont("Consolas")
|
|
|
|
block = self.firstVisibleBlock()
|
|
block_number = block.blockNumber()
|
|
|
|
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
|
|
bottom = top + self.blockBoundingRect(block).height()
|
|
|
|
while block.isValid() and top <= event.rect().bottom():
|
|
if block.isVisible() and bottom >= event.rect().top():
|
|
painter.setPen(Qt.gray)
|
|
painter.drawText(-2, top,
|
|
self.line_number_area.width(),
|
|
self.fontMetrics().height(),
|
|
Qt.AlignmentFlag.AlignRight,
|
|
f"{block_number + 1}")
|
|
|
|
block = block.next()
|
|
top = bottom
|
|
bottom = top + self.blockBoundingRect(block).height()
|
|
block_number += 1
|
|
|
|
def update_line_number_area_width(self):
|
|
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
|
|
|
|
def update_line_number_area(self, rect, dy):
|
|
if dy:
|
|
self.line_number_area.scroll(0, dy)
|
|
else:
|
|
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
|
|
|
|
if rect.contains(self.viewport().rect()):
|
|
self.update_line_number_area_width()
|
|
|
|
def highlight_current_line(self):
|
|
extra_selections = []
|
|
|
|
if not self.isReadOnly():
|
|
selection = QTextEdit.ExtraSelection()
|
|
selection.format.setBackground(QColor(Qt.lightGray))
|
|
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
|
|
|
|
selection.cursor = self.textCursor()
|
|
selection.cursor.clearSelection()
|
|
|
|
extra_selections.append(selection)
|
|
|
|
self.setExtraSelections(extra_selections)
|
|
|
|
|
|
class SpinBoxWithoutWheel(QSpinBox):
|
|
def wheelEvent(self, event):
|
|
event.ignore()
|
|
|
|
|
|
class DoubleSpinBoxWithoutWheel(QDoubleSpinBox):
|
|
def wheelEvent(self, event):
|
|
event.ignore()
|
|
|
|
|
|
class ProbabilitySpinBox(DoubleSpinBoxWithoutWheel):
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.setSingleStep(0.01)
|
|
self.valueChanged.connect(self.update_color)
|
|
self.update_color()
|
|
|
|
def update_color(self):
|
|
if self.value() > 0:
|
|
self.setStyleSheet("color: black;")
|
|
else:
|
|
self.setStyleSheet("color: gray;")
|
|
|
|
|
|
class TimeEditWithoutWheel(QTimeEdit):
|
|
def wheelEvent(self, event):
|
|
event.ignore()
|
|
|
|
|
|
class DirWidget(QWidget):
|
|
def __init__(self, default_dir: Path | str | None = None):
|
|
super().__init__()
|
|
|
|
layout = QHBoxLayout()
|
|
layout.setSpacing(2)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.dir_lineedit = QLineEdit()
|
|
self.dir_lineedit.setFont("Consolas")
|
|
self.dir_lineedit.setText(default_dir)
|
|
|
|
self.dir_set_btn = QPushButton()
|
|
self.dir_set_btn.setIcon(QIcon("./icons/ic_folder_open_48px.svg"))
|
|
self.dir_set_btn.setIconSize(QSize(16, 16))
|
|
self.dir_set_btn.setToolTip("Set directory with selection dialog")
|
|
|
|
self.dir_open_btn = QPushButton()
|
|
self.dir_open_btn.setIcon(QIcon("./icons/ic_open_in_new_24px.svg"))
|
|
self.dir_open_btn.setIconSize(QSize(16, 16))
|
|
self.dir_open_btn.setToolTip("Open directory")
|
|
|
|
layout.addWidget(self.dir_set_btn)
|
|
layout.addWidget(self.dir_lineedit)
|
|
layout.addWidget(self.dir_open_btn)
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
class UrlLineEdit(QLineEdit):
|
|
|
|
def __init__(self, initial_text: str | None = None):
|
|
super().__init__()
|
|
self.setFont("Consolas")
|
|
self.textChanged.connect(self.color_text)
|
|
self.setText(initial_text)
|
|
|
|
def mousePressEvent(self, event):
|
|
modifiers = QApplication.keyboardModifiers()
|
|
if modifiers == Qt.ControlModifier:
|
|
self.open_url()
|
|
|
|
def open_url(self):
|
|
if validators.url(self.text()):
|
|
webbrowser.open(self.text())
|
|
|
|
def color_text(self):
|
|
if validators.url(self.text()):
|
|
self.setStyleSheet("color: blue;")
|
|
else:
|
|
self.setStyleSheet("color: black;")
|
|
|
|
|
|
class UrlWidget(QWidget):
|
|
def __init__(self, initial_url: str | None = None):
|
|
super().__init__()
|
|
|
|
layout = QHBoxLayout()
|
|
layout.setSpacing(2)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.url_lineedit = UrlLineEdit(initial_url)
|
|
self.url_lineedit.textChanged.connect(self.toggle_url_open_btn)
|
|
|
|
self.url_open_btn = QPushButton()
|
|
self.url_open_btn.setIcon(QIcon("./icons/ic_open_in_new_24px.svg"))
|
|
self.url_open_btn.setIconSize(QSize(16, 16))
|
|
self.url_open_btn.setToolTip("Open in browser")
|
|
self.url_open_btn.clicked.connect(self.url_lineedit.open_url)
|
|
|
|
self.toggle_url_open_btn()
|
|
|
|
layout.addWidget(self.url_lineedit)
|
|
layout.addWidget(self.url_open_btn)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def toggle_url_open_btn(self):
|
|
url_valid = bool(validators.url(self.url_lineedit.text()))
|
|
self.url_open_btn.setEnabled(url_valid)
|
|
|
|
|
|
class TelegraphingCheckbox(QCheckBox):
|
|
hover = Signal(str)
|
|
|
|
def __init__(self, text: str, description_text: str | None = None):
|
|
super().__init__(text)
|
|
|
|
self.description_text = description_text
|
|
|
|
def enterEvent(self, event):
|
|
self.hover.emit(self.description_text)
|
|
|
|
def leaveEvent(self, event):
|
|
self.hover.emit("")
|