Compare commits

...

9 Commits

Author SHA1 Message Date
d4b962769f install isort and black for code formatting 2025-05-16 08:40:43 +02:00
d9c362419b handle tabs correctly when highlighting text
With the old solution I replaced tabs with four spaces and then did
some calculations to get the correct byte positions for a coordinate.
With the new solution (using FontMetric.horizontalAdvance) this is not
necessary anymore and we can use tha native tab.
But I cannot change the tab width. It is always 8 characters.
You can set tabStopDistance on horizontalAdvance(), but not on
elided_text() and I did not find a way to use it while rendering text.
2025-05-06 20:45:19 +02:00
bfd8ce841f add .envrc so that direnv can set the venv dir to the path 2025-05-06 20:17:10 +02:00
a41e5b79a3 add 'about qt' dialog 2025-05-06 20:14:59 +02:00
75578b6126 rename venv 2025-04-06 19:02:46 +02:00
9afc4d1d9c fix exception when selecting text by clicking behind the last character 2025-04-06 19:02:33 +02:00
d36724f3e7 replace watchdog with active polling thread
watchdog does not work on windows. For some
reason file modification events are not
emitted.
Fixed by replacing watchdog with a thread
that polls the modification date ever 0.5s.

Also fixed a bug that the hit view was not
properly destructed.
2025-04-06 09:34:08 +02:00
bcd525d787 ignore version file 2025-04-05 10:46:38 +02:00
8289042af4 remove version file from repository
I don't want to have to commit an ever changing version file.
2025-04-05 10:46:00 +02:00
16 changed files with 138 additions and 74 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
PATH=$(pwd)/venv312/bin:$PATH

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ dist
testbed
icons-not-used
venv*
*.spec
*.spec
/version.txt

1
.idea/misc.xml generated
View File

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

View File

@@ -1,6 +1,7 @@
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"

View File

@@ -1,21 +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
from pathlib import Path
import os
__version__ = Path(os.path.dirname(os.path.realpath(__file__)) + os.sep + "version.txt").read_text()
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")
@@ -27,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()
@@ -67,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)
@@ -76,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()

View File

@@ -18,9 +18,7 @@ arguments = [
'--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'
]

View File

@@ -1,5 +1,6 @@
pip==25.0.1
PySide6_Essentials==6.8.2.1
setuptools==77.0.3
watchdog==6.0.0
pyinstaller==6.12.0
isort==6.0.1
black==25.1.0

View 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)

View File

@@ -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 *
@@ -75,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()

View File

@@ -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',

View File

@@ -279,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):

View File

@@ -23,55 +23,31 @@ 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):
@@ -503,7 +479,7 @@ class InnerBigText(QWidget):
# # 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("")
@@ -581,7 +557,6 @@ class InnerBigText(QWidget):
def _toggle_follow(self):
self._follow = not self._follow
print(f"follow={self._follow}")
self.update()
def _update_highlight_selected_text(self):

View File

@@ -15,7 +15,7 @@ class Line:
self._cache_char_to_column()
def get_width_in_px(self, font_metric: QFontMetrics):
return font_metric.horizontalAdvance(self._line)
return font_metric.horizontalAdvance(self.line_prepared_for_display())
def byte_offset(self) -> int:
return self._byte_offset
@@ -43,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
@@ -108,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

View File

@@ -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"

View File

@@ -177,3 +177,7 @@ ä---------ä----------ä---------ä----------ä---------ä----------ä--
17
18
19
アンドレアス
アンドレアス

View File

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