Compare commits
7 Commits
9c64acf77e
...
69dd5ed1e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 69dd5ed1e3 | |||
| 8c740da879 | |||
| ddd377da7e | |||
| 8cf02c8f6a | |||
| 871cb4e08a | |||
| ed450424a5 | |||
| 00d4f2317a |
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@@ -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
3
.idea/ravenlog.iml
generated
@@ -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>
|
||||||
@@ -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",
|
|
||||||
"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()
|
||||||
|
|||||||
Reference in New Issue
Block a user