select word on double click
This commit is contained in:
11
README.md
11
README.md
@@ -1,13 +1,18 @@
|
|||||||
# Features
|
#  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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
14
bigtext.py
14
bigtext.py
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user