Compare commits

...

6 Commits

Author SHA1 Message Date
8ce0c1bf9e switch direction of the range start/end icons
With the old direction they were overlapping each other,
which made it impossible to move the end slider.
2025-03-24 20:07:35 +01:00
04a0310eee bump copyright year 2025-03-24 19:37:37 +01:00
3a2aa4f109 get version from git tag 2025-03-24 19:36:43 +01:00
9902be0a48 feature: follow the file and show always the end when new data comes 2025-03-24 19:20:49 +01:00
61132d242f fix: graphemes are not correctly highlighted
Graphemes don't all have the same width, not even when you use a monospace font.
For latin characters it usually works find to assume the same width. But emojis,
japanese or chinese characters have have different width. There are even some
ultra wide characters like 𒐫 or ﷽. There is also a thing
called 'half-width' character. E.g. the japanese 'a' can be ア or ア.

Fixed by actually computing the width of graphemes and using pixel.
2025-03-24 17:49:27 +01:00
21b2da1e69 update pyside and pyinstaller 2025-03-23 20:54:03 +01:00
19 changed files with 323 additions and 173 deletions

View File

@@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 16 16">
<path style="fill: #dddddd; stroke: black; stroke-linejoin:round;"
d="M8,13
L1,3
L15,3
d="M8,3
L1,13
L15,13
z
"/>
<line x1="0.5" y1="13.5" x2="15.5" y2="13.5" style="stroke:black;"/>
<line x1="0.5" y1="2.5" x2="15.5" y2="2.5" style="stroke:black;"/>
</svg>

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -1,10 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 16 16">
<path style="fill: #dddddd; stroke: black; stroke-linejoin:round;"
d="M8,3
L1,13
L15,13
d="M8,13
L1,3
L15,3
z
"/>
<line x1="0.5" y1="2.5" x2="15.5" y2="2.5" style="stroke:black;"/>
<line x1="0.5" y1="13.5" x2="15.5" y2="13.5" style="stroke:black;"/>
</svg>

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -10,8 +10,10 @@ from src import install
from src.pluginregistry import PluginRegistry
import gettext
from src.ui.icon import Icon
from pathlib import Path
import os
__version__ = '0.2.1'
__version__ = Path(os.path.dirname(os.path.realpath(__file__)) + os.sep + "version.txt").read_text()
gettext.install('krowlog', 'locale')

View File

@@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: RavenLog\n"
"POT-Creation-Date: 2024-03-24 12:00+0100\n"
"PO-Revision-Date: 2024-03-24 12:02+0100\n"
"POT-Creation-Date: 2025-03-24 19:02+0100\n"
"PO-Revision-Date: 2025-03-24 19:17+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de\n"
@@ -15,10 +15,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.0.1\n"
"X-Generator: Poedit 3.5\n"
#: src/mainwindow.py:32 src/plugins/krowlog/aboutdialog.py:30
#: src/plugins/krowlogplugin.py:85
#: src/mainwindow.py:32 src/new_big_text/bigger_text.py:75
#: src/plugins/krowlog/aboutdialog.py:30 src/plugins/krowlogplugin.py:85
msgid "KrowLog"
msgstr "KrowLog"
@@ -146,31 +146,31 @@ msgstr "&Über KrowLog"
msgid "E&xit"
msgstr "&Beenden"
#: src/plugins/logfile/filterwidget.py:181
#: src/plugins/logfile/filterwidget.py:192
msgid "Cancel"
msgstr "Abbrechen"
#: src/plugins/logfile/filterwidget.py:187
#: src/plugins/logfile/filterwidget.py:198
msgid "save query"
msgstr "suche speichern"
#: src/plugins/logfile/filterwidget.py:192
#: src/plugins/logfile/filterwidget.py:203
msgid "ignore case"
msgstr "Groß-/Kleinschreibung ignorieren"
#: src/plugins/logfile/filterwidget.py:196
#: src/plugins/logfile/filterwidget.py:207
msgid "regex"
msgstr "RegExp"
#: src/plugins/logfile/filterwidget.py:206
#: src/plugins/logfile/filterwidget.py:217
msgid "only matches"
msgstr "nur Treffer"
#: src/plugins/logfile/filterwidget.py:298
#: src/plugins/logfile/filterwidget.py:309
msgid "({hits} lines)"
msgstr "({hits} Zeilen)"
#: src/plugins/logfileplugin.py:35 src/ui/bigtext/bigtext.py:258
#: src/plugins/logfileplugin.py:35 src/ui/bigtext/bigtext.py:263
msgid "&Highlighter"
msgstr "&Hervorhebungen"
@@ -200,65 +200,69 @@ msgstr "Öffne Datei"
#: src/plugins/timediff/time_diff_menu_widget.py:32
msgid "ms"
msgstr ""
msgstr "ms"
#: src/plugins/timediff/time_diff_menu_widget.py:33
msgid "s"
msgstr ""
msgstr "s"
#: src/plugins/timediff/time_diff_menu_widget.py:34
msgid "m"
msgstr ""
msgstr "m"
#: src/plugins/timediff/time_diff_menu_widget.py:35
msgid "h"
msgstr ""
msgstr "h"
#: src/ui/bigtext/bigtext.py:238
#: src/ui/bigtext/bigtext.py:243
msgid "&Copy to Clipboard"
msgstr "In Zwischenablage &Kopieren"
#: src/ui/bigtext/bigtext.py:246
#: src/ui/bigtext/bigtext.py:251
msgid "Copy to &File"
msgstr "In &Datei Kopieren"
#: src/ui/bigtext/bigtext.py:252
#: src/ui/bigtext/bigtext.py:257
msgid "Select &All"
msgstr "&Alles Selektieren"
#: src/ui/bigtext/bigtext.py:268
#: src/ui/bigtext/bigtext.py:270
msgid "&Follow"
msgstr "&Folgen"
#: src/ui/bigtext/bigtext.py:280
msgid "Set Range Start"
msgstr "Setze Start des Anzeigebereichs"
#: src/ui/bigtext/bigtext.py:276
#: src/ui/bigtext/bigtext.py:288
msgid "Set Range End"
msgstr "Setze Ende des Anzeigebereichs"
#: src/ui/bigtext/bigtext.py:283
#: src/ui/bigtext/bigtext.py:295
msgid "Reset Range"
msgstr "Anzeigebereich Zurücksetzen"
#: src/ui/bigtext/bigtext.py:458
#: src/ui/bigtext/bigtext.py:526
msgid "warning"
msgstr "Achtung"
#: src/ui/bigtext/bigtext.py:459
#: src/ui/bigtext/bigtext.py:527
msgid "You have selected <b>{0}</b> of data."
msgstr "Du hast <b>{0}</b> selektiert."
#: src/ui/bigtext/bigtext.py:464
#: src/ui/bigtext/bigtext.py:532
msgid "Copy {0} to Clipboard"
msgstr "Kopiere {0} in die Zwischenablage"
#: src/ui/bigtext/bigtext.py:468
#: src/ui/bigtext/bigtext.py:536
msgid "Write to File"
msgstr "Schreibe in Datei"
#: src/ui/bigtext/bigtext.py:492
#: src/ui/bigtext/bigtext.py:560
msgid "Save File"
msgstr "Speichere Datei"
#: src/ui/bigtext/bigtext.py:529
#: src/ui/bigtext/bigtext.py:605
msgid "selected {0} - {1:,.0f}:{2:,.0f}"
msgstr "selektiert {0} - {1:,.0f}:{2:,.0f}"
@@ -306,6 +310,10 @@ msgstr "Zeilenhintergrund"
msgid "Regular Expression"
msgstr "Regulärer Ausdruck"
#: src/ui/bigtext/newhighlightingdialog.py:185
msgid "File Type:"
msgstr "Dateityp:"
#: src/ui/colorbutton.py:20
msgid "Strawberry Cream"
msgstr "Strawberry Cream"
@@ -370,7 +378,7 @@ msgstr "Grau"
msgid "transparent"
msgstr "Transparent"
#: src/ui/rangeslider.py:180
#: src/ui/rangeslider.py:190
msgid "showing bytes {0} to {1} ({2})"
msgstr "Anzeigebereich: Bytes {0} bis {1} ({2})"

View File

@@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-03-24 12:00+0100\n"
"POT-Creation-Date: 2025-03-24 19:02+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -15,8 +15,8 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n"
#: src/mainwindow.py:32 src/plugins/krowlog/aboutdialog.py:30
#: src/plugins/krowlogplugin.py:85
#: src/mainwindow.py:32 src/new_big_text/bigger_text.py:75
#: src/plugins/krowlog/aboutdialog.py:30 src/plugins/krowlogplugin.py:85
msgid "KrowLog"
msgstr ""
@@ -140,31 +140,31 @@ msgstr ""
msgid "E&xit"
msgstr ""
#: src/plugins/logfile/filterwidget.py:181
#: src/plugins/logfile/filterwidget.py:192
msgid "Cancel"
msgstr ""
#: src/plugins/logfile/filterwidget.py:187
#: src/plugins/logfile/filterwidget.py:198
msgid "save query"
msgstr ""
#: src/plugins/logfile/filterwidget.py:192
#: src/plugins/logfile/filterwidget.py:203
msgid "ignore case"
msgstr ""
#: src/plugins/logfile/filterwidget.py:196
#: src/plugins/logfile/filterwidget.py:207
msgid "regex"
msgstr ""
#: src/plugins/logfile/filterwidget.py:206
#: src/plugins/logfile/filterwidget.py:217
msgid "only matches"
msgstr ""
#: src/plugins/logfile/filterwidget.py:298
#: src/plugins/logfile/filterwidget.py:309
msgid "({hits} lines)"
msgstr ""
#: src/plugins/logfileplugin.py:35 src/ui/bigtext/bigtext.py:258
#: src/plugins/logfileplugin.py:35 src/ui/bigtext/bigtext.py:263
msgid "&Highlighter"
msgstr ""
@@ -208,51 +208,55 @@ msgstr ""
msgid "h"
msgstr ""
#: src/ui/bigtext/bigtext.py:238
#: src/ui/bigtext/bigtext.py:243
msgid "&Copy to Clipboard"
msgstr ""
#: src/ui/bigtext/bigtext.py:246
#: src/ui/bigtext/bigtext.py:251
msgid "Copy to &File"
msgstr ""
#: src/ui/bigtext/bigtext.py:252
#: src/ui/bigtext/bigtext.py:257
msgid "Select &All"
msgstr ""
#: src/ui/bigtext/bigtext.py:268
#: src/ui/bigtext/bigtext.py:270
msgid "&Follow"
msgstr ""
#: src/ui/bigtext/bigtext.py:280
msgid "Set Range Start"
msgstr ""
#: src/ui/bigtext/bigtext.py:276
#: src/ui/bigtext/bigtext.py:288
msgid "Set Range End"
msgstr ""
#: src/ui/bigtext/bigtext.py:283
#: src/ui/bigtext/bigtext.py:295
msgid "Reset Range"
msgstr ""
#: src/ui/bigtext/bigtext.py:458
#: src/ui/bigtext/bigtext.py:526
msgid "warning"
msgstr ""
#: src/ui/bigtext/bigtext.py:459
#: src/ui/bigtext/bigtext.py:527
msgid "You have selected <b>{0}</b> of data."
msgstr ""
#: src/ui/bigtext/bigtext.py:464
#: src/ui/bigtext/bigtext.py:532
msgid "Copy {0} to Clipboard"
msgstr ""
#: src/ui/bigtext/bigtext.py:468
#: src/ui/bigtext/bigtext.py:536
msgid "Write to File"
msgstr ""
#: src/ui/bigtext/bigtext.py:492
#: src/ui/bigtext/bigtext.py:560
msgid "Save File"
msgstr ""
#: src/ui/bigtext/bigtext.py:529
#: src/ui/bigtext/bigtext.py:605
msgid "selected {0} - {1:,.0f}:{2:,.0f}"
msgstr ""
@@ -300,6 +304,10 @@ msgstr ""
msgid "Regular Expression"
msgstr ""
#: src/ui/bigtext/newhighlightingdialog.py:185
msgid "File Type:"
msgstr ""
#: src/ui/colorbutton.py:20
msgid "Strawberry Cream"
msgstr ""
@@ -364,7 +372,7 @@ msgstr ""
msgid "transparent"
msgstr ""
#: src/ui/rangeslider.py:180
#: src/ui/rangeslider.py:190
msgid "showing bytes {0} to {1} ({2})"
msgstr ""

View File

@@ -1,6 +1,8 @@
import PyInstaller.__main__
import os
os.system("git -C . describe --match \"*.*.*\" --tags > version.txt")
PyInstaller.__main__.run([
'krowlog.py',
# '--onefile',
@@ -12,6 +14,7 @@ PyInstaller.__main__.run([
'--add-binary', 'icons' + os.pathsep + 'icons',
'--add-binary', 'locales' + os.pathsep + 'locales',
'--add-binary', 'LICENSE' + os.pathsep + '.',
'--add-binary', 'version.txt' + os.pathsep + '.',
'--hidden-import=krowlog',
'--hidden-import=watchdog',
'--hidden-import=watchdog.observers',

View File

@@ -1,5 +1,5 @@
pip==24.0
PySide6_Essentials==6.7.0
setuptools==69.5.1
watchdog==4.0.0
pyinstaller==6.6.0
pip==25.0.1
PySide6_Essentials==6.8.2.1
setuptools==77.0.3
watchdog==6.0.0
pyinstaller==6.12.0

View File

@@ -63,7 +63,7 @@ class AboutDialog(QDialog):
result.layout = QVBoxLayout(result)
label = Label("{0}<br>{1}<br>{2}".format(
_("KrowLog is a viewer for log files of arbitrary size."),
_("(c) 2022-2024 Andreas Huber"),
_("(c) 2022-2025 Andreas Huber"),
_("License: LGPL v3")
))
result.layout.addWidget(label)

View File

@@ -237,7 +237,7 @@ class FilterWidget(QWidget):
(handle, self.tmp_filename) = tempfile.mkstemp()
os.close(handle)
self.filter_model = LogFileModel(self.tmp_filename, self.source_model.settings, source_model.get_file())
self.hits_view = BigText(self.filter_model, show_range_slider=False)
self.hits_view = BigText(self.filter_model, show_range_slider=False, show_follow_action=False)
self.layout.addWidget(filter_bar)
self.layout.addWidget(self.hits_view)

View File

@@ -14,6 +14,9 @@ class BigScrollBar(QWidget):
code involved. We work around this by converting the python int
into a string."""
user_interaction = Signal()
"""signals that the user changed a value (the opposite is that some code changed the value) """
class ScrollEvent(enum.IntEnum):
PageUp = 1
PageDown = 2
@@ -138,21 +141,27 @@ class BigScrollBar(QWidget):
trigger_repeat_action = True
match self.repeat_action_control:
case QStyle.SubControl.SC_ScrollBarAddPage:
self.user_interaction.emit()
self.scroll_event.emit(self.ScrollEvent.PageDown)
case QStyle.SubControl.SC_ScrollBarSubPage:
self.user_interaction.emit()
self.scroll_event.emit(self.ScrollEvent.PageUp)
if self.value <= self.minimum:
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarAddLine:
self.user_interaction.emit()
self.scroll_event.emit(self.ScrollEvent.LinesDown)
case QStyle.SubControl.SC_ScrollBarSubLine:
self.user_interaction.emit()
self.scroll_event.emit(self.ScrollEvent.LinesUp)
if self.value <= self.minimum:
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarFirst:
self.user_interaction.emit()
self.set_value(self.minimum)
trigger_repeat_action = False
case QStyle.SubControl.SC_ScrollBarLast:
self.user_interaction.emit()
self.set_value(self.maximum)
trigger_repeat_action = False
case _:
@@ -180,6 +189,7 @@ class BigScrollBar(QWidget):
if not r.contains(event.position().toPoint()):
new_position = self.snap_back_position
self.user_interaction.emit()
# print(f"move to value: {new_position}")
self.set_value(new_position)
@@ -227,7 +237,10 @@ class BigScrollBar(QWidget):
self.repeat_action_timer.stop()
#self.update()
def set_value(self, value: int):
def set_value(self, value: int, emit_change_event=True):
changed = self.value != value
self.value = value
self.value_changed.emit(str(self.value))
if emit_change_event and changed:
# print(f"emitting value changed: {self.value}")
self.value_changed.emit(str(self.value))
self.update()

View File

@@ -18,6 +18,7 @@ from src.ui.bigtext.highlighted_range import HighlightedRange
from src.ui.bigtext.line import Line
from src.ui.bigtext.logFileModel import LogFileModel
from src.ui.bigtext.newhighlightingdialog import NewHighlightingDialog
from src.ui.bigtext.selectionPos import SelectionPos
from src.ui.icon import Icon
from src.ui.rangeslider import RangeSlider
from src.util.conversion import humanbytes
@@ -30,6 +31,7 @@ from src.i18n import _
log = logging.getLogger("bigtext")
class FileObserver(FileSystemEventHandler):
def __init__(self, big_text):
@@ -75,10 +77,11 @@ class FileWatchdogThread(QRunnable):
class BigText(QWidget):
trigger_update = Signal()
def __init__(self, model: LogFileModel, show_range_slider=True):
def __init__(self, model: LogFileModel, show_range_slider=True, show_follow_action=True):
super(BigText, self).__init__()
self.show_range_slider = show_range_slider
self.show_follow_action = show_follow_action
self.model = model
self.grid = QGridLayout()
@@ -100,6 +103,7 @@ class BigText(QWidget):
# self.v_scroll_bar.setPageStep(1)
self.v_scroll_bar.value_changed.connect(self.big_text.v_scroll_value_changed)
self.v_scroll_bar.scroll_event.connect(self.big_text.v_scroll_event)
self.v_scroll_bar.user_interaction.connect(self.big_text.user_scroll_interaction)
if show_range_slider:
self.range_limit = RangeSlider()
@@ -145,13 +149,15 @@ class BigText(QWidget):
# noinspection PyArgumentList,PyTypeChecker
class InnerBigText(QWidget):
_byte_offset = 0
_left_offset = 0 # number of characters the horizontal scrollbar was moved to the right
_left_offset = 0 # number of pixels the horizontal scrollbar was moved to the right
scroll_lines = 0
longest_line = 0
_range_start = 0
_range_end = -1
_follow = False
def __init__(self, parent: BigText, model: LogFileModel, v_scaled_scrollbar: ScaledScrollBar):
super(InnerBigText, self).__init__()
self.char_height = None
@@ -260,6 +266,12 @@ class InnerBigText(QWidget):
manage_highlighting.setShortcut("CTRL+H")
menu.addAction(manage_highlighting)
if self.parent.show_follow_action:
follow = QAction(_("&Follow"), self, triggered=self._toggle_follow)
follow.setCheckable(True)
follow.setChecked(self._follow)
menu.addAction(follow)
if self.parent.show_range_slider:
menu.addSeparator()
@@ -307,8 +319,12 @@ class InnerBigText(QWidget):
self.parent.range_limit.set_range_start(0)
self.parent.range_limit.set_range_end(self.model.byte_count())
def user_scroll_interaction(self):
self._follow = False
def scroll_by_lines(self, scroll_lines: int):
self.scroll_lines = scroll_lines
self._follow = False
self.update()
self.parent.v_scroll_bar.set_value(self._byte_offset)
@@ -320,8 +336,8 @@ class InnerBigText(QWidget):
# noinspection PyTypeChecker
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.ShiftModifier:
offset = self.to_byte_offset(e)
self.selection_highlight.set_end_byte(offset)
selection_pos = self.to_byte_offset(e)
self.selection_highlight.set_end_byte(selection_pos)
self._update_highlight_selected_text()
self.update()
return
@@ -331,16 +347,16 @@ class InnerBigText(QWidget):
line_number = self.y_pos_to_line(e.pos().y())
if line_number == self._last_double_click_line_number and line_number < len(self.lines):
line: Line = self.lines[line_number]
self.selection_highlight.set_start(line.byte_offset())
self.selection_highlight.set_end_byte(line.byte_end())
self.selection_highlight.set_start(SelectionPos(line.byte_offset(), True, 1))
self.selection_highlight.set_end_byte(SelectionPos(line.byte_end() - 1, False, 1))
self._update_highlight_selected_text()
self.update()
return
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier:
offset = self.to_byte_offset(e)
self.selection_highlight.set_start(offset)
self.selection_highlight.set_end_byte(offset)
selection_pos = self.to_byte_offset(e)
self.selection_highlight.set_start(selection_pos)
self.selection_highlight.set_end_byte(selection_pos)
self._update_highlight_selected_text()
self.update()
@@ -355,14 +371,17 @@ class InnerBigText(QWidget):
self._last_double_click_time = time.time()
self._last_double_click_line_number = self.y_pos_to_line(e.pos().y())
offset = self.to_byte_offset(e)
(_word, start_byte, end_byte) = self.model.read_word_at(offset)
selection_pos = self.to_byte_offset(e)
(word, start_byte, end_byte) = self.model.read_word_at(selection_pos.pos())
if start_byte >= 0 and end_byte >= 0:
self.selection_highlight.set_start(start_byte)
self.selection_highlight.set_end_byte(end_byte)
bytes_of_first_char = len(f"{word[0]}".encode("utf8"))
self.selection_highlight.set_start(SelectionPos(start_byte, True, bytes_of_first_char))
bytes_of_last_char = len(f"{word[-1]}".encode("utf8"))
self.selection_highlight.set_end_byte(
SelectionPos(end_byte - bytes_of_last_char, False, bytes_of_last_char))
else:
self.selection_highlight.set_start(offset)
self.selection_highlight.set_end_byte(offset)
self.selection_highlight.set_start(selection_pos)
self.selection_highlight.set_end_byte(selection_pos)
self._update_highlight_selected_text()
self.update()
@@ -372,10 +391,10 @@ class InnerBigText(QWidget):
if e.buttons() != Qt.MouseButton.LeftButton:
return
current_byte = self.to_byte_offset(e)
selection_pos = self.to_byte_offset(e)
if self.selection_highlight.end_byte != current_byte:
self.selection_highlight.set_end_byte(current_byte)
if self.selection_highlight.max_byte() != selection_pos.pos():
self.selection_highlight.set_end_byte(selection_pos)
self._update_highlight_selected_text()
self.update()
# print("-> %s,%s" %(self._selection_start_byte, self._selection_end_byte))
@@ -386,10 +405,16 @@ class InnerBigText(QWidget):
self.scroll_by_lines(-1)
if line_number > int(self.lines_shown()):
self.scroll_by_lines(1)
if column_in_line <= 1:
# if column_in_line <= 1:
# self._left_offset = max(0, self._left_offset - 2)
# self.update()
if e.pos().x() <= 1:
self._left_offset = max(0, self._left_offset - 2)
self.update()
if column_in_line + 1 >= self.columns_shown():
# if column_in_line + 1 >= self.columns_shown():
# self._left_offset = self._left_offset + 2
# self.update()
if e.pos().x() + 1 >= self.width():
self._left_offset = self._left_offset + 2
self.update()
@@ -416,12 +441,13 @@ class InnerBigText(QWidget):
case BigScrollBar.ScrollEvent.PageDown:
self.scroll_by_lines(int(self.lines_shown()) - 1)
def update_longest_line(self, length: int):
width_in_chars = self.width() / self.char_width
def update_longest_line(self, line: Line):
# print("width_in_chars: %d" % width_in_chars)
if self.longest_line < length:
self.longest_line = length
maximum = max(0, length - width_in_chars + 1)
text_width_in_px = line.get_width_in_px(self.font_metric);
if self.longest_line < text_width_in_px:
self.longest_line = text_width_in_px
maximum = max(0, text_width_in_px - self.width() + 1)
self.parent.h_scroll_bar.setMaximum(round(maximum))
def y_pos_to_line(self, y: int) -> int:
@@ -436,29 +462,63 @@ class InnerBigText(QWidget):
def columns_shown(self) -> float:
return self.width() / float(self.char_width)
def to_byte_offset(self, e: QMouseEvent) -> int:
def to_byte_offset(self, e: QMouseEvent) -> SelectionPos:
x = e.pos().x() + self._left_offset
line_number = self.y_pos_to_line(e.pos().y())
if line_number < len(self.lines):
line: Line = self.lines[line_number]
column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset
column_in_line = min(column_in_line, line.length_in_columns()) # x was behind the last column of this line
char_in_line = line.column_to_char(column_in_line)
# print("%s in line %s column_in_line=%s" % (char_in_line, line_number, column_in_line))
byte_in_line = line.char_index_to_byte(char_in_line)
current_byte = line.byte_offset() + byte_in_line
# print("%s + %s = %s" % (line.byte_offset(), char_in_line, current_byte))
text: str = line.line()
text = text.replace("\n", "").replace("\r", "")
elided_text: str = self.elided_text(text, 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_width = self.font_metric.horizontalAdvance(next_char)
pos_is_in_left_half = x < (left_x_offset + char_width / 2)
bytes_of_char = len(next_char.encode("utf8"))
else:
# print(f"{x} is after last char, elided_text={elided_text}")
# the position is after the last character / behind the end of the line
pass
# print(f"{x} -> {byte_offset} {'left' if pos_is_in_left_half else 'right'} bytes_of_char={bytes_of_char}")
return SelectionPos(byte_offset, pos_is_in_left_half, bytes_of_char)
# column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset
# column_in_line = min(column_in_line, line.length_in_columns()) # x was behind the last column of this line
# char_in_line = line.column_to_char(column_in_line)
# # print("%s in line %s column_in_line=%s" % (char_in_line, line_number, column_in_line))
# byte_in_line = line.char_index_to_byte(char_in_line)
# current_byte = line.byte_offset() + byte_in_line
# # print("%s + %s = %s" % (line.byte_offset(), char_in_line, current_byte))
else:
current_byte = self.model.byte_count()
return current_byte
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 _has_selection(self):
return self.selection_highlight.start_byte != self.selection_highlight.end_byte
return self.selection_highlight.min_byte() != self.selection_highlight.max_byte()
def copy_selection(self):
if self._has_selection():
start = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
start = self.selection_highlight.min_byte()
end = self.selection_highlight.max_byte()
bytes_human_readable = humanbytes(end - start)
if end - start > (1024 ** 2) * 5:
you_sure = QMessageBox(
@@ -492,8 +552,8 @@ class InnerBigText(QWidget):
def _copy_selection_to_file(self):
if self._has_selection():
start = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
start = self.selection_highlight.min_byte()
end = self.selection_highlight.max_byte()
dialog = QFileDialog(self)
(selected_file, _filter) = dialog.getSaveFileName(
parent=self,
@@ -507,18 +567,26 @@ class InnerBigText(QWidget):
PluginRegistry.execute("open_file", selected_file)
def _select_all(self):
self.selection_highlight.start_byte = self.model.get_line_start_at(self._range_start)
start_byte = self.model.get_line_start_at(self._range_start)
if self._range_end < 0 or self.model.byte_count() <= self._range_end:
self.selection_highlight.end_byte = self.model.byte_count()
end_byte = self.model.byte_count()
else:
self.selection_highlight.end_byte = self.model.get_line_start_at(self._range_end)
end_byte = self.model.get_line_start_at(self._range_end)
self.selection_highlight.set_start(SelectionPos(start_byte, True, 1))
self.selection_highlight.set_end_byte(SelectionPos(end_byte, False, 1))
self._update_highlight_selected_text()
self.update()
def _toggle_follow(self):
self._follow = not self._follow
print(f"follow={self._follow}")
self.update()
def _update_highlight_selected_text(self):
start_byte = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
end_byte = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
start_byte = self.selection_highlight.min_byte()
end_byte = self.selection_highlight.max_byte()
self._update_status_bar(start_byte, end_byte)
@@ -545,14 +613,30 @@ class InnerBigText(QWidget):
def paintEvent(self, event: QPaintEvent) -> None:
start_ns = time.process_time_ns()
painter = QPainter(self)
# font = "Courier New" if sys.platform == 'win32' or sys.platform == 'cygwin' else "Monospace"
painter.setFont(QFont("Courier New", self.model.settings.getint_session('general', "font_size")))
# "Courier New"
# "JetBrains Mono"
# "Noto Sans Mono"
# "Noto Color Emoji"
# "Andale Mono"
qfont = QFont("Monospace", self.model.settings.getint_session('general', "font_size"))
qfont.setStyleHint(QFont.StyleHint.Monospace)
painter.setFont(qfont)
self.font_metric = painter.fontMetrics()
painter.setPen(QColor(0, 0, 0))
self.update_font_metrics(painter)
lines_to_show = math.ceil(self.lines_shown())
# print("%s / %s = %s" %(self.height(), float(self.char_height), lines_to_show))
if self._range_end >= 0 and self._follow:
self.scroll_lines = 0
self._byte_offset = self.model.byte_count() - 1
self.parent.v_scroll_bar.set_value(self._byte_offset)
self.lines = self.model.data(self._byte_offset, self.scroll_lines, lines_to_show, self._range_start,
self._range_end)
# print("lines_to_show: %d returned: %d" % (lines_to_show, len(self.lines)))
@@ -567,7 +651,7 @@ class InnerBigText(QWidget):
self.parent.range_limit.set_maximum(byte_count)
for line in self.lines:
self.update_longest_line(len(line.line()))
self.update_longest_line(line)
highlighters = self.model.highlighters()
if self.model.get_query_highlight():
@@ -586,23 +670,24 @@ class InnerBigText(QWidget):
if optional_highlight_range:
highlight_ranges = highlight_ranges + optional_highlight_range
self.draw_highlights(highlight_ranges, painter, y_line_offset)
self.draw_highlights(highlight_ranges, painter, y_line_offset, line)
y_line_offset = y_line_offset + self.char_height
left_offset = int(-1 * self._left_offset * self.char_width)
# left_offset = int(-1 * self._left_offset * self.char_width)
left_offset = int(-1 * self._left_offset)
y_line_offset = self.char_height
for line in self.lines:
text = line.line_prepared_for_display()
text = text[self._left_offset:self._left_offset + math.ceil(
self.columns_shown())] # reduce string to the visible section before drawing
painter.drawText(0, y_line_offset, text)
# text = text[self._left_offset:self._left_offset + math.ceil(
# self.columns_shown())] # reduce string to the visible section before drawing
painter.drawText(-self._left_offset, y_line_offset, text)
y_line_offset = y_line_offset + self.char_height
painter.end()
end_ns = time.process_time_ns()
# print(f"paint took {(end_ns - start_ns) / 1000000.0}")
def draw_highlights(self, highlights: [HighlightedRange], painter: QPainter, y_line_offset: int):
def draw_highlights(self, highlights: [HighlightedRange], painter: QPainter, y_line_offset: int, line: Line):
for highlight in highlights:
if highlight.is_highlight_full_line():
@@ -612,15 +697,17 @@ class InnerBigText(QWidget):
self.highlight_background(painter, rect, highlight.get_brush_full_line())
for highlight in highlights:
left_offset = self._left_offset * self.char_width
x1 = highlight.get_start() * self.char_width
width = highlight.get_width() * self.char_width
x1 = self.font_metric.horizontalAdvance(
line.prefix_bytes(highlight.get_start()).decode("utf8", errors="replace"))
width = self.font_metric.horizontalAdvance(
line.substr_bytes(highlight.get_start(), highlight.get_width()).decode("utf8", errors="replace"))
y1 = y_line_offset - self.char_height + self.char_height / 7
height = self.char_height
left = round(x1 - left_offset)
if x1 + width < left_offset \
or x1 > left_offset + self.width():
left = round(x1 - self._left_offset)
if x1 + width < self._left_offset \
or x1 > self._left_offset + self.width():
# too far left or too far right
continue

View File

@@ -71,12 +71,15 @@ class HighlightRegex(Highlight):
# but we only want to highlight the groups
first_group = 1 if len(match.groups()) > 0 else 0
for i in range(first_group, len(match.groups()) + 1):
start_column = line.char_to_column(match.start(i))
end_column = line.char_to_column(match.end(i))
start_char_index = match.start(i)
start_byte_index = len(line.prefix(start_char_index).encode("utf8"))
end_char_index = match.end(i)
width = len(line.substr(start_char_index, end_char_index - start_char_index).encode("utf8"))
# print(f"highlight: {start_column}:{end_column} - {match.group(i)}")
result.append(HighlightedRange(
start_column,
end_column - start_column,
start_byte_index,
width,
highlight_full_line=True,
brush=self._brush_hit,
brush_full_line=self._brush_line

View File

@@ -7,50 +7,45 @@ from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QColor
from src.settings.settings import Settings
from src.ui.bigtext.selectionPos import SelectionPos
class HighlightSelection(Highlight):
start_byte = 0
end_byte = 0
start = SelectionPos(0, False, 0)
end = SelectionPos(0, False, 0)
def set_start(self, start_byte):
self.start_byte = start_byte
def set_start(self, start: SelectionPos):
self.start = start
def set_end_byte(self, end_byte):
self.end_byte = end_byte
def set_end_byte(self, end: SelectionPos):
self.end = 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())
def compute_highlight(self, line: Line) -> Optional[List[HighlightedRange]]:
begin = min(self.start_byte, self.end_byte)
end = max(self.start_byte, self.end_byte)
begin = self.min_byte()
end = self.max_byte()
if line.intersects(begin, end):
if line.includes_byte(begin):
start_byte_in_line = begin - line.byte_offset()
else:
start_byte_in_line = 0
start_char = line.byte_index_to_char_index(start_byte_in_line)
if line.includes_byte(end):
length_in_bytes = end - line.byte_offset() - start_byte_in_line
end_char = line.byte_index_to_char_index(start_byte_in_line + length_in_bytes)
else:
# renders the highlighting to the end of the line
# this is how selections usually behave
length_in_bytes = Settings.max_line_length() - start_byte_in_line
# note: this mixes chars and bytes, but that should not matter, because
# it just means that we render the highlight into the invisible range on the right
end_char = start_char + length_in_bytes
start_column = line.char_to_column(start_char)
end_column = line.char_to_column(end_char)
if end_column >= 0:
length_in_columns = end_column - start_column
else:
length_in_columns = 4096
return [HighlightedRange(start_column, length_in_columns, brush=QBrush(QColor(156, 215, 255, 192)),
# print(f"compute_highlight: {line.substr_bytes(begin, end)} begin={begin} end={end} start_byte_in_line={start_byte_in_line} length_in_bytes={length_in_bytes}")
return [HighlightedRange(start_byte_in_line, length_in_bytes, brush=QBrush(QColor(156, 215, 255, 192)),
pen=Qt.PenStyle.NoPen)]
else:
return None

View File

@@ -1,16 +1,22 @@
import unicodedata
from PySide6.QtGui import QFontMetrics
import constants
class Line:
def __init__(self, byte_offset: int, byte_end: int, line: str):
def __init__(self, byte_offset: int, byte_end: int, line: str, bytes: str):
self._byte_offset = byte_offset
self._byte_end = byte_end
self._line = line
self._bytes = bytes
self._cache_char_to_column()
def get_width_in_px(self, font_metric: QFontMetrics):
return font_metric.horizontalAdvance(self._line)
def byte_offset(self) -> int:
return self._byte_offset
@@ -131,11 +137,20 @@ class Line:
def prefix(self, index: int) -> str:
return self._line[0:index]
def prefix_bytes(self, byte_index: int) -> str:
return self._bytes[0:byte_index]
def substr(self, offset: int, length: int) -> str:
return self._line[offset:offset+length]
def substr_bytes(self, byte_offset: int, byte_length: int) -> str:
return self._bytes[byte_offset:byte_offset + byte_length]
def suffix(self, index: int) -> str:
return self._line[index:]
def suffix_bytes(self, byte_index: int) -> str:
return self._bytes[byte_index:]
def __str__(self):
return "%s (%d->%d)" % (self._line, self._byte_offset, self._byte_end)

View File

@@ -136,7 +136,7 @@ class LogFileModel:
return re.match(r"\w", char) is not None
def prune_cache(self, range_start: int, range_end: int):
print(f"cache size: {len(self._line_cache.keys())}")
# print(f"cache size: {len(self._line_cache.keys())}")
for key in list(self._line_cache.keys()):
line = self._line_cache[key]
if range_start > line.byte_end() or line.byte_offset() > range_end:
@@ -171,7 +171,7 @@ class LogFileModel:
new_offset = f.tell()
if 0 <= range_end < new_offset:
break
line = Line(offset, new_offset, line_bytes.decode("utf8", errors="ignore"))
line = Line(offset, new_offset, line_bytes.decode("utf8", errors="ignore"), line_bytes)
if previous_line_is_complete: # only cache lines when we know they are complete
self._line_cache[offset] = line
offset = new_offset

View File

@@ -12,6 +12,7 @@ from src.util import conversion
from src.util.color import to_qcolor
from src.i18n import _
class RangeSliderHandle():
def __init__(self, value: int):
self.value = value
@@ -74,26 +75,29 @@ class RangeSlider(QWidget):
painter = QPainter(self)
self._draw_background(painter)
self._draw_hits(painter)
self._draw_handle(painter, self.lower_value)
self._draw_handle(painter, self.upper_value, direction=-1)
self._draw_handle(painter, self.lower_value, direction=-1)
self._draw_handle(painter, self.upper_value)
painter.end()
def _draw_background(self, painter: QPainter) -> None:
painter.setBrush(to_qcolor("50ade8"))
painter.setPen(to_qcolor("dddddd"))
# the 1px wide grey center line
rect = QRect(round(10), self._handle_width, round(1),
self.height() - 2 * self._handle_width)
painter.drawRoundedRect(rect, 3.0, 3.0)
# the blue line
rect = QRect(round(7),
self._value_to_pixel(self.lower_value.value) + self._handle_width,
self._value_to_pixel(self.lower_value.value),
round(6),
self._value_to_pixel(self.upper_value.value - self.lower_value.value) - 2 * self._handle_width)
self._value_to_pixel(self.upper_value.value - self.lower_value.value) - 1 * self._handle_width)
painter.drawRoundedRect(rect, 3.0, 3.0)
def _draw_handle(self, painter: QPainter, handle: RangeSliderHandle, direction=1) -> None:
y_pixel = self._value_to_pixel(handle.value)
h = self._handle_width - 1 # height of the handle
painter.setBrush(to_qcolor("dddddd"))
painter.setPen(to_qcolor("444444"))
@@ -101,15 +105,7 @@ class RangeSlider(QWidget):
painter.drawLine(2, y_pixel, 18, y_pixel)
painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, True)
painter.drawPolygon(
(QPoint(10, y_pixel), QPoint(18, y_pixel + 12 * direction), QPoint(2, y_pixel + 12 * direction)))
def _draw_handle_circle(self, painter: QPainter, handle: RangeSliderHandle) -> None:
y_pixel = self._value_to_pixel(handle.value)
painter.setBrush(to_qcolor("dddddd"))
painter.setPen(to_qcolor("444444"))
painter.setRenderHint(PySide6.QtGui.QPainter.RenderHint.Antialiasing, True)
painter.drawEllipse(QPoint(self._width / 2, y_pixel), self._handle_width / 2 - 1, self._handle_width / 2 - 1)
(QPoint(10, y_pixel), QPoint(18, y_pixel + h * direction), QPoint(2, y_pixel + h * direction)))
def _value_to_pixel(self, value: int) -> int:
value_percent = value / self.max_value
@@ -149,11 +145,11 @@ class RangeSlider(QWidget):
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier:
pos: QPoint = e.pos()
if self._is_on_handle(self.lower_value, pos.y(), direction=1):
if self._is_on_handle(self.lower_value, pos.y(), direction=-1):
self.selected_handle = self.lower_value
self.selection_drag_range = (self.min_value, self.upper_value.value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())
if self._is_on_handle(self.upper_value, pos.y(), direction=-1):
if self._is_on_handle(self.upper_value, pos.y(), direction=1):
self.selected_handle = self.upper_value
self.selection_drag_range = (self.lower_value.value, self.max_value)
self.drag_y_offset_in_handle = self.selected_handle.value - self._pixel_to_value(pos.y())

View File

@@ -1,4 +1,4 @@
01234
012345
01234567890123456789
012345678901234567890123456789012345678901234567890123456789
tab indentation:
@@ -22,6 +22,25 @@ x◌᷍◌◌᷍◌x
Control characters:

------------------------------
wide and half width characters:
123456789
アンドレアス
アンドレアス アンドレアス アンドレアス アンドレアス アンドレアス アンドレアス アンドレアス
アンドレアス
canadian aboriginal:
ᑭᓇᑐᐃᓐᓇᑦᑎᐊᖅᒥᒃ
simplified chinese:
任何人不得使为奴隶或奴
Thai:
ทุกคนมีสิทธิที่จะได้
Nastaliq Urdu (rl):
چونکہ یہ تمام
Braille:
⠑⠧⠑⠗⠽⠕⠝⠑
Arabic (rl):
ولما كانت
------------------------------
👍🏿 dark thumbs up (U+1F44D + U+1F3FF - THUMBS UP SIGN + EMOJI MODIFIER FITZPATRICK TYPE-6)
ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------ä---------ä----------<
2019-08-07 00:00:10,391 [catalina-exec-40] INFO c.r.c.u.l.PerformancePointcut - Executed HealthCheckController.checkOperativeness in 1 ms successful. [jv3fw7r2.m1u5]

1
version.txt Normal file
View File

@@ -0,0 +1 @@
0.2.1-36-g9902be0