Compare commits

...

7 Commits

Author SHA1 Message Date
69dd5ed1e3 update to python3.12 2025-03-23 14:01:06 +01:00
8c740da879 horizontal scrolling 2024-11-24 09:20:57 +01:00
ddd377da7e change font size via mouse wheel 2024-11-23 08:55:25 +01:00
8cf02c8f6a cleanup 2024-11-09 08:11:59 +01:00
871cb4e08a seems to be working quite great 2024-10-24 19:09:51 +02:00
ed450424a5 somewhat working state 2024-08-03 08:52:37 +02:00
00d4f2317a somewhat working state 2024-07-06 10:22:22 +02:00
3 changed files with 227 additions and 38 deletions

4
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.11 (krowlog)" /> <option name="sdkName" value="Python 3.12 (krowlog)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (krowlog)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (krowlog)" project-jdk-type="Python SDK" />
</project> </project>

3
.idea/ravenlog.iml generated
View File

@@ -8,8 +8,9 @@
<excludeFolder url="file://$MODULE_DIR$/build" /> <excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" /> <excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/icons-not-used" /> <excludeFolder url="file://$MODULE_DIR$/icons-not-used" />
<excludeFolder url="file://$MODULE_DIR$/venv312" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.11 (krowlog)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.12 (krowlog)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@@ -9,9 +9,9 @@ from io import TextIOWrapper
from typing import List from typing import List
from PySide6 import QtCore, QtGui from PySide6 import QtCore, QtGui
from PySide6.QtGui import QPaintEvent, QPainter, QFont, QFontMetrics, QColor from PySide6.QtGui import QPaintEvent, QPainter, QFont, QFontMetrics, QColor, QBrush, QWheelEvent
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QStatusBar from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QStatusBar, QGridLayout, QSizePolicy, QScrollBar
from PySide6.QtCore import QTimer, QPoint, Qt from PySide6.QtCore import QTimer, QPoint, Qt, QRect, QLine, Slot
import sys import sys
from src.pluginregistry import PluginRegistry from src.pluginregistry import PluginRegistry
import gettext import gettext
@@ -19,6 +19,8 @@ import gettext
__version__ = '0.2.1' __version__ = '0.2.1'
from src.i18n import _ from src.i18n import _
from src.ui.bigtext.BigScrollBar import BigScrollBar
from src.ui.bigtext.bigtext import InnerBigText
gettext.install('krowlog', 'locale') gettext.install('krowlog', 'locale')
@@ -71,9 +73,9 @@ class MainWindow(QMainWindow):
super(MainWindow, self).__init__(*args, **kwargs) super(MainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle(_("KrowLog")) self.setWindowTitle(_("KrowLog"))
self.setMinimumWidth(600) self.setMinimumWidth(800)
self.setMinimumHeight(480) self.setMinimumHeight(880)
bigger_text = BiggerText() bigger_text = BiggerText(FileModel("testdata/testset.txt"))
self.setCentralWidget(bigger_text) self.setCentralWidget(bigger_text)
self.status_bar = QStatusBar(self) self.status_bar = QStatusBar(self)
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
@@ -100,7 +102,7 @@ class LineType(enum.IntEnum):
class Line: class Line:
def __init__(self, byte_offset: int, byte_end: int, text: str, type: LineType = LineType.Full): def __init__(self, byte_offset: int, byte_end: int, text: str, bytes: str, type: LineType = LineType.Full):
""" """
:type byte_offset: int the offset of the first byte of this line :type byte_offset: int the offset of the first byte of this line
:type byte_end: int the offset of the last byte of this line :type byte_end: int the offset of the last byte of this line
@@ -110,6 +112,7 @@ class Line:
self._byte_offset = byte_offset self._byte_offset = byte_offset
self._byte_end = byte_end self._byte_end = byte_end
self._text = text self._text = text
self._bytes = bytes
self._type = type self._type = type
def byte_offset(self) -> int: def byte_offset(self) -> int:
@@ -121,6 +124,9 @@ class Line:
def text(self) -> str: def text(self) -> str:
return self._text return self._text
def bytes(self) -> str:
return self._bytes
def type(self) -> LineType: def type(self) -> LineType:
return self._type return self._type
@@ -168,7 +174,7 @@ class FileModel:
decoded_line = line.decode(encoding, errors="replace") decoded_line = line.decode(encoding, errors="replace")
line_type = LineType.Begin if previous_line_type == LineType.End or previous_line_type == LineType.Full else LineType.Middle line_type = LineType.Begin if previous_line_type == LineType.End or previous_line_type == LineType.Full else LineType.Middle
return Line(start_of_line, end_of_line, decoded_line, line_type) return Line(start_of_line, end_of_line, decoded_line, line, line_type)
elif pos_of_newline >= 0: elif pos_of_newline >= 0:
start_of_line = read_offset start_of_line = read_offset
end_of_line = read_offset + pos_of_newline end_of_line = read_offset + pos_of_newline
@@ -176,7 +182,7 @@ class FileModel:
decoded_line = line.decode(encoding, errors="replace") decoded_line = line.decode(encoding, errors="replace")
line_type = LineType.Full if previous_line_type == LineType.End or previous_line_type == LineType.Full else LineType.End line_type = LineType.Full if previous_line_type == LineType.End or previous_line_type == LineType.Full else LineType.End
return Line(start_of_line, end_of_line, decoded_line, line_type) return Line(start_of_line, end_of_line, decoded_line, line, line_type)
else: else:
# line does not end in this buffer # line does not end in this buffer
@@ -201,61 +207,243 @@ class FileModel:
return lines return lines
def get_selection(self, byte_start: int, byte_end: int):
with FileWithTimeout.open(self.file, 5, 'rb') as f:
start = min(byte_start, byte_end)
end = max(byte_start, byte_end)
f.seek(start)
b = f.read(end - start)
# print(f"read {end - start } bytes -> {b}")
return b.decode("utf-8", errors="replace")
class SelectionPos:
def __init__(self, index: int, is_in_left_half: bool, num_bytes_of_char: int):
self.index = index
self.is_in_left_half = is_in_left_half
self.num_bytes_of_char = num_bytes_of_char
def __repr__(self):
return f"{self.index}{'🞀' if self.is_in_left_half else '🞂'}({self.num_bytes_of_char})"
def pos(self):
return self.index + (0 if self.is_in_left_half else self.num_bytes_of_char)
class Selection:
def __init__(self, start: SelectionPos = SelectionPos(0, False, 0), end: SelectionPos = SelectionPos(0, False, 0)):
self.start = start
self.end = end
def __repr__(self):
return f"{self.start}:{self.end}"
def min_byte(self) -> int:
return min(self.start.pos(), self.end.pos())
def max_byte(self) -> int:
return max(self.start.pos(), self.end.pos())
class BiggerText(QWidget): class BiggerText(QWidget):
def __init__(self, model: FileModel):
def __init__(self, ):
super(BiggerText, self).__init__() super(BiggerText, self).__init__()
self._model = model
self.grid = QGridLayout()
self.grid.setContentsMargins(0, 0, 0, 0)
self.grid.setHorizontalSpacing(0)
self.grid.setVerticalSpacing(0)
self.setLayout(self.grid)
self.v_scroll_bar = BigScrollBar()
self.big_text_area = BiggerTextArea(self, model, self.v_scroll_bar)
self.big_text_area.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
self.h_scroll_bar = QScrollBar(Qt.Orientation.Horizontal)
self.h_scroll_bar.setMinimum(0)
self.h_scroll_bar.setMaximum(1)
self.h_scroll_bar.valueChanged.connect(self.big_text_area.h_scroll_event)
# self.v_scroll_bar.value_changed.connect(self.big_text_area.v_scroll_value_changed)
# self.v_scroll_bar.scroll_event.connect(self.big_text_area.v_scroll_event)
self.grid.addWidget(self.big_text_area, 0, 1)
self.grid.addWidget(self.h_scroll_bar, 1, 1)
self.grid.addWidget(self.v_scroll_bar, 0, 2)
class BiggerTextArea(QWidget):
def __init__(self, parent: BiggerText, model: FileModel, v_scroll_bar: BigScrollBar):
super(BiggerTextArea, self).__init__()
self.parent = parent
self._v_scroll_bar = v_scroll_bar
self._left_offset = 0
self.longest_line = 1
self._font_size = 20
self.selection = Selection()
self.mouse_pressed = False
self._encoding = "utf8"
self.file_model: FileModel = model
# font ="Andale Mono" # font ="Andale Mono"
# font = "JetBrains Mono" # font = "JetBrains Mono"
# font = "Monospace" # not found # font = "Monospace" # not found
# font = "ZedMono" # is not found # font = "ZedMono" # is not found
# font = "Noto Sans Mono" #font = "Noto Sans Mono"
font = "Noto Color Emoji" font = "Noto Color Emoji"
font_size = 20
qfont = QFont(font, font_size) qfont = QFont(font, self._font_size)
# qfont.setStyleHint(QFont.StyleHint.Monospace) # qfont.setStyleHint(QFont.StyleHint.Monospace)
self.qfont = qfont self.qfont = qfont
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier: if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier:
offset = self.to_byte_offset(e.pos()) self.selection.start = self.to_byte_offset(e.position())
self.selection.end = self.selection.start
self.mouse_pressed = True
self.update()
return def mouseReleaseEvent(self, event):
self.mouse_pressed = False
def to_byte_offset(self, pos: QPoint): def mouseMoveEvent(self, event):
line = self.y_pos_to_line(pos.y()) if self.mouse_pressed:
self.selection.end = self.to_byte_offset(event.position())
#print(f"selection: {self.selection} -> {self.file_model.get_selection(self.selection.min_byte(), self.selection.max_byte())}")
self.update()
def y_pos_to_line(self, y: int) -> int: def wheelEvent(self, event: QWheelEvent):
direction = 1 if event.angleDelta().y() < 0 else -1
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
self._font_size = max(4, min(50, self._font_size - direction))
self.update()
else:
# print("wheel event fired :) %s" % (direction))
self.scroll_by_lines(direction * 3)
@Slot()
def h_scroll_event(self, left_offset: int):
self._left_offset = left_offset
# print("left_offset: %d" % left_offset)
self.update()
def to_byte_offset(self, pos: QPoint) -> SelectionPos:
line_number = self.y_pos_to_line_number_on_screen(pos.y())
line = self.lines_to_render[line_number]
text: str = line.text()
text = text.replace("\n", "").replace("\r", "")
elided_text = self.elided_text(text, pos.x())
byte_offset = line.byte_offset() + len(elided_text.encode("utf8"))
left_x_offset = self.font_metric.horizontalAdvance(elided_text)
next_char = ""
pos_is_in_left_half = False
bytes_of_char = 0
if len(text) > len(elided_text): # has another character
next_char = text[len(elided_text)]
char_with = self.font_metric.horizontalAdvance(next_char)
pos_is_in_left_half = pos.x() < (left_x_offset + char_with / 2)
bytes_of_char = len(next_char.encode("utf8"))
else:
# the position is after the last character / behind the end of the line
pass
print(
f"to_byte_offset({pos.x()}, {pos.y()}) -> {left_x_offset} -- elided_text '{elided_text}' next_char '{next_char}' -> byte_offset {byte_offset} pos_is_in_left_half: {pos_is_in_left_half}")
return SelectionPos(byte_offset, pos_is_in_left_half, bytes_of_char)
def elided_text(self, text: str, width: int):
w = width + self.font_metric.horizontalAdvance("")
elided_text = self.font_metric.elidedText(text + "", Qt.TextElideMode.ElideRight, w,
Qt.TextFlag.TextWrapAnywhere)
elided_text = elided_text[0:-1] if elided_text.endswith('') else elided_text # remove the trailing '…'
return elided_text
def y_pos_to_line_number_on_screen(self, y: int) -> int:
return int(y / self.char_height) return int(y / self.char_height)
def update_longest_line(self, lines: [Line]):
for line in lines:
width_for_full_line = self.font_metric.horizontalAdvance(line.text())
# print("width_in_chars: %d" % width_in_chars)
if self.longest_line < width_for_full_line:
self.longest_line = width_for_full_line
maximum = max(0, self.longest_line - self.width() + 1)
self.parent.h_scroll_bar.setMaximum(round(maximum))
def paintEvent(self, event: QPaintEvent) -> None: def paintEvent(self, event: QPaintEvent) -> None:
start_ns = time.process_time_ns() start_ns = time.process_time_ns()
painter = QPainter(self) painter = QPainter(self)
font = painter.font() font = painter.font()
font.setPointSize(20) font.setPointSize(self._font_size)
painter.setFont(font) painter.setFont(font)
font_metric: QFontMetrics = painter.fontMetrics() # ---
self.char_height = font_metric.height() self.font_metric = painter.fontMetrics()
self.char_height = self.font_metric.height()
lines_to_read = self.height() / self.char_height + 1
lines_to_render = [ self.lines_to_render: [Line] = self.file_model.read(0, lines_to_read, 200, self._encoding)
"im0 ひらがな 王 フーバー 🔴🟢 7²",
"iiiiiiiiii", self.update_longest_line(self.lines_to_render)
"",
"12345678",
"nonspacing marks:",
"next line consists of a%CC%88",
"äääääääääääääääääääääääääääääää|",
"アンドレアス",
"アンドレアス"
]
painter.setPen(QColor(0, 0, 0)) painter.setPen(QColor(0, 0, 0))
line_on_screen = 1 line_on_screen = 1
for line in lines_to_render: for line in self.lines_to_render:
painter.drawText(QPoint(0, line_on_screen * char_height), line) x_start = -1
x_end = -1
# selection starts before line
if self.selection.min_byte() < line.byte_offset():
x_start = 0
# selection starts in line
if line.byte_offset() <= self.selection.min_byte() <= line.byte_end():
left_offset_in_bytes = self.selection.min_byte() - line.byte_offset()
bytes = line.bytes()[0:left_offset_in_bytes]
chars = bytes.decode(self._encoding, errors="replace")
x_start = self.font_metric.horizontalAdvance(chars)
#print(f"width({chars}) -> bounding_rect={self.font_metric.boundingRect(chars).width()}px or horizontalAdvance={self.font_metric.horizontalAdvance(chars)}")
# selection ends after line
if self.selection.max_byte() > line.byte_end():
x_end = self.width()
# selection ends in line
if line.byte_offset() <= self.selection.max_byte() <= line.byte_end():
left_offset_in_bytes = self.selection.max_byte() - line.byte_offset()
bytes = line.bytes()[0:left_offset_in_bytes]
x_end = self.font_metric.horizontalAdvance(bytes.decode(self._encoding, errors="replace")) - x_start
if x_start >= 0 and x_end >= 0:
#print(f"highlighting in line {line_on_screen} -- x_start: {x_start} -> x_end: {x_end}")
prev_brush = painter.brush()
prev_pen = painter.pen()
painter.setBrush(QBrush(QColor(0, 255, 255)))
painter.setPen(QColor(0, 0, 0, 0))
painter.drawRect(
QRect(x_start - self._left_offset,
int(line_on_screen * self.char_height + int(self.char_height * 0.1)), x_end,
-self.char_height))
painter.setBrush(prev_brush)
painter.setPen(prev_pen)
painter.drawText(QPoint(-self._left_offset, line_on_screen * self.char_height), line.text())
line_on_screen = line_on_screen + 1 line_on_screen = line_on_screen + 1
painter.end() painter.end()