From f471f4785eb578bea4c3c5900fecb32ce6827403 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Mon, 1 Nov 2021 15:13:41 +0100 Subject: [PATCH] select word on double click --- README.md | 11 ++++++++--- aboutdialog.py | 2 +- bigtext.py | 14 +++++++++++++- logFileModel.py | 37 ++++++++++++++++++++++++++++++------- testlogfilemodel.py | 39 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 88 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 654f345..1787bac 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ -# Features +# ![Logo for RavenLog](icon7.png "Logo for RavenLog") RavenLog -* Open text files of arbitrary size. +RavenLog is a viewer for text files of arbitrary length. + +## Features + +* Text files of arbitrary size. * UTF-8 support * Filter files with regular expressions. -* Filter matching with ignore case enabled by default. +* Case insensitive filtering enabled by default. * Filter matches are highlighted. * Filter results don't disappear when changing tabs. * Colored highlighting of lines or matches based on regular expression. * Pleasing color palette. * Highlighters are automatically saved and restored. +* Select arbitrary strings (not just fill lines). * Copy protection: Users is warned before creating a clipboard more than 5 MB in size. They can choose to copy the selection into a new file instead. diff --git a/aboutdialog.py b/aboutdialog.py index 4cb17b8..fc3320e 100644 --- a/aboutdialog.py +++ b/aboutdialog.py @@ -54,7 +54,7 @@ class AboutDialog(QDialog): label = Label(self.tr(textwrap.dedent(""" Log file viewer
(c) 2021 Open Text Corporation
- License: to be decided"""))) + License: GPL"""))) result.layout.addWidget(label) return result diff --git a/bigtext.py b/bigtext.py index 05cd618..089d13d 100644 --- a/bigtext.py +++ b/bigtext.py @@ -184,6 +184,18 @@ class InnerBigText(QWidget): self.selection_highlight.set_end_byte(offset) self.update() + def mouseDoubleClickEvent(self, e: QtGui.QMouseEvent) -> None: + if e.buttons() == Qt.MouseButton.LeftButton: + offset = self.to_byte_offset(e) + (_word, start_byte, end_byte) = self.model.read_word_at(offset) + if start_byte >= 0 and end_byte >= 0: + self.selection_highlight.set_start(start_byte) + self.selection_highlight.set_end_byte(end_byte) + else: + self.selection_highlight.set_start(offset) + self.selection_highlight.set_end_byte(offset) + self.update() + def mouseMoveEvent(self, e: QMouseEvent): if e.buttons() != Qt.MouseButton.LeftButton: @@ -243,7 +255,7 @@ class InnerBigText(QWidget): if line_number < len(self.lines): line = self.lines[line_number] column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset - char_in_line = min(column_in_line, line.length()) + char_in_line = min(column_in_line, line.length() - 1) # print("%s in line %s" % (char_in_line, line_number)) byte_in_line = line.char_index_to_byte(char_in_line) current_byte = line.byte_offset() + byte_in_line diff --git a/logFileModel.py b/logFileModel.py index e584089..bb142b1 100644 --- a/logFileModel.py +++ b/logFileModel.py @@ -76,19 +76,40 @@ class LogFileModel: target.write(buffer) offset = new_offset - def data(self, byte_offset, scroll_lines, lines) -> List[Line]: + def read_word_at(self, byte_offset: int) -> (str, int, int): + lines = self.data(byte_offset, 0, 1) + if len(lines) == 0: + return ("", -1, -1) + line: Line = lines[0] + if not lines[0].includes_byte(byte_offset): + return ("", -1, -1) + + offset_in_line = byte_offset - line.byte_offset() + current_char = line.line()[line.byte_index_to_char_index(offset_in_line)] + if not self._is_word_char(current_char): + return (current_char, byte_offset, byte_offset + 1) + start_in_line = byte_offset - line.byte_offset() + while start_in_line - 1 >= 0 and self._is_word_char(line.line()[start_in_line - 1]): + start_in_line = start_in_line - 1 + end_in_line = byte_offset - line.byte_offset() + while end_in_line < len(line.line()) and self._is_word_char(line.line()[end_in_line]): + end_in_line = end_in_line + 1 + start_byte = start_in_line + line.byte_offset() + end_byte = end_in_line + line.byte_offset() + return (line.line()[start_in_line:end_in_line], start_byte, end_byte) + + def _is_word_char(self, char: str) -> bool: + return re.match("\w", char) + + def data(self, byte_offset: int, scroll_lines: int, lines: int) -> List[Line]: # print("data(%s, %s, %s)" % (byte_offset, scroll_lines, lines)) lines_before_offset: List[Line] = [] lines_after_offset: List[Line] = [] lines_to_find = lines + abs(scroll_lines) lines_to_return = math.ceil(lines) - start = time.time() # with self._lock: if True: - duration = time.time() - start - if duration > 10: - print("data lock acquision %.4f" % ()) # TODO handle lines longer than 4096 bytes # TODO abort file open after a few secons: https://docs.python.org/3/library/signal.html#example with open(self._file, 'rb') as f: @@ -102,8 +123,8 @@ class LogFileModel: new_offset = f.tell() line = Line(offset, new_offset, l.decode("utf8", errors="ignore").replace("\r", "").replace("\n", "")) - # print("%s %s" %(line.byte_offset(), line.line())) - if offset < byte_offset: + # print("%s %s %s" %(line.byte_offset(), line.line(), line.byte_end())) + if line.byte_end() <= byte_offset: # line.byte_end() returns the end byte +1 lines_before_offset.append(line) else: lines_after_offset.append(line) @@ -120,6 +141,8 @@ class LogFileModel: result = all_lines[-lines_to_return + 1:] # print("returning %s lines" % (len(result))) + # if len(result) > 0: + # print("returning %s %d -> %d" % (result[0].line(), result[0].byte_offset(), result[0].byte_end())) return result def byte_count(self) -> int: diff --git a/testlogfilemodel.py b/testlogfilemodel.py index 3305e81..54a98e3 100644 --- a/testlogfilemodel.py +++ b/testlogfilemodel.py @@ -1,9 +1,11 @@ import unittest import tempfile +from configparser import ConfigParser from os.path import join from logFileModel import LogFileModel from line import Line +from settings import Settings class TestLogFileModel(unittest.TestCase): @@ -11,7 +13,7 @@ class TestLogFileModel(unittest.TestCase): def setUp(self): self.test_dir = tempfile.TemporaryDirectory() self.tmpfile = join(self.test_dir.name, "my.log") - self.model = LogFileModel(self.tmpfile) + self.model = LogFileModel(self.tmpfile, Settings(ConfigParser())) def tearDown(self): self.test_dir.cleanup() @@ -31,7 +33,7 @@ class TestLogFileModel(unittest.TestCase): def test_load_from_middle_of_first_line(self): self.write_str("abc\ndef\nghi\njkl") - expected_lines = ["def", "ghi", "jkl"] + expected_lines = ["abc", "def", "ghi"] lines = self.model.data(1, 0, 3) @@ -40,7 +42,7 @@ class TestLogFileModel(unittest.TestCase): def test_read_from_newline_character(self): self.write_str("abc\ndef\nghi\njkl") - expected_lines = ["def", "ghi"] + expected_lines = ["abc", "def"] lines = self.model.data(3, 0, 2) @@ -137,6 +139,37 @@ class TestLogFileModel(unittest.TestCase): line_str = [l.line() for l in lines] self.assertEqual(expected_lines, line_str) + def test_read_word_at_middle_of_line(self): + text = "0___\nlorem ipsum dolor sit amet\n2___" + self.write_str(text) + expected = ("ipsum", text.index("ipsum"), text.index("ipsum") + len("ipsum")) + actual = self.model.read_word_at(text.index("ipsum") + 2) + self.assertEqual(expected, actual) + + def test_read_word_at_start_of_line(self): + text = "0___\nlorem ipsum dolor sit amet\n2___" + word = "lorem" + self.write_str(text) + expected = (word, text.index(word), text.index(word) + len(word)) + actual = self.model.read_word_at(text.index(word)) + self.assertEqual(expected, actual) + + def test_read_word_at_end_of_line(self): + text = "0___\nlorem ipsum dolor sit amet\n2___" + word = "amet" + self.write_str(text) + expected = (word, text.index(word), text.index(word) + len(word)) + actual = self.model.read_word_at(text.index(word)) + self.assertEqual(expected, actual) + + def test_read_word_at_beginning_of_file(self): + text = "lorem ipsum dolor sit amet\n1___" + word = "lorem" + self.write_str(text) + expected = (word, text.index(word), text.index(word) + len(word)) + actual = self.model.read_word_at(text.index(word)) + self.assertEqual(expected, actual) + if __name__ == '__main__': unittest.main()