Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4b962769f | |||
| d9c362419b | |||
| bfd8ce841f | |||
| a41e5b79a3 | |||
| 75578b6126 | |||
| 9afc4d1d9c | |||
| d36724f3e7 | |||
| bcd525d787 | |||
| 8289042af4 | |||
| 0246a3fb19 | |||
| 617c7f161f | |||
| 67f16571f1 | |||
| be5e0c9ae6 | |||
| 51c02b3d55 | |||
| c19cdf6f41 | |||
| 8ce0c1bf9e | |||
| 04a0310eee | |||
| 3a2aa4f109 | |||
| 9902be0a48 | |||
| 61132d242f | |||
| 21b2da1e69 | |||
| 69dd5ed1e3 | |||
| 8c740da879 | |||
| ddd377da7e | |||
| 8cf02c8f6a | |||
| 871cb4e08a | |||
| ed450424a5 | |||
| 00d4f2317a | |||
| 9c64acf77e | |||
| d561facb7e | |||
| 329775fb26 | |||
| be53c209ea | |||
| aa2bfa967e | |||
| 2b91b19ef3 | |||
| 9c1b8298be | |||
| 811e3c7f82 | |||
| 6d7accffde | |||
| 2cd6c2ec1b | |||
| 3e793596c2 | |||
| 7a574f7ed4 | |||
| 7d20bae74d | |||
| 9b9399f120 | |||
| 3d6cf84cd7 | |||
| 2b65e61e43 | |||
| 6538e85f37 | |||
| 76f7baecf3 | |||
| 7f4f6ab004 | |||
| 270b3a8683 | |||
| b8b4b4e790 | |||
| 66d6a728cc | |||
| 56189f4094 | |||
| 5f30862a83 | |||
| 017a51a24a | |||
| 442d3173c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ testbed
|
||||
icons-not-used
|
||||
venv*
|
||||
*.spec
|
||||
/version.txt
|
||||
|
||||
5
.idea/misc.xml
generated
5
.idea/misc.xml
generated
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (krowlog)" />
|
||||
<option name="enabledOnSave" value="true" />
|
||||
<option name="sdkName" value="Python 3.12 (krowlog)" />
|
||||
</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>
|
||||
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$/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/icons-not-used" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv312" />
|
||||
</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" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -17,7 +17,7 @@ KrowLog is a viewer for text files of arbitrary size.
|
||||
* Select arbitrary strings (not just full lines).
|
||||
* Double click selects word.
|
||||
* Triple click selects line.
|
||||
* Copy protection: Users is warned before creating a clipboard more than 5 MB in size. They can choose to copy the
|
||||
* Copy protection: Users are warned before creating a clipboard more than 5 MB in size. They can choose to copy the
|
||||
selection into a new file instead.
|
||||
* Optionally open a new tab when saving selection as file.
|
||||
* Panel for temporary notes
|
||||
|
||||
16
changelog.txt
Normal file
16
changelog.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Next Version
|
||||
* Feature: Add changelog.
|
||||
* Fix: When the range sliders are overlapping the end slider cannot be moved.
|
||||
* Feature: Get version number from git tags.
|
||||
* Feature: You can now "follow" a file. When enabled the file is automatically reloaded
|
||||
and scrolled to the end. Any manual scroll action disables "follow" mode.
|
||||
* Feature: Better support for fonts and characters with non-uniform width.
|
||||
* Fix: Cannot scroll to arbitrary positions in a file if the file is larger than 2GB
|
||||
* Feature: File type specific highlighters.
|
||||
|
||||
0.2.1
|
||||
* Feature: Show how many bytes are selected.
|
||||
* Feature: Highlighters can be disabled.
|
||||
* Feature: If a regex contains a group, then only the group is highlighted.
|
||||
Using a filter expression like '(\d+)ms' will only hightlight the number.
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
|
||||
krow_icon = "icons" + os.sep + "krowlog.svg"
|
||||
qt_icon = "icons" + os.sep + "qt-logo.png"
|
||||
license_file = os.path.dirname(os.path.realpath(__file__)) + os.sep + "LICENSE"
|
||||
changelog_file = os.path.dirname(os.path.realpath(__file__)) + os.sep + "changelog.txt"
|
||||
|
||||
tab_width = 4
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -8,12 +8,12 @@ Just run `make_installer.py` with the following command. The distribution can be
|
||||
to run this on all target platforms.
|
||||
|
||||
```
|
||||
venv311/bin/python make_installer.py
|
||||
venv312/bin/python make_installer.py
|
||||
```
|
||||
|
||||
## Update Python
|
||||
|
||||
1. install the latest python version. We need the dev version, because PyInstaller requires it.
|
||||
`sudo apt install python-3.11-dev python-3.11-venv`
|
||||
2. create new virtual environment with `python3.11 -m venv /path/to/venv`
|
||||
`sudo apt install python-3.12-dev python-3.12-venv`
|
||||
2. create new virtual environment with `python3.12 -m venv /path/to/venv`
|
||||
3. select the venv in PyCharm
|
||||
29
krowlog.py
29
krowlog.py
@@ -1,19 +1,26 @@
|
||||
import argparse
|
||||
import gettext
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import QTimer
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import constants
|
||||
from src import install
|
||||
from src.pluginregistry import PluginRegistry
|
||||
import gettext
|
||||
from src.ui.icon import Icon
|
||||
|
||||
__version__ = '0.2.1'
|
||||
version_file = Path(
|
||||
os.path.dirname(os.path.realpath(__file__)) + os.sep + "version.txt"
|
||||
)
|
||||
__version__ = version_file.read_text() if version_file.is_file() else "0.0.0"
|
||||
|
||||
gettext.install('krowlog', 'locale')
|
||||
gettext.install("krowlog", "locale")
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("main")
|
||||
@@ -25,7 +32,7 @@ def register_signal_handler():
|
||||
|
||||
|
||||
def stop_signal(signum, _stackframe):
|
||||
""" Handle terminate signal """
|
||||
"""Handle terminate signal"""
|
||||
try:
|
||||
log.info("Terminate signal received. %s", signum)
|
||||
QtCore.QCoreApplication.quit()
|
||||
@@ -65,7 +72,9 @@ class CmdArgs:
|
||||
|
||||
def parse_command_line_parameters() -> CmdArgs:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('files', metavar='F', type=str, nargs='*', help='file(s) to open')
|
||||
parser.add_argument(
|
||||
"files", metavar="F", type=str, nargs="*", help="file(s) to open"
|
||||
)
|
||||
namespace = parser.parse_args()
|
||||
return CmdArgs(files=namespace.files)
|
||||
|
||||
@@ -74,7 +83,9 @@ if __name__ == "__main__":
|
||||
cmd_args = parse_command_line_parameters()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(Icon(constants.krow_icon)) # works only for Linux (but only X11, not Wayland)
|
||||
app.setWindowIcon(
|
||||
Icon(constants.krow_icon)
|
||||
) # works only for Linux (but only X11, not Wayland)
|
||||
|
||||
# install stuff, e.g. a desktop file, set icon on Windows
|
||||
install.install()
|
||||
|
||||
Binary file not shown.
@@ -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})"
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import PyInstaller.__main__
|
||||
import os
|
||||
import sys
|
||||
|
||||
PyInstaller.__main__.run([
|
||||
os.system("git -C . describe --match \"*.*.*\" --tags > version.txt")
|
||||
|
||||
arguments = [
|
||||
'krowlog.py',
|
||||
# '--onefile',
|
||||
'--noconfirm',
|
||||
@@ -12,9 +15,14 @@ PyInstaller.__main__.run([
|
||||
'--add-binary', 'icons' + os.pathsep + 'icons',
|
||||
'--add-binary', 'locales' + os.pathsep + 'locales',
|
||||
'--add-binary', 'LICENSE' + os.pathsep + '.',
|
||||
'--add-binary', 'changelog.txt' + os.pathsep + '.',
|
||||
'--add-binary', 'version.txt' + os.pathsep + '.',
|
||||
'--hidden-import=krowlog',
|
||||
'--hidden-import=watchdog',
|
||||
'--hidden-import=watchdog.observers',
|
||||
'--hidden-import=watchdog.version',
|
||||
'--hidden-import=__future__',
|
||||
'--hidden-import=configparser'
|
||||
])
|
||||
]
|
||||
|
||||
if sys.platform == 'win32' or sys.platform == 'cygwin':
|
||||
arguments.append('--version-file=version.py')
|
||||
|
||||
PyInstaller.__main__.run(arguments)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pip==24.0
|
||||
PySide6_Essentials==6.6.2
|
||||
setuptools==69.2.0
|
||||
watchdog==4.0.0
|
||||
pyinstaller==6.5.0
|
||||
pip==25.0.1
|
||||
PySide6_Essentials==6.8.2.1
|
||||
setuptools==77.0.3
|
||||
pyinstaller==6.12.0
|
||||
isort==6.0.1
|
||||
black==25.1.0
|
||||
|
||||
0
src/new_big_text/__init__.py
Normal file
0
src/new_big_text/__init__.py
Normal file
461
src/new_big_text/bigger_text.py
Normal file
461
src/new_big_text/bigger_text.py
Normal file
@@ -0,0 +1,461 @@
|
||||
import argparse
|
||||
import enum
|
||||
|
||||
import signal, os
|
||||
import logging
|
||||
import signal
|
||||
import time
|
||||
from io import TextIOWrapper
|
||||
from typing import List
|
||||
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtGui import QPaintEvent, QPainter, QFont, QFontMetrics, QColor, QBrush, QWheelEvent
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QStatusBar, QGridLayout, QSizePolicy, QScrollBar
|
||||
from PySide6.QtCore import QTimer, QPoint, Qt, QRect, QLine, Slot
|
||||
import sys
|
||||
from src.pluginregistry import PluginRegistry
|
||||
import gettext
|
||||
|
||||
__version__ = '0.2.1'
|
||||
|
||||
from src.i18n import _
|
||||
from src.ui.bigtext.BigScrollBar import BigScrollBar
|
||||
from src.ui.bigtext.bigtext import InnerBigText
|
||||
|
||||
gettext.install('krowlog', 'locale')
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("main")
|
||||
|
||||
|
||||
def register_signal_handler():
|
||||
signal.signal(signal.SIGINT, stop_signal)
|
||||
signal.signal(signal.SIGTERM, stop_signal)
|
||||
|
||||
|
||||
def stop_signal(signum, _stackframe):
|
||||
""" Handle terminate signal """
|
||||
try:
|
||||
log.info("Terminate signal received. %s", signum)
|
||||
QtCore.QCoreApplication.quit()
|
||||
except Exception:
|
||||
log.exception("Exception occurred while terminating")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
MAX_LINE_LENGTH = 4096
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("main")
|
||||
|
||||
|
||||
class FileWithTimeout:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def handler(signum, frame):
|
||||
signame = signal.Signals(signum).name
|
||||
print(f'Signal handler called with signal {signame} ({signum})')
|
||||
raise OSError("Couldn't open device!")
|
||||
|
||||
def open(path: str, timeout_in_seconds: int = 10, mode="rb", encoding=None) -> TextIOWrapper:
|
||||
# Set the signal handler and a 5-second alarm
|
||||
signal.signal(signal.SIGALRM, FileWithTimeout.handler)
|
||||
signal.alarm(timeout_in_seconds)
|
||||
fd = open(path, mode=mode, encoding=encoding)
|
||||
signal.alarm(0) # Disable the alarm
|
||||
return fd
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MainWindow, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_("KrowLog"))
|
||||
self.setMinimumWidth(800)
|
||||
self.setMinimumHeight(880)
|
||||
bigger_text = BiggerText(FileModel("testdata/testset.txt"))
|
||||
self.setCentralWidget(bigger_text)
|
||||
self.status_bar = QStatusBar(self)
|
||||
self.setStatusBar(self.status_bar)
|
||||
self.status_bar.setAutoFillBackground(True)
|
||||
self.status_bar.showMessage("bla blub")
|
||||
|
||||
|
||||
def apply_time_workaround():
|
||||
# workaround to make signals work in QT apps.
|
||||
# They do not work out of the box, because the main thread
|
||||
# is running in C++ code once app.exec() is executed
|
||||
# Forcing an empty lambda to be executed periodically gives
|
||||
# control back to python and allows python to react to signals
|
||||
timer = QTimer()
|
||||
timer.timeout.connect(lambda: None)
|
||||
timer.start(100)
|
||||
|
||||
|
||||
class LineType(enum.IntEnum):
|
||||
Full = 1
|
||||
Begin = 2
|
||||
Middle = 3
|
||||
End = 4
|
||||
|
||||
|
||||
class Line:
|
||||
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_end: int the offset of the last byte of this line
|
||||
:type text: str the decoded text
|
||||
:type continued: bool True if the previous line was too long and has been split
|
||||
"""
|
||||
self._byte_offset = byte_offset
|
||||
self._byte_end = byte_end
|
||||
self._text = text
|
||||
self._bytes = bytes
|
||||
self._type = type
|
||||
|
||||
def byte_offset(self) -> int:
|
||||
return self._byte_offset
|
||||
|
||||
def byte_end(self) -> int:
|
||||
return self._byte_end
|
||||
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
|
||||
def bytes(self) -> str:
|
||||
return self._bytes
|
||||
|
||||
def type(self) -> LineType:
|
||||
return self._type
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%d->%d %s)" % (self._text, self._byte_offset, self._byte_end, self._type.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s (%d->%d %s)" % (repr(self._text), self._byte_offset, self._byte_end, self._type.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Line):
|
||||
# don't attempt to compare against unrelated types
|
||||
return NotImplemented
|
||||
|
||||
return (self._byte_offset == other._byte_offset
|
||||
and self._byte_end == other._byte_end
|
||||
and self._text == other._text
|
||||
and self._type == other._type)
|
||||
|
||||
def __hash__(self):
|
||||
# necessary for instances to behave sanely in dicts and sets.
|
||||
return hash((self._byte_offset, self._byte_end, self._text, self._type))
|
||||
|
||||
|
||||
class FileModel:
|
||||
BUFFER_SIZE = 2 ** 16 # for production use this should be at least 4096, better 64kb
|
||||
|
||||
def __init__(self, file: str):
|
||||
self.file = file
|
||||
|
||||
def _read_line(self, file: TextIOWrapper, read_offset: int, max_line_length: int, encoding: str,
|
||||
previous_line_type: LineType) -> Line | None:
|
||||
file.seek(read_offset)
|
||||
buffer: str = file.read(self.BUFFER_SIZE)
|
||||
if not buffer:
|
||||
# end of file reached
|
||||
return None
|
||||
|
||||
while True:
|
||||
pos_of_newline = buffer.find(b"\n")
|
||||
if pos_of_newline > max_line_length:
|
||||
start_of_line = read_offset
|
||||
end_of_line = read_offset + max_line_length - 1
|
||||
line = buffer[0:max_line_length]
|
||||
|
||||
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
|
||||
return Line(start_of_line, end_of_line, decoded_line, line, line_type)
|
||||
elif pos_of_newline >= 0:
|
||||
start_of_line = read_offset
|
||||
end_of_line = read_offset + pos_of_newline
|
||||
line = buffer[0:pos_of_newline + 1]
|
||||
|
||||
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
|
||||
return Line(start_of_line, end_of_line, decoded_line, line, line_type)
|
||||
|
||||
else:
|
||||
# line does not end in this buffer
|
||||
# read the next chunk and stitch the line together
|
||||
raise "not yet implemented"
|
||||
|
||||
def read(self, file_offset: int, lines_to_read: int, max_line_length=512, encoding="utf8") -> [Line]:
|
||||
lines: list[Line] = []
|
||||
with FileWithTimeout.open(self.file, 5, 'rb') as f:
|
||||
read_offset = max(0, file_offset - max_line_length * 6) # factor 6 is due to multibyte characters in utf8
|
||||
f.seek(read_offset)
|
||||
|
||||
previous_line_type = LineType.Full
|
||||
while len(lines) < lines_to_read:
|
||||
l: Line | None = self._read_line(f, read_offset, max_line_length, encoding, previous_line_type)
|
||||
if l is None:
|
||||
break
|
||||
if file_offset <= l.byte_end():
|
||||
lines.append(l)
|
||||
read_offset = l.byte_end() + 1
|
||||
previous_line_type = l.type()
|
||||
|
||||
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):
|
||||
def __init__(self, model: FileModel):
|
||||
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 = "JetBrains Mono"
|
||||
# font = "Monospace" # not found
|
||||
# font = "ZedMono" # is not found
|
||||
#font = "Noto Sans Mono"
|
||||
font = "Noto Color Emoji"
|
||||
|
||||
qfont = QFont(font, self._font_size)
|
||||
# qfont.setStyleHint(QFont.StyleHint.Monospace)
|
||||
self.qfont = qfont
|
||||
|
||||
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
||||
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.NoModifier:
|
||||
self.selection.start = self.to_byte_offset(e.position())
|
||||
self.selection.end = self.selection.start
|
||||
self.mouse_pressed = True
|
||||
self.update()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self.mouse_pressed = False
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
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 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)
|
||||
|
||||
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:
|
||||
start_ns = time.process_time_ns()
|
||||
painter = QPainter(self)
|
||||
font = painter.font()
|
||||
font.setPointSize(self._font_size)
|
||||
painter.setFont(font)
|
||||
|
||||
# ---
|
||||
self.font_metric = painter.fontMetrics()
|
||||
self.char_height = self.font_metric.height()
|
||||
lines_to_read = self.height() / self.char_height + 1
|
||||
|
||||
self.lines_to_render: [Line] = self.file_model.read(0, lines_to_read, 200, self._encoding)
|
||||
|
||||
self.update_longest_line(self.lines_to_render)
|
||||
|
||||
painter.setPen(QColor(0, 0, 0))
|
||||
|
||||
line_on_screen = 1
|
||||
for line in self.lines_to_render:
|
||||
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
|
||||
|
||||
painter.end()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
apply_time_workaround()
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
register_signal_handler()
|
||||
|
||||
app.exec()
|
||||
53
src/new_big_text/test_file_model.py
Normal file
53
src/new_big_text/test_file_model.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import unittest
|
||||
|
||||
from src.new_big_text.bigger_text import FileModel, Line, LineType
|
||||
|
||||
|
||||
class FileModelTestCase(unittest.TestCase):
|
||||
def test_read_lines_with_offset_0(self):
|
||||
fm = FileModel("testdata/readlines.txt")
|
||||
actual_lines = fm.read(file_offset=0, lines_to_read=5, max_line_length=16, encoding="utf8")
|
||||
self.assertEqual([
|
||||
Line(0, 1, '1\n'),
|
||||
Line(2, 4, '12\n'),
|
||||
Line(5, 8, '123\n'),
|
||||
Line(9, 13, '1234\n'),
|
||||
Line(14, 19, '12345\n')
|
||||
],
|
||||
actual_lines)
|
||||
|
||||
def test_read_lines_with_offset_in_middle_of_line(self):
|
||||
fm = FileModel("testdata/readlines.txt")
|
||||
file_offset = "1\n12\n123\n".find("123") + 1
|
||||
actual_lines = fm.read(
|
||||
file_offset=file_offset, # at char 2 in line 3
|
||||
lines_to_read=5,
|
||||
max_line_length=16,
|
||||
encoding="utf8")
|
||||
self.assertEqual([
|
||||
Line(5, 8, '123\n'),
|
||||
Line(9, 13, '1234\n'),
|
||||
Line(14, 19, '12345\n'),
|
||||
Line(20, 26, '123456\n'),
|
||||
Line(27, 34, '1234567\n')
|
||||
],
|
||||
actual_lines)
|
||||
|
||||
def test_read_long_line__buffer_larger_than_line(self):
|
||||
fm = FileModel("testdata/longLines.txt")
|
||||
fm.BUFFER_SIZE = 512
|
||||
actual_lines = fm.read(
|
||||
file_offset=0,
|
||||
lines_to_read=3,
|
||||
max_line_length=10,
|
||||
encoding="utf8"
|
||||
)
|
||||
self.assertEqual([
|
||||
Line(0, 9, "1aaaaaaaa-", LineType.Begin),
|
||||
Line(10, 19, "bbbbbbbbb-", LineType.Middle),
|
||||
Line(20, 27, "ccccccc\n", LineType.End),
|
||||
], actual_lines)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -55,9 +55,9 @@ def _new_recursive(current_action_id: str, items: [MenuContribution]) -> [MenuCo
|
||||
for item in items:
|
||||
|
||||
mc: MenuContribution = item
|
||||
print("%s checking %s" % (current_action_id, mc.action_id))
|
||||
# print("%s checking %s" % (current_action_id, mc.action_id))
|
||||
if mc.after == current_action_id:
|
||||
print("%s adding %s" % (current_action_id, mc.action_id))
|
||||
#print("%s adding %s" % (current_action_id, mc.action_id))
|
||||
result.append(mc)
|
||||
result = result + _new_recursive(mc.action_id, items)
|
||||
|
||||
@@ -65,7 +65,7 @@ def _new_recursive(current_action_id: str, items: [MenuContribution]) -> [MenuCo
|
||||
|
||||
|
||||
def _recursive_half_order_adder(result: [MenuContribution], items: [MenuContribution]):
|
||||
print("%s -- %s" % ([mc.action_id for mc in result], [mc.action_id for mc in items]))
|
||||
#print("%s -- %s" % ([mc.action_id for mc in result], [mc.action_id for mc in items]))
|
||||
for item in items:
|
||||
mc: MenuContribution = item
|
||||
if mc.after:
|
||||
|
||||
79
src/plugins/krowlog/about_qt_dialog.py
Normal file
79
src/plugins/krowlog/about_qt_dialog.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import textwrap
|
||||
|
||||
import PySide6
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QPalette
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import constants
|
||||
|
||||
import krowlog
|
||||
from src.ui.icon import Icon
|
||||
from src.ui.label import Label
|
||||
from src.ui.vbox import VBox
|
||||
from src.i18n import _
|
||||
|
||||
|
||||
class AboutQTDialog(QDialog):
|
||||
"""Dialog for showing info about KrowLog"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(AboutQTDialog, self).__init__(parent)
|
||||
self.setWindowTitle(_("About QT"))
|
||||
self.setModal(True)
|
||||
# self.setMinimumWidth(850)
|
||||
# self.setFixedHeight(400)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
text = f"""
|
||||
<b>About QT</b>
|
||||
<p>This program uses QT version {PySide6.QtCore.__version__}.</p>
|
||||
|
||||
<p>QT is a C++ toolkit for cross-platform application development.</p>
|
||||
|
||||
<p>Qt provides single-source portability across all major desktop
|
||||
operating systems. It is also available for embedded Linux and other
|
||||
embedded and mobile operating systems.</p>
|
||||
|
||||
<p>Qt is available under multiple licensing options designed to accommodate
|
||||
the needs of our various users.</p>
|
||||
|
||||
<p>Qt licensed under our commercial license agreement is appropriate for
|
||||
development of proprietary/commercial software where you do not want to
|
||||
share any source code with third parties or otherwise cannot comply with
|
||||
the terms of GNU (L)GPL.</p>
|
||||
|
||||
<p>Qt licensed under GNU (L)GPL is appropriate for the development of Qt
|
||||
applications provided you can comply with the terms and conditions of the
|
||||
respective licenses.</p>
|
||||
|
||||
Please see <a href="http://qt.io/licensing">qt.io/licensing</a> for an<
|
||||
overview of Qt licensing.
|
||||
|
||||
<p>Copyright (C) 2025 The Qt Company Ltd and other contributors.</p>
|
||||
|
||||
<p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p>
|
||||
|
||||
<p>Qt is The Qt Company Ltd product developed as an open source project.
|
||||
See <a href="http://qt.io">qt.io</a> for more information.</p>
|
||||
"""
|
||||
label = Label(text)
|
||||
label.setWordWrap(True)
|
||||
|
||||
app_icon = QLabel()
|
||||
app_icon.setPixmap(Icon(constants.qt_icon).pixmap(64, 64))
|
||||
heading = QWidget(self)
|
||||
hbox = QHBoxLayout(heading)
|
||||
hbox.addWidget(app_icon)
|
||||
hbox.addWidget(label)
|
||||
hbox.addSpacerItem(QSpacerItem(1, 1, hData=QSizePolicy.Policy.Expanding))
|
||||
|
||||
heading.layout = hbox
|
||||
self.layout.addWidget(heading)
|
||||
|
||||
buttons = QDialogButtonBox(self)
|
||||
buttons.setStandardButtons(QDialogButtonBox.StandardButton.Close)
|
||||
buttons.rejected.connect(self.close)
|
||||
self.layout.addWidget(buttons)
|
||||
self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
@@ -1,7 +1,6 @@
|
||||
import textwrap
|
||||
|
||||
import PySide6
|
||||
from watchdog import version as watchdog_version
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QPalette
|
||||
from PySide6.QtWidgets import *
|
||||
@@ -22,8 +21,8 @@ class AboutDialog(QDialog):
|
||||
super(AboutDialog, self).__init__(parent)
|
||||
self.setWindowTitle(_("About KrowLog"))
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(650)
|
||||
self.setFixedHeight(300)
|
||||
self.setMinimumWidth(850)
|
||||
self.setFixedHeight(400)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
@@ -50,6 +49,7 @@ class AboutDialog(QDialog):
|
||||
tabs.addTab(self._about(), _("About"))
|
||||
tabs.addTab(self._libraries(), _("Libraries"))
|
||||
tabs.addTab(self._license(), _("License"))
|
||||
tabs.addTab(self._changelog(), _("Changelog"))
|
||||
|
||||
self.layout.addWidget(tabs)
|
||||
|
||||
@@ -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)
|
||||
@@ -74,11 +74,9 @@ class AboutDialog(QDialog):
|
||||
<ul>
|
||||
<li>PySide6-Essentials {pyside} (LGPL v3) - <a href="https://doc.qt.io/qtforpython-6/">https://doc.qt.io/qtforpython-6/</a></li>
|
||||
<li>Qt6 {qt} (LGPL v3) - <a href="https://code.qt.io/cgit/qt/qtbase.git/">https://code.qt.io/cgit/qt/qtbase.git/</a></li>
|
||||
<li>watchdog {watchdog} (Apache 2.0) - <a href="https://github.com/gorakhargosh/watchdog">https://github.com/gorakhargosh/watchdog</a></li>
|
||||
</ul>""".format(
|
||||
pyside=PySide6.__version__,
|
||||
qt=PySide6.QtCore.__version__,
|
||||
watchdog=watchdog_version.VERSION_STRING)
|
||||
qt=PySide6.QtCore.__version__)
|
||||
label = textwrap.dedent(dependencies)
|
||||
|
||||
result = QWidget()
|
||||
@@ -106,3 +104,25 @@ class AboutDialog(QDialog):
|
||||
panel.setBackgroundRole(QPalette.ColorRole.Light)
|
||||
result.layout.addWidget(panel)
|
||||
return result
|
||||
|
||||
def _changelog(self) -> QWidget:
|
||||
with open(constants.changelog_file, 'r') as file:
|
||||
text = file.read()
|
||||
|
||||
result = QWidget()
|
||||
result.layout = QVBoxLayout(result)
|
||||
result.layout.setContentsMargins(0, 0, 0, 0)
|
||||
result.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
|
||||
label = Label(text)
|
||||
label.setFont(QFont("Monospace"))
|
||||
label.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
|
||||
panel = QScrollArea(result)
|
||||
panel.setContentsMargins(0, 0, 0, 0)
|
||||
panel.setViewportMargins(0, 0, 0, 0)
|
||||
panel.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
|
||||
panel.setWidget(label)
|
||||
panel.setBackgroundRole(QPalette.ColorRole.Light)
|
||||
result.layout.addWidget(panel)
|
||||
return result
|
||||
|
||||
@@ -5,6 +5,7 @@ from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDockWidget, QMessageBox
|
||||
|
||||
import constants
|
||||
from src.plugins.krowlog.about_qt_dialog import AboutQTDialog
|
||||
from src.plugins.krowlog.aboutdialog import AboutDialog
|
||||
from src.mainwindow import MainWindow
|
||||
from src.pluginbase import PluginBase
|
||||
@@ -36,6 +37,7 @@ class KrowLogPlugin(PluginBase):
|
||||
return [
|
||||
MenuContribution("file", action=self._action_close(), action_id="close application", after="<last>"),
|
||||
MenuContribution("help", action=self._action_about(), action_id="open about dialog", after="<last>"),
|
||||
MenuContribution("help", action=self._action_about_qt(), action_id="open about QT dialog", after="<last>"),
|
||||
MenuContribution("settings", menu=self._sub_menu_languages(), action_id="recent files menu"),
|
||||
]
|
||||
|
||||
@@ -108,6 +110,14 @@ class KrowLogPlugin(PluginBase):
|
||||
)
|
||||
return about_action
|
||||
|
||||
def _action_about_qt(self) -> RAction:
|
||||
action = RAction(
|
||||
_("&About QT"),
|
||||
action=lambda: AboutQTDialog().exec(),
|
||||
icon_file=constants.qt_icon
|
||||
)
|
||||
return action
|
||||
|
||||
def _action_close(self) -> RAction:
|
||||
icon = "close" if sys.platform == 'win32' or sys.platform == 'cygwin' else "exit"
|
||||
close_action = RAction(_("E&xit"), action=lambda: self.main_window.destruct(), shortcut='Ctrl+X',
|
||||
|
||||
@@ -38,7 +38,8 @@ class FilterTask(QRunnable):
|
||||
on_before: Callable[[], None],
|
||||
on_finish: Callable[[], None],
|
||||
show_only_matches: bool,
|
||||
matches_separator: str
|
||||
matches_separator: str,
|
||||
zoned_plugin_registry: ZonedPluginRegistry
|
||||
):
|
||||
super(FilterTask, self).__init__()
|
||||
self.source_model = source_model
|
||||
@@ -53,6 +54,7 @@ class FilterTask(QRunnable):
|
||||
self.filter_match_found_listeners = filter_match_found_listeners
|
||||
self.show_only_matches = show_only_matches
|
||||
self.matches_separator = matches_separator
|
||||
self.zoned_plugin_registry = zoned_plugin_registry
|
||||
|
||||
def only_matches(self, line: str, regex: re.Pattern):
|
||||
result = ""
|
||||
@@ -87,7 +89,10 @@ class FilterTask(QRunnable):
|
||||
listener(-1, -1) # notify listeners that a new search started
|
||||
|
||||
hits_count = 0
|
||||
hits_positions: set[float] = set(())
|
||||
self.zoned_plugin_registry.execute("update_hit_positions", hits_positions)
|
||||
last_progress_report = time.time()
|
||||
source_file_size = self.source_model.byte_count()
|
||||
try:
|
||||
with open(self.source_model.get_file(), "rb") as source:
|
||||
source.seek(self.source_model.range_start)
|
||||
@@ -121,6 +126,12 @@ class FilterTask(QRunnable):
|
||||
target.write(line.encode("utf8"))
|
||||
hits_count = hits_count + 1
|
||||
|
||||
hits_positions_before = len(hits_positions)
|
||||
hits_positions.add(round(source_line_offset / source_file_size, 3))
|
||||
hits_positions_after = len(hits_positions)
|
||||
if hits_positions_before != hits_positions_after:
|
||||
self.zoned_plugin_registry.execute("update_hit_positions", hits_positions)
|
||||
|
||||
# sometime buffering can hide results for a while
|
||||
# We force a flush periodically.
|
||||
if line_count % 10000 == 0:
|
||||
@@ -225,8 +236,8 @@ class FilterWidget(QWidget):
|
||||
|
||||
(handle, self.tmp_filename) = tempfile.mkstemp()
|
||||
os.close(handle)
|
||||
self.filter_model = LogFileModel(self.tmp_filename, self.source_model.settings)
|
||||
self.hits_view = BigText(self.filter_model, show_range_slider=False)
|
||||
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, show_follow_action=False)
|
||||
|
||||
self.layout.addWidget(filter_bar)
|
||||
self.layout.addWidget(self.hits_view)
|
||||
@@ -268,6 +279,7 @@ class FilterWidget(QWidget):
|
||||
|
||||
def destruct(self):
|
||||
self._cancel_search()
|
||||
self.hits_view.destruct()
|
||||
os.remove(self.tmp_filename)
|
||||
|
||||
def _cancel_search(self):
|
||||
@@ -350,6 +362,7 @@ class FilterWidget(QWidget):
|
||||
lambda: self.search_is_running.emit(True),
|
||||
lambda: self.search_is_running.emit(False),
|
||||
show_only_matches,
|
||||
self.matches_separator.text()
|
||||
self.matches_separator.text(),
|
||||
self._zoned_plugin_registry
|
||||
)
|
||||
QThreadPool.globalInstance().start(self.filter_task)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
|
||||
from src.pluginbase import PluginBase
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
from src.plugins.logfile.filterviewsyncer import FilterViewSyncer
|
||||
from src.plugins.logfile.filterwidget import FilterWidget
|
||||
@@ -17,6 +18,7 @@ class FullTabWidget(Tab):
|
||||
self._model = model
|
||||
self._zoned_plugin_registry = zoned_plugin_registry
|
||||
self.file_view = BigText(model)
|
||||
self._zoned_plugin_registry.register_plugin("TabWidgetPlugin", TabWidgetPlugin(self.file_view))
|
||||
self.filter_hit_view = FilterWidget(self._model, self._zoned_plugin_registry)
|
||||
self.filter_view_syncer = FilterViewSyncer(self.file_view)
|
||||
self.filter_hit_view.add_line_click_listener(self.filter_view_syncer.click_listener)
|
||||
@@ -51,3 +53,12 @@ class FullTabWidget(Tab):
|
||||
# overriding abstract method
|
||||
def on_reveal(self):
|
||||
self.filter_hit_view.on_reveal()
|
||||
|
||||
|
||||
class TabWidgetPlugin(PluginBase):
|
||||
def __init__(self, file_view: BigText):
|
||||
super(TabWidgetPlugin, self).__init__()
|
||||
self._file_view = file_view
|
||||
|
||||
def update_hit_positions(self, hit_positions: set[float]):
|
||||
self._file_view.update_hit_positions(hit_positions)
|
||||
|
||||
246
src/ui/bigtext/BigScrollBar.py
Normal file
246
src/ui/bigtext/BigScrollBar.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import enum
|
||||
|
||||
from PySide6.QtGui import QWheelEvent
|
||||
from PySide6.QtWidgets import QWidget, QStylePainter, QStyle, QStyleOptionSlider
|
||||
from PySide6.QtCore import Qt, QSize, QEvent, QRect, QPoint, Signal, QTimer
|
||||
|
||||
|
||||
class BigScrollBar(QWidget):
|
||||
value_changed = Signal(str)
|
||||
"""Signal emitted when the range slider value changes.
|
||||
**Note**: The value is a string and must be parsed into an int.
|
||||
QT's signal api only supports 32bit integers. Ints larger
|
||||
than 2**32-1 will overflow. Probably because there is some C/C++
|
||||
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
|
||||
LinesUp = 3
|
||||
LinesDown = 4
|
||||
|
||||
scroll_event = Signal(ScrollEvent)
|
||||
|
||||
pressedControl = QStyle.SubControl.SC_None
|
||||
click_offset = 0
|
||||
|
||||
scale = 10000
|
||||
|
||||
minimum = 0
|
||||
value = 0
|
||||
maximum = 100
|
||||
|
||||
def __init__(self):
|
||||
super(BigScrollBar, self).__init__()
|
||||
self.repeat_action_timer = QTimer()
|
||||
self.repeat_action_control = QStyle.SubControl.SC_None
|
||||
self.repeat_action_timer.setSingleShot(True)
|
||||
self.repeat_action_timer.timeout.connect(self.execute_control)
|
||||
self.repeat_action_timer.setInterval(50)
|
||||
|
||||
def setMinimum(self, min: int):
|
||||
self.minimum = min
|
||||
if self.value < self.minimum:
|
||||
self.set_value(self.minimum)
|
||||
|
||||
def setMaximum(self, max: int):
|
||||
self.maximum = max
|
||||
if self.value > self.maximum:
|
||||
self.set_value(self.maximum)
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QStylePainter(self)
|
||||
o = self.style_options()
|
||||
# print(f"style_options: sliderPosition: {o.sliderPosition}")
|
||||
p.drawComplexControl(QStyle.ComplexControl.CC_ScrollBar, o)
|
||||
|
||||
def style_options(self) -> QStyleOptionSlider:
|
||||
o = QStyleOptionSlider()
|
||||
o.initFrom(self)
|
||||
o.orientation = Qt.Orientation.Vertical
|
||||
o.subControls = QStyle.SubControl.SC_All
|
||||
|
||||
if self.pressedControl != QStyle.SubControl.SC_None:
|
||||
o.activeSubControls = self.pressedControl # QStyle.SubControl.SC_None
|
||||
o.state = o.state | QStyle.StateFlag.State_Sunken
|
||||
|
||||
o.singleStep = 1
|
||||
|
||||
# scale values to 10000
|
||||
|
||||
t = self.scale / self.maximum
|
||||
o.minimum = self.minimum * t
|
||||
o.maximum = self.maximum * t
|
||||
o.sliderPosition = int(self.value * t)
|
||||
# print(f"t={t}")
|
||||
# print(f"({self.minimun}, {self.value}, {self.maximum}) -> ({o.minimum},{o.sliderPosition},{o.maximum})")
|
||||
|
||||
return o
|
||||
|
||||
def sizeHint(self):
|
||||
scroll_bar_extend: int = self.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarExtent)
|
||||
scroll_bar_slider_min: int = self.style().pixelMetric(QStyle.PixelMetric.PM_ScrollBarSliderMin)
|
||||
# print(f"scroll_bar_extend: {scroll_bar_extend}, scroll_bar_slider_min: {scroll_bar_slider_min}")
|
||||
# for vertical
|
||||
size = QSize(scroll_bar_extend, scroll_bar_extend * 2 + scroll_bar_slider_min)
|
||||
|
||||
opts = self.style_options()
|
||||
return self.style().sizeFromContents(QStyle.ContentsType.CT_ScrollBar, opts, size, self)
|
||||
|
||||
def event(self, event: QEvent) -> bool:
|
||||
# print(f"event type: {event.type()}")
|
||||
|
||||
if event.type() == QEvent.Type.MouseButtonPress:
|
||||
pass
|
||||
# print(f"mouse button pressed")
|
||||
|
||||
return super().event(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
||||
if self.repeat_action_timer.isActive():
|
||||
self.stopRepeatAction()
|
||||
|
||||
if not (event.button() == Qt.MouseButton.LeftButton or event.button() == Qt.MouseButton.MiddleButton):
|
||||
return
|
||||
|
||||
style_options = self.style_options()
|
||||
|
||||
self.pressedControl = self.style().hitTestComplexControl(QStyle.ComplexControl.CC_ScrollBar, style_options,
|
||||
event.position().toPoint(), self)
|
||||
# print(f"pressedControl {self.pressedControl}")
|
||||
|
||||
sr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, style_options,
|
||||
QStyle.SubControl.SC_ScrollBarSlider, self)
|
||||
click: QPoint = event.position().toPoint()
|
||||
# print(f"pressYValue {pressYValue}")
|
||||
|
||||
if self.pressedControl == QStyle.SubControl.SC_ScrollBarSlider:
|
||||
self.click_offset = click.y() - sr.y()
|
||||
self.snap_back_position = self.value
|
||||
|
||||
if (self.pressedControl == QStyle.SubControl.SC_ScrollBarAddPage
|
||||
or self.pressedControl == QStyle.SubControl.SC_ScrollBarSubPage) \
|
||||
and event.button() == Qt.MouseButton.MiddleButton:
|
||||
slider_length = sr.height()
|
||||
self.set_value(self.pixelPosToRangeValue(event.position().toPoint().y()))
|
||||
self.pressedControl = QStyle.SubControl.SC_ScrollBarSlider
|
||||
self.click_offset = slider_length / 2
|
||||
return
|
||||
|
||||
self.repeat_action_control = self.pressedControl
|
||||
self.execute_control()
|
||||
# self.repaint(self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, style_options, self.pressedControl))
|
||||
|
||||
def execute_control(self):
|
||||
# print(f"execute_control: {self.repeat_action_control}")
|
||||
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 _:
|
||||
trigger_repeat_action = False
|
||||
|
||||
if trigger_repeat_action:
|
||||
# print(f"schedule repeat action: {self.repeat_action_control}")
|
||||
self.repeat_action_timer.start()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self.pressedControl = QStyle.SubControl.SC_None
|
||||
self.stopRepeatAction()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.pressedControl == QStyle.SubControl.SC_None:
|
||||
return
|
||||
|
||||
if self.pressedControl == QStyle.SubControl.SC_ScrollBarSlider:
|
||||
click: QPoint = event.position().toPoint()
|
||||
new_position = self.pixelPosToRangeValue(click.y() - self.click_offset)
|
||||
m = self.style().pixelMetric(QStyle.PixelMetric.PM_MaximumDragDistance, self.style_options(), self)
|
||||
if m >= 0:
|
||||
r: QRect = self.rect()
|
||||
r.adjust(-m, -m, m, m)
|
||||
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)
|
||||
|
||||
# stop repeat action when pointer leaves control
|
||||
pr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, self.style_options(),
|
||||
self.pressedControl, self)
|
||||
if not pr.contains(event.position().toPoint()):
|
||||
self.stopRepeatAction()
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
event.ignore()
|
||||
|
||||
# when using a touchpad we can have simultaneous horizontal and vertical movement
|
||||
horizontal = abs(event.angleDelta().x()) > abs(event.angleDelta().y())
|
||||
if horizontal:
|
||||
return
|
||||
|
||||
scroll_event = self.ScrollEvent.LinesDown if event.angleDelta().y() < 0 else self.ScrollEvent.LinesUp
|
||||
self.scroll_event.emit(scroll_event)
|
||||
|
||||
def pixelPosToRangeValue(self, pos: int):
|
||||
opt: QStyleOptionSlider = self.style_options()
|
||||
|
||||
gr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, opt,
|
||||
QStyle.SubControl.SC_ScrollBarGroove, self)
|
||||
sr: QRect = self.style().subControlRect(QStyle.ComplexControl.CC_ScrollBar, opt,
|
||||
QStyle.SubControl.SC_ScrollBarSlider, self)
|
||||
|
||||
# only for vertical scrollbars
|
||||
slider_length = sr.height()
|
||||
slider_min = gr.y()
|
||||
slider_max = gr.bottom() - slider_length + 1
|
||||
|
||||
val = QStyle.sliderValueFromPosition(opt.minimum, opt.maximum, pos - slider_min,
|
||||
slider_max - slider_min, opt.upsideDown)
|
||||
|
||||
t = self.scale / self.maximum
|
||||
val = int(val / t)
|
||||
# print(f"pixelPosToRangeValue({pos}) -> {val}")
|
||||
return val
|
||||
|
||||
def stopRepeatAction(self):
|
||||
self.repeat_action_control = QStyle.SubControl.SC_None
|
||||
# print(f"stop repeat action: {self.repeat_action_control}")
|
||||
self.repeat_action_timer.stop()
|
||||
#self.update()
|
||||
|
||||
def set_value(self, value: int, emit_change_event=True):
|
||||
changed = self.value != value
|
||||
self.value = value
|
||||
if emit_change_event and changed:
|
||||
# print(f"emitting value changed: {self.value}")
|
||||
self.value_changed.emit(str(self.value))
|
||||
self.update()
|
||||
@@ -11,72 +11,53 @@ from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from src.ui.ScaledScrollBar import ScaledScrollBar
|
||||
from src.ui.bigtext.BigScrollBar import BigScrollBar
|
||||
from src.ui.bigtext.highlight_regex import HighlightRegex
|
||||
from src.ui.bigtext.highlight_selection import HighlightSelection
|
||||
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
|
||||
from src.pluginregistry import PluginRegistry
|
||||
from threading import Event
|
||||
|
||||
from src.settings.settings import Settings
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from src.i18n import _
|
||||
|
||||
log = logging.getLogger("bigtext")
|
||||
|
||||
class FileObserver(FileSystemEventHandler):
|
||||
|
||||
def __init__(self, big_text):
|
||||
super(FileObserver, self).__init__()
|
||||
self.big_text = big_text
|
||||
self._last_mtime = -1
|
||||
|
||||
def on_modified(self, event):
|
||||
# slow down the updates. This is needed, because the file is modified
|
||||
# constantly, which would lead to constant re-rendering, which would
|
||||
# block the UI thread and make the UI unresponsive.
|
||||
# Note: we don't miss events, because they are queued and de-duplicated
|
||||
if not event.is_directory:
|
||||
try:
|
||||
mtime = os.stat(event.src_path).st_mtime
|
||||
if mtime != self._last_mtime:
|
||||
self._last_mtime = mtime
|
||||
time.sleep(0.5)
|
||||
self.big_text.trigger_update.emit()
|
||||
except FileNotFoundError:
|
||||
# ignore: happens when closing the application, because tmp files are deleted,
|
||||
# which triggers a modification event
|
||||
pass
|
||||
|
||||
|
||||
class FileWatchdogThread(QRunnable):
|
||||
|
||||
def __init__(self, big_text, file: str):
|
||||
super(FileWatchdogThread, self).__init__()
|
||||
self.file = file
|
||||
self.big_text = big_text
|
||||
self.observer = Observer()
|
||||
self.stop = Event()
|
||||
|
||||
def run(self) -> None:
|
||||
self.observer.schedule(FileObserver(self.big_text), self.file)
|
||||
self.observer.start()
|
||||
_last_mtime = None
|
||||
while not self.stop.is_set():
|
||||
mtime = os.stat(self.file).st_mtime
|
||||
if mtime != _last_mtime:
|
||||
_last_mtime = mtime
|
||||
self.big_text.trigger_update.emit()
|
||||
self.stop.wait(0.5)
|
||||
|
||||
def destruct(self):
|
||||
self.observer.stop()
|
||||
# self.observer.join(1)
|
||||
self.stop.set()
|
||||
|
||||
|
||||
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()
|
||||
@@ -85,12 +66,9 @@ class BigText(QWidget):
|
||||
self.grid.setVerticalSpacing(0)
|
||||
self.setLayout(self.grid)
|
||||
|
||||
self.v_scroll_bar = ScaledScrollBar()
|
||||
self.v_scroll_bar = BigScrollBar()
|
||||
|
||||
self.range_limit = RangeSlider()
|
||||
self.range_limit.value_changed.connect(self._range_limit_event)
|
||||
|
||||
self.big_text = InnerBigText(self, model, self.v_scroll_bar, self.range_limit)
|
||||
self.big_text = InnerBigText(self, model, self.v_scroll_bar)
|
||||
self.big_text.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
|
||||
|
||||
self.h_scroll_bar = QScrollBar(Qt.Orientation.Horizontal)
|
||||
@@ -99,12 +77,13 @@ class BigText(QWidget):
|
||||
self.h_scroll_bar.valueChanged.connect(self.big_text.h_scroll_event)
|
||||
|
||||
# self.v_scroll_bar.setPageStep(1)
|
||||
self.v_scroll_bar.scaledValueChanged.connect(self.big_text.v_scroll_event)
|
||||
self.v_scroll_bar.scrolled_to_end.connect(self.big_text.v_scroll_update_follow_tail)
|
||||
|
||||
|
||||
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()
|
||||
self.range_limit.value_changed.connect(self._range_limit_event)
|
||||
self.grid.addWidget(self.range_limit, 0, 0)
|
||||
self.grid.addWidget(self.big_text, 0, 1)
|
||||
self.grid.addWidget(self.h_scroll_bar, 1, 1)
|
||||
@@ -121,6 +100,10 @@ class BigText(QWidget):
|
||||
def get_file(self):
|
||||
return self.model.get_file()
|
||||
|
||||
def update_hit_positions(self, hit_positions: set[float]):
|
||||
if self.range_limit:
|
||||
self.range_limit.update_hit_positions(hit_positions)
|
||||
|
||||
def add_line_click_listener(self, listener: Callable[[int], None]):
|
||||
"""
|
||||
:param listener: a callable, the parameter is the byte offset of the clicked line
|
||||
@@ -142,21 +125,21 @@ 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
|
||||
|
||||
def __init__(self, parent: BigText, model: LogFileModel, v_scaled_scrollbar: ScaledScrollBar,
|
||||
range_limit: RangeSlider):
|
||||
_follow = False
|
||||
|
||||
def __init__(self, parent: BigText, model: LogFileModel, v_scaled_scrollbar: ScaledScrollBar):
|
||||
super(InnerBigText, self).__init__()
|
||||
self.char_height = None
|
||||
self.char_width = None
|
||||
self.model = model
|
||||
self._v_scaled_scrollbar = v_scaled_scrollbar
|
||||
self._range_limit = range_limit
|
||||
self.parent = parent
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
|
||||
@@ -169,8 +152,6 @@ class InnerBigText(QWidget):
|
||||
self._last_double_click_time = 0
|
||||
self._last_double_click_line_number = -1
|
||||
|
||||
self._follow_tail = False
|
||||
|
||||
self.highlight_selected_text = HighlightRegex(
|
||||
"",
|
||||
is_regex=False,
|
||||
@@ -219,7 +200,6 @@ class InnerBigText(QWidget):
|
||||
elif e.modifiers() == Qt.KeyboardModifier.ControlModifier and e.key() == 65: # ctrl + a
|
||||
self._select_all()
|
||||
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
direction = 1 if event.angleDelta().y() < 0 else -1
|
||||
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
||||
@@ -262,6 +242,13 @@ 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()
|
||||
|
||||
set_range_start = QAction(
|
||||
@@ -308,21 +295,25 @@ 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.setValue(self._byte_offset)
|
||||
self.parent.v_scroll_bar.set_value(self._byte_offset)
|
||||
|
||||
def scroll_to_byte(self, byte_offset: int):
|
||||
self._byte_offset = min(byte_offset, self.model.byte_count())
|
||||
self.update()
|
||||
self.parent.v_scroll_bar.setValue(self._byte_offset)
|
||||
self.parent.v_scroll_bar.set_value(self._byte_offset)
|
||||
|
||||
# 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
|
||||
@@ -332,16 +323,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()
|
||||
|
||||
@@ -356,14 +347,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()
|
||||
@@ -373,10 +367,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))
|
||||
@@ -387,10 +381,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()
|
||||
|
||||
@@ -401,20 +401,29 @@ class InnerBigText(QWidget):
|
||||
self.update()
|
||||
|
||||
@Slot()
|
||||
def v_scroll_event(self, byte_offset: str):
|
||||
def v_scroll_value_changed(self, byte_offset: str):
|
||||
self._byte_offset = int(byte_offset)
|
||||
self.update()
|
||||
|
||||
@Slot()
|
||||
def v_scroll_update_follow_tail(self, scrolled_to_end: bool):
|
||||
self._follow_tail = scrolled_to_end
|
||||
def v_scroll_event(self, event: BigScrollBar.ScrollEvent):
|
||||
match event:
|
||||
case BigScrollBar.ScrollEvent.LinesUp:
|
||||
self.scroll_by_lines(-3)
|
||||
case BigScrollBar.ScrollEvent.LinesDown:
|
||||
self.scroll_by_lines(3)
|
||||
case BigScrollBar.ScrollEvent.PageUp:
|
||||
self.scroll_by_lines(-(int(self.lines_shown()) - 1))
|
||||
case BigScrollBar.ScrollEvent.PageDown:
|
||||
self.scroll_by_lines(int(self.lines_shown()) - 1)
|
||||
|
||||
def update_longest_line(self, line: Line):
|
||||
|
||||
def update_longest_line(self, length: int):
|
||||
width_in_chars = self.width() / self.char_width
|
||||
# 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:
|
||||
@@ -429,29 +438,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
|
||||
return SelectionPos(current_byte, True, 1)
|
||||
|
||||
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(
|
||||
@@ -485,13 +528,13 @@ 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,
|
||||
caption=_("Save File"),
|
||||
dir=os.path.dirname(self.model.get_file())
|
||||
dir=os.path.dirname(self.model.get_original_file())
|
||||
)
|
||||
if selected_file:
|
||||
self.model.write_range(start, end, selected_file)
|
||||
@@ -500,18 +543,25 @@ 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
|
||||
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)
|
||||
|
||||
@@ -533,22 +583,35 @@ class InnerBigText(QWidget):
|
||||
PluginRegistry.execute("update_status_bar", "")
|
||||
|
||||
def _file_changed(self):
|
||||
if self._follow_tail:
|
||||
self.scroll_to_byte(self.model.byte_count())
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
start_ns = time.process_time_ns()
|
||||
# print(f"paint {self.model.get_file()} at {self._byte_offset} with follow_tail={self._follow_tail}")
|
||||
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)))
|
||||
@@ -559,10 +622,11 @@ class InnerBigText(QWidget):
|
||||
byte_count = self.model.byte_count()
|
||||
vmax = byte_count - 1 if self._range_end < 0 else min(self._range_end, self.model.byte_count() - 1)
|
||||
self.parent.v_scroll_bar.setMaximum(vmax)
|
||||
if self.parent.show_range_slider:
|
||||
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():
|
||||
@@ -581,23 +645,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():
|
||||
@@ -607,15 +672,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
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fnmatch
|
||||
from typing import Optional
|
||||
|
||||
from src.ui.bigtext.highlight import Highlight
|
||||
@@ -12,7 +13,7 @@ import re
|
||||
class HighlightRegex(Highlight):
|
||||
|
||||
def __init__(self, query: str, ignore_case: bool, is_regex: bool, hit_background_color: str = "None",
|
||||
line_background_color: str = "None", active: bool = True):
|
||||
line_background_color: str = "None", active: bool = True, activated_for_file_type: str = "*"):
|
||||
self.active = active
|
||||
self.query = query
|
||||
self.ignore_case = ignore_case
|
||||
@@ -20,6 +21,7 @@ class HighlightRegex(Highlight):
|
||||
self.regex = self._get_regex()
|
||||
self.hit_background_color = hit_background_color
|
||||
self.line_background_color = line_background_color
|
||||
self.activated_for_file_type = activated_for_file_type
|
||||
self._brush_hit = self.brush(self.hit_background_color)
|
||||
self._brush_line = self.brush(self.line_background_color)
|
||||
|
||||
@@ -69,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
|
||||
@@ -99,3 +104,15 @@ class HighlightRegex(Highlight):
|
||||
alpha = int(color[6:8], 16)
|
||||
return QBrush(QColor(red, green, blue, alpha))
|
||||
return QBrush()
|
||||
|
||||
def set_activated_for_file_type(self, activated_for_file_type: str):
|
||||
self.activated_for_file_type = activated_for_file_type
|
||||
|
||||
def file_type_matches(self, file_name: str) -> bool:
|
||||
if self.activated_for_file_type is None or len(self.activated_for_file_type) == 0:
|
||||
return True
|
||||
glob_patterns: [str] = self.activated_for_file_type.split(",") # support multiple globs like: "*.txt, *.csv"
|
||||
for glob_pattern in glob_patterns:
|
||||
if fnmatch.fnmatch(file_name, glob_pattern.strip()):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@ class Highlighting:
|
||||
is_regex = session.getboolean(section, "is-regex", fallback=False)
|
||||
line_background_color = session.get(section, "line.background.color", fallback="None")
|
||||
hit_background_color = session.get(section, "hit.background.color", fallback="None")
|
||||
activated_for_file_type = session.get(section, "activated-for-file-type", fallback="*")
|
||||
|
||||
try:
|
||||
highlight = HighlightRegex(
|
||||
@@ -33,7 +34,8 @@ class Highlighting:
|
||||
is_regex=is_regex,
|
||||
hit_background_color=hit_background_color,
|
||||
line_background_color=line_background_color,
|
||||
active=active
|
||||
active=active,
|
||||
activated_for_file_type=activated_for_file_type
|
||||
)
|
||||
result.append(highlight)
|
||||
except:
|
||||
@@ -57,6 +59,7 @@ class Highlighting:
|
||||
settings.session.set(section, "is-regex", str(highlighter.is_regex))
|
||||
settings.session.set(section, "line.background.color", highlighter.line_background_color)
|
||||
settings.session.set(section, "hit.background.color", highlighter.hit_background_color)
|
||||
settings.session.set(section, "activated-for-file-type", highlighter.activated_for_file_type)
|
||||
|
||||
@staticmethod
|
||||
def remove_highlighting_sections(settings: Settings):
|
||||
|
||||
@@ -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_prepared_for_display())
|
||||
|
||||
def byte_offset(self) -> int:
|
||||
return self._byte_offset
|
||||
|
||||
@@ -37,7 +43,8 @@ class Line:
|
||||
return len(prefix_chars)
|
||||
|
||||
def line_prepared_for_display(self) -> str:
|
||||
line = self._line_tabs_replaced()
|
||||
# line = self._line_tabs_replaced()
|
||||
line = self._line
|
||||
line = self._replace_control_chars_with_pictures(line)
|
||||
return line
|
||||
|
||||
@@ -102,7 +109,7 @@ class Line:
|
||||
if not result in self._column_to_char_cache:
|
||||
self._column_to_char_cache[result] = i
|
||||
current_char = self._line[i]
|
||||
if current_char == "\t":
|
||||
if False and current_char == "\t":
|
||||
result = result + constants.tab_width - result % constants.tab_width
|
||||
else:
|
||||
result = result + 1
|
||||
@@ -116,7 +123,7 @@ class Line:
|
||||
# todo there are many other character combinations that should be skipped
|
||||
while i < len(self._line) and unicodedata.category(self._line[i]) == "Mn":
|
||||
self._char_to_column_cache[i] = result - 1
|
||||
if not result in self._column_to_char_cache:
|
||||
if (result - 1) not in self._column_to_char_cache:
|
||||
self._column_to_char_cache[result - 1] = i
|
||||
i = i + 1
|
||||
|
||||
@@ -131,11 +138,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)
|
||||
@@ -7,6 +7,7 @@ from src.ui.bigtext.highlighting import Highlighting
|
||||
from src.ui.bigtext.line import Line
|
||||
import os
|
||||
from src.settings.settings import Settings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class LogFileModel:
|
||||
@@ -21,21 +22,32 @@ class LogFileModel:
|
||||
range_start = 0
|
||||
range_end = -1
|
||||
|
||||
def __init__(self, file: str, settings: Settings):
|
||||
def __init__(self, file: str, settings: Settings, original_file: str = False):
|
||||
"""
|
||||
:param file:
|
||||
:param settings:
|
||||
:param original_file: used in the filter widget to denote the original file, the one being filtered, because 'file' points to the tmp file
|
||||
"""
|
||||
self.settings = settings
|
||||
self._file = os.path.realpath(file)
|
||||
self._original_file = os.path.realpath(original_file) if original_file else self._file
|
||||
self._file_name = os.path.basename(self._original_file)
|
||||
self._line_cache = {}
|
||||
|
||||
def highlighters(self):
|
||||
all_highlighters = Highlighting.read_config(self.settings)
|
||||
active_highlighters = []
|
||||
for h in all_highlighters:
|
||||
if h.is_active():
|
||||
if h.is_active() and h.file_type_matches(self._file_name):
|
||||
active_highlighters.append(h)
|
||||
return active_highlighters
|
||||
|
||||
def get_file(self):
|
||||
return self._file
|
||||
|
||||
def get_original_file(self):
|
||||
return self._original_file
|
||||
|
||||
def __str__(self):
|
||||
return self._file
|
||||
|
||||
@@ -123,6 +135,14 @@ class LogFileModel:
|
||||
def _is_word_char(self, char: str) -> bool:
|
||||
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())}")
|
||||
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:
|
||||
del self._line_cache[key]
|
||||
|
||||
|
||||
def data(self, byte_offset: int, scroll_lines: int, lines: int, range_start: int, range_end: int) -> List[Line]:
|
||||
# print("data(%s, %s, %s)" % (byte_offset, scroll_lines, lines))
|
||||
lines_before_offset: List[Line] = []
|
||||
@@ -138,19 +158,36 @@ class LogFileModel:
|
||||
offset = max(0,
|
||||
max(range_start - self.settings.max_line_length(), offset - self.settings.max_line_length()))
|
||||
|
||||
self.prune_cache(range_start, range_end)
|
||||
|
||||
previous_line_is_complete = False
|
||||
f.seek(offset)
|
||||
while l := f.readline():
|
||||
while True:
|
||||
line: Line | None = self._line_cache.get(offset)
|
||||
if line is None:
|
||||
line_bytes = f.readline()
|
||||
if not line_bytes:
|
||||
break
|
||||
new_offset = f.tell()
|
||||
if 0 <= range_end < new_offset:
|
||||
break
|
||||
line = Line(offset, new_offset, l.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
|
||||
previous_line_is_complete = True
|
||||
else:
|
||||
# print(f"loaded cached line at offset {offset}")
|
||||
offset = line.byte_end() # line.byte_end() returns the end byte +1
|
||||
f.seek(offset)
|
||||
previous_line_is_complete = True
|
||||
|
||||
if line.byte_end() <= byte_offset: # line.byte_end() returns the end byte +1
|
||||
if line.byte_offset() >= range_start: # only add if in range
|
||||
lines_before_offset.append(line)
|
||||
else:
|
||||
lines_after_offset.append(line)
|
||||
offset = f.tell()
|
||||
|
||||
if len(lines_after_offset) >= lines_to_find:
|
||||
break
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class NewHighlightingDialog(QDialog):
|
||||
|
||||
def _new_highlighter(self):
|
||||
highlight_regex = HighlightRegex("", ignore_case=True, is_regex=True, hit_background_color="ccb400",
|
||||
line_background_color="fff080")
|
||||
line_background_color="None")
|
||||
self._add_highlight_regex_to_list(highlight_regex, select=True)
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class HighlightListItemWidget(QWidget):
|
||||
self.active = QCheckBox("")
|
||||
self.active.setChecked(highlight_regex.is_active())
|
||||
self.active.stateChanged.connect(self._change_active_state)
|
||||
self.layout.addWidget(self.active, row, 0, 3, 1, alignment=QtCore.Qt.AlignmentFlag.AlignVCenter)
|
||||
self.layout.addWidget(self.active, row, 0, 4, 1, alignment=QtCore.Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
query = QLineEdit(self)
|
||||
query.setText(highlight_regex.query)
|
||||
@@ -181,6 +181,18 @@ class HighlightListItemWidget(QWidget):
|
||||
is_regex.setEnabled(highlight_regex.is_active())
|
||||
self.layout.addWidget(is_regex, row, 3)
|
||||
|
||||
row = row + 1
|
||||
activated_for_file_type_label = QLabel(_("File Type:"), self)
|
||||
activated_for_file_type_label.setEnabled(highlight_regex.is_active())
|
||||
self.layout.addWidget(activated_for_file_type_label, row, 1)
|
||||
activated_for_file_type = QLineEdit(self)
|
||||
activated_for_file_type.setEnabled(highlight_regex.is_active())
|
||||
activated_for_file_type.setText(highlight_regex.activated_for_file_type)
|
||||
activated_for_file_type.textChanged[str].connect(
|
||||
lambda: highlight_regex.set_activated_for_file_type(activated_for_file_type.text()))
|
||||
self.layout.addWidget(activated_for_file_type, row, 2)
|
||||
|
||||
|
||||
def _change_active_state(self):
|
||||
active = self.active.isChecked()
|
||||
self.highlight_regex.set_active(active)
|
||||
|
||||
11
src/ui/bigtext/selectionPos.py
Normal file
11
src/ui/bigtext/selectionPos.py
Normal file
@@ -0,0 +1,11 @@
|
||||
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)
|
||||
@@ -31,9 +31,9 @@ class MyTestCase(unittest.TestCase):
|
||||
self.assertEqual(9, line.column_to_char(15)) # tab
|
||||
self.assertEqual(10, line.column_to_char(16)) # g
|
||||
|
||||
def test_column_to_char_ignore_nonspacing_mark_charaters(self):
|
||||
def test_column_to_char_ignore_nonspacing_mark_characters(self):
|
||||
"""
|
||||
nonspacing mark charaters are those little decorations that are applied to the previous character,
|
||||
nonspacing mark characters are those little decorations that are applied to the previous character,
|
||||
e.g. x\u0308 to make ẍ
|
||||
:return:
|
||||
"""
|
||||
@@ -63,7 +63,7 @@ class MyTestCase(unittest.TestCase):
|
||||
|
||||
def test_char_to_column_ignore_nonspacing_mark_charaters(self):
|
||||
"""
|
||||
nonspacing mark charaters are those little decorations that are applied to the previous character,
|
||||
nonspacing mark characters are those little decorations that are applied to the previous character,
|
||||
e.g. x\u0308 to make ẍ
|
||||
:return:
|
||||
"""
|
||||
@@ -78,20 +78,6 @@ class MyTestCase(unittest.TestCase):
|
||||
self.assertEqual(2, line.char_to_column(4)) # z̈
|
||||
self.assertEqual(2, line.char_to_column(5)) # z̈
|
||||
|
||||
def test_line_tabs_replaced(self):
|
||||
byte_offset = 123
|
||||
text = "\ta\tb" # will be rendered as: ....abc where . represents a whitespace column
|
||||
expected = " a b"
|
||||
line = Line(byte_offset=byte_offset, byte_end=byte_offset + len(text.encode("utf8")), line=text)
|
||||
self.assertEqual(expected, line.line_prepared_for_display())
|
||||
|
||||
def test_line_tabs_replaced_performance(self):
|
||||
byte_offset = 123
|
||||
text = "a\t" * 10000
|
||||
expected = "a " * 10000
|
||||
line = Line(byte_offset=byte_offset, byte_end=byte_offset + len(text.encode("utf8")), line=text)
|
||||
self.assertEqual(expected, line.line_prepared_for_display())
|
||||
|
||||
def test_byte_index_to_char_index(self):
|
||||
byte_offset = 123
|
||||
text = "x\u0308y\u0308z\u0308\t\u0308a"
|
||||
@@ -119,7 +105,7 @@ class MyTestCase(unittest.TestCase):
|
||||
for i in range(128):
|
||||
if unicodedata.category(chr(i)) == "Cc":
|
||||
# print(i, " -> ", ord(chr(i)), " --> ", chr(9216 + i))
|
||||
if not i in [9, 10, 11, 13]:
|
||||
if i not in [9, 10, 11, 13]:
|
||||
text = text + chr(i)
|
||||
|
||||
line = Line(byte_offset=byte_offset, byte_end=byte_offset + len(text.encode("utf8")), line=text)
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
import PySide6
|
||||
from PySide6 import QtGui
|
||||
from PySide6.QtCore import QRect, QPoint, Signal
|
||||
from PySide6.QtGui import QPainter, Qt
|
||||
from PySide6.QtGui import QPainter, Qt, QColor, QPen
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from src.pluginregistry import PluginRegistry
|
||||
@@ -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
|
||||
@@ -44,8 +45,6 @@ class RangeSlider(QWidget):
|
||||
super(RangeSlider, self).__init__()
|
||||
self.setFixedWidth(self._width)
|
||||
|
||||
self.draw_ticks = False
|
||||
|
||||
self.min_value = 0
|
||||
self.max_value = 100
|
||||
|
||||
@@ -54,6 +53,8 @@ class RangeSlider(QWidget):
|
||||
|
||||
self.selected_handle = None
|
||||
self.selection_drag_range = (self.min_value, self.max_value)
|
||||
self.drag_y_offset_in_handle = 0
|
||||
self._hit_positions: set[float] = set(())
|
||||
|
||||
def set_maximum(self, max: int):
|
||||
if self.max_value == max:
|
||||
@@ -66,45 +67,37 @@ class RangeSlider(QWidget):
|
||||
self.upper_value.value = max
|
||||
self._emit_value_changed()
|
||||
|
||||
def update_hit_positions(self, hit_positions: set[float]):
|
||||
# print(f"updated hit positions in range slider:{len(hit_positions)} -> {hit_positions}")
|
||||
self._hit_positions = hit_positions
|
||||
|
||||
def paintEvent(self, event: PySide6.QtGui.QPaintEvent) -> None:
|
||||
painter = QPainter(self)
|
||||
self._draw_background(painter)
|
||||
if self.draw_ticks:
|
||||
self._draw_ticks(painter)
|
||||
self._draw_handle(painter, self.lower_value)
|
||||
self._draw_handle(painter, self.upper_value, direction=-1)
|
||||
|
||||
self._draw_hits(painter)
|
||||
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_ticks(self, painter: QPainter) -> None:
|
||||
painter.setPen(to_qcolor("333333"))
|
||||
|
||||
min_tick_distance = 25
|
||||
full_height = self.height() - 2 * self._handle_width
|
||||
ticks = math.floor(full_height / min_tick_distance)
|
||||
actual_tick_distance = full_height / ticks
|
||||
print(f"ticks {ticks}")
|
||||
y = actual_tick_distance + self._handle_width
|
||||
while y < full_height:
|
||||
painter.drawLine(8, y, 12, y)
|
||||
y = y + actual_tick_distance
|
||||
|
||||
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"))
|
||||
@@ -112,21 +105,32 @@ 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
|
||||
pixel = (self.height() - 2 * self._handle_width) * value_percent + self._handle_width
|
||||
return pixel
|
||||
|
||||
def _draw_hits(self, painter: QPainter) -> None:
|
||||
color = to_qcolor("000000")
|
||||
color.setAlpha(192)
|
||||
pen = QPen(color)
|
||||
pen.setWidth(1)
|
||||
painter.setPen(pen)
|
||||
|
||||
# compute where to draw a line and then deduplicate then, so that we don't draw lines multiple times
|
||||
# this is for performance and because we use transparency and drawing a line multiple times would make it
|
||||
# darker
|
||||
paint_at_y_positions: set[int] = set(())
|
||||
for hit_position in self._hit_positions:
|
||||
y = (self.height() - 2 * self._handle_width) * hit_position + self._handle_width
|
||||
y = round(y)
|
||||
paint_at_y_positions.add(y)
|
||||
|
||||
for y in paint_at_y_positions:
|
||||
painter.drawLine(2, y, 18, y)
|
||||
|
||||
def _pixel_to_value(self, pixel: int) -> int:
|
||||
pixel_percent = (pixel - self._handle_width) / (self.height() - 2 * self._handle_width)
|
||||
return int(math.floor(self.max_value * pixel_percent))
|
||||
@@ -141,12 +145,14 @@ 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)
|
||||
if self._is_on_handle(self.upper_value, pos.y(), direction=-1):
|
||||
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):
|
||||
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())
|
||||
|
||||
def mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None:
|
||||
self.selected_handle = None
|
||||
@@ -154,7 +160,7 @@ class RangeSlider(QWidget):
|
||||
def mouseMoveEvent(self, e: PySide6.QtGui.QMouseEvent) -> None:
|
||||
if self.selected_handle != None:
|
||||
pos: QPoint = e.pos()
|
||||
value = self._pixel_to_value(pos.y())
|
||||
value = self._pixel_to_value(pos.y()) + self.drag_y_offset_in_handle
|
||||
if self.selection_drag_range[0] <= value <= self.selection_drag_range[1]:
|
||||
self.selected_handle.value = value
|
||||
# print("%s, %s" %(self.lower_value.value, self.upper_value.value))
|
||||
|
||||
@@ -7,12 +7,22 @@ def is_hex_color(color: str):
|
||||
return re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def is_hex_color_with_alpha(color: str):
|
||||
return re.match("[0-9a-f]{8}", color, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def to_qcolor(color: str):
|
||||
if is_hex_color(color):
|
||||
red = int(color[0:2], 16)
|
||||
green = int(color[2:4], 16)
|
||||
blue = int(color[4:6], 16)
|
||||
return QColor(red, green, blue)
|
||||
if is_hex_color_with_alpha(color):
|
||||
red = int(color[0:2], 16)
|
||||
green = int(color[2:4], 16)
|
||||
blue = int(color[4:6], 16)
|
||||
alpha = int(color[6:8], 16)
|
||||
return QColor(red, green, blue, alpha)
|
||||
elif color in QColor().colorNames():
|
||||
return QColor(color)
|
||||
return QColor(255, 255, 255)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
01234
|
||||
012345
|
||||
01234567890123456789
|
||||
012345678901234567890123456789012345678901234567890123456789
|
||||
tab indentation:
|
||||
@@ -22,6 +22,25 @@ x◌᷍◌◌᷍◌x
|
||||
Control characters:
|
||||
|
||||
------------------------------
|
||||
wide and half width characters:
|
||||
12345678
|
||||
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]
|
||||
@@ -158,3 +177,7 @@ ä---------ä----------ä---------ä----------ä---------ä----------ä--
|
||||
17
|
||||
18
|
||||
19
|
||||
|
||||
|
||||
アンドレアス
|
||||
アンドレアス
|
||||
28
version.py
Normal file
28
version.py
Normal file
@@ -0,0 +1,28 @@
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=(0, 2, 1, 0),
|
||||
prodvers=(0, 2, 1, 0),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
u'040904B0',
|
||||
[StringStruct(u'CompanyName', u''),
|
||||
StringStruct(u'FileDescription', u'KrowLog is a viewer for log files of arbitrary size.'),
|
||||
StringStruct(u'FileVersion', u'0.2.1' ),
|
||||
StringStruct(u'InternalName', u'krowlog'),
|
||||
StringStruct(u'LegalCopyright', u'\xa9 Andreas Huber'),
|
||||
StringStruct(u'OriginalFilename', u'krowlog.exe'),
|
||||
StringStruct(u'ProductName', u'KrowLog'),
|
||||
StringStruct(u'ProductVersion', u'0.2.1-dev')])
|
||||
]),
|
||||
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
|
||||
]
|
||||
)
|
||||
Reference in New Issue
Block a user