select word on double click

This commit is contained in:
2021-11-01 15:13:41 +01:00
parent 2f1aabb379
commit f471f4785e
5 changed files with 88 additions and 15 deletions

View File

@@ -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 * UTF-8 support
* Filter files with regular expressions. * Filter files with regular expressions.
* Filter matching with ignore case enabled by default. * Case insensitive filtering enabled by default.
* Filter matches are highlighted. * Filter matches are highlighted.
* Filter results don't disappear when changing tabs. * Filter results don't disappear when changing tabs.
* Colored highlighting of lines or matches based on regular expression. * Colored highlighting of lines or matches based on regular expression.
* Pleasing color palette. * Pleasing color palette.
* Highlighters are automatically saved and restored. * 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 * 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. selection into a new file instead.

View File

@@ -54,7 +54,7 @@ class AboutDialog(QDialog):
label = Label(self.tr(textwrap.dedent(""" label = Label(self.tr(textwrap.dedent("""
Log file viewer<br> Log file viewer<br>
(c) 2021 Open Text Corporation<br> (c) 2021 Open Text Corporation<br>
<a href="https://www.opentext.com/">License: to be decided</a>"""))) <a href="https://www.opentext.com/">License: GPL</a>""")))
result.layout.addWidget(label) result.layout.addWidget(label)
return result return result

View File

@@ -184,6 +184,18 @@ class InnerBigText(QWidget):
self.selection_highlight.set_end_byte(offset) self.selection_highlight.set_end_byte(offset)
self.update() 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): def mouseMoveEvent(self, e: QMouseEvent):
if e.buttons() != Qt.MouseButton.LeftButton: if e.buttons() != Qt.MouseButton.LeftButton:
@@ -243,7 +255,7 @@ class InnerBigText(QWidget):
if line_number < len(self.lines): if line_number < len(self.lines):
line = self.lines[line_number] line = self.lines[line_number]
column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset 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)) # print("%s in line %s" % (char_in_line, line_number))
byte_in_line = line.char_index_to_byte(char_in_line) byte_in_line = line.char_index_to_byte(char_in_line)
current_byte = line.byte_offset() + byte_in_line current_byte = line.byte_offset() + byte_in_line

View File

@@ -76,19 +76,40 @@ class LogFileModel:
target.write(buffer) target.write(buffer)
offset = new_offset 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)) # print("data(%s, %s, %s)" % (byte_offset, scroll_lines, lines))
lines_before_offset: List[Line] = [] lines_before_offset: List[Line] = []
lines_after_offset: List[Line] = [] lines_after_offset: List[Line] = []
lines_to_find = lines + abs(scroll_lines) lines_to_find = lines + abs(scroll_lines)
lines_to_return = math.ceil(lines) lines_to_return = math.ceil(lines)
start = time.time()
# with self._lock: # with self._lock:
if True: if True:
duration = time.time() - start
if duration > 10:
print("data lock acquision %.4f" % ())
# TODO handle lines longer than 4096 bytes # TODO handle lines longer than 4096 bytes
# TODO abort file open after a few secons: https://docs.python.org/3/library/signal.html#example # TODO abort file open after a few secons: https://docs.python.org/3/library/signal.html#example
with open(self._file, 'rb') as f: with open(self._file, 'rb') as f:
@@ -102,8 +123,8 @@ class LogFileModel:
new_offset = f.tell() new_offset = f.tell()
line = Line(offset, new_offset, line = Line(offset, new_offset,
l.decode("utf8", errors="ignore").replace("\r", "").replace("\n", "")) l.decode("utf8", errors="ignore").replace("\r", "").replace("\n", ""))
# print("%s %s" %(line.byte_offset(), line.line())) # print("%s %s %s" %(line.byte_offset(), line.line(), line.byte_end()))
if offset < byte_offset: if line.byte_end() <= byte_offset: # line.byte_end() returns the end byte +1
lines_before_offset.append(line) lines_before_offset.append(line)
else: else:
lines_after_offset.append(line) lines_after_offset.append(line)
@@ -120,6 +141,8 @@ class LogFileModel:
result = all_lines[-lines_to_return + 1:] result = all_lines[-lines_to_return + 1:]
# print("returning %s lines" % (len(result))) # 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 return result
def byte_count(self) -> int: def byte_count(self) -> int:

View File

@@ -1,9 +1,11 @@
import unittest import unittest
import tempfile import tempfile
from configparser import ConfigParser
from os.path import join from os.path import join
from logFileModel import LogFileModel from logFileModel import LogFileModel
from line import Line from line import Line
from settings import Settings
class TestLogFileModel(unittest.TestCase): class TestLogFileModel(unittest.TestCase):
@@ -11,7 +13,7 @@ class TestLogFileModel(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_dir = tempfile.TemporaryDirectory() self.test_dir = tempfile.TemporaryDirectory()
self.tmpfile = join(self.test_dir.name, "my.log") self.tmpfile = join(self.test_dir.name, "my.log")
self.model = LogFileModel(self.tmpfile) self.model = LogFileModel(self.tmpfile, Settings(ConfigParser()))
def tearDown(self): def tearDown(self):
self.test_dir.cleanup() self.test_dir.cleanup()
@@ -31,7 +33,7 @@ class TestLogFileModel(unittest.TestCase):
def test_load_from_middle_of_first_line(self): def test_load_from_middle_of_first_line(self):
self.write_str("abc\ndef\nghi\njkl") self.write_str("abc\ndef\nghi\njkl")
expected_lines = ["def", "ghi", "jkl"] expected_lines = ["abc", "def", "ghi"]
lines = self.model.data(1, 0, 3) lines = self.model.data(1, 0, 3)
@@ -40,7 +42,7 @@ class TestLogFileModel(unittest.TestCase):
def test_read_from_newline_character(self): def test_read_from_newline_character(self):
self.write_str("abc\ndef\nghi\njkl") self.write_str("abc\ndef\nghi\njkl")
expected_lines = ["def", "ghi"] expected_lines = ["abc", "def"]
lines = self.model.data(3, 0, 2) lines = self.model.data(3, 0, 2)
@@ -137,6 +139,37 @@ class TestLogFileModel(unittest.TestCase):
line_str = [l.line() for l in lines] line_str = [l.line() for l in lines]
self.assertEqual(expected_lines, line_str) 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__': if __name__ == '__main__':
unittest.main() unittest.main()