rename ravenlog to krowlog
There is a database named RavenDB. KrowLog starts with a K, which is a) distinctive and b) has an association to KDE.
This commit is contained in:
25
src/plugins/__init__.py
Normal file
25
src/plugins/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from inspect import isclass
|
||||
from pkgutil import iter_modules
|
||||
from pathlib import Path
|
||||
from importlib import import_module
|
||||
from src.pluginbase import PluginBase
|
||||
|
||||
# iterate through the modules in the current package
|
||||
from src.pluginregistry import PluginRegistry
|
||||
|
||||
if False:
|
||||
package_dir = Path(__file__).resolve().parent
|
||||
for (_, module_name, _) in iter_modules([str(package_dir)]):
|
||||
# import the module and iterate through its attributes
|
||||
module = import_module(f"{__name__}.{module_name}")
|
||||
print("module: %s" % module)
|
||||
|
||||
for attribute_name in dir(module):
|
||||
if attribute_name == "PluginBase":
|
||||
continue
|
||||
attribute = getattr(module, attribute_name)
|
||||
|
||||
if isclass(attribute) and issubclass(attribute, PluginBase):
|
||||
globals()[attribute_name] = attribute
|
||||
PluginRegistry.register_plugin(attribute_name, attribute)
|
||||
print("%s -> %s :: %s in %s" % (attribute_name, attribute, module_name, module))
|
||||
0
src/plugins/domain/__init__.py
Normal file
0
src/plugins/domain/__init__.py
Normal file
51
src/plugins/domain/menucontribution.py
Normal file
51
src/plugins/domain/menucontribution.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.domain.rmenu import RMenu
|
||||
|
||||
id_counter = 0
|
||||
|
||||
|
||||
def next_id() -> str:
|
||||
global id_counter
|
||||
id_counter = id_counter + 1
|
||||
return "action_%d" % id_counter
|
||||
|
||||
|
||||
class MenuContribution():
|
||||
def __init__(self,
|
||||
menu_id: str,
|
||||
action: RAction = None,
|
||||
menu: RMenu = None,
|
||||
action_id=None,
|
||||
after=None):
|
||||
super(MenuContribution, self).__init__()
|
||||
self.menu_id = menu_id
|
||||
self.action = action
|
||||
self.menu = menu
|
||||
self.action_id = action_id if action_id else next_id()
|
||||
self.after = after
|
||||
|
||||
|
||||
def _sort_by_action_id(menu_contributions: [MenuContribution]) -> [MenuContribution]:
|
||||
return sorted(menu_contributions, key=lambda mc: mc.action_id)
|
||||
|
||||
|
||||
def sort_menu_contributions(menu_contributions: [MenuContribution]) -> [MenuContribution]:
|
||||
result = []
|
||||
items = _sort_by_action_id(menu_contributions[:])
|
||||
|
||||
_recursive_half_order_adder(result, items, None)
|
||||
|
||||
# add remaining items to the end (ordered by their action_id)
|
||||
# This resolves cycles.
|
||||
for item in items:
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def _recursive_half_order_adder(result: [MenuContribution], items: [MenuContribution], parent):
|
||||
for item in items:
|
||||
mc: MenuContribution = item
|
||||
if not mc.after:
|
||||
result.append(mc)
|
||||
items.remove(mc)
|
||||
_recursive_half_order_adder(result, items, mc.action_id)
|
||||
88
src/plugins/domain/raction.py
Normal file
88
src/plugins/domain/raction.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtGui import QAction, QIcon
|
||||
from PySide6.QtWidgets import QMenu
|
||||
|
||||
|
||||
class RAction():
|
||||
|
||||
def __init__(self,
|
||||
label: str,
|
||||
action: Callable[[], None] = None,
|
||||
shortcut: str = None,
|
||||
icon_from_theme: str = None,
|
||||
icon_file: str = None,
|
||||
checkable: bool = False,
|
||||
checked: bool = False
|
||||
):
|
||||
"""
|
||||
|
||||
:param label: the label
|
||||
:param action: the callback to be executed when clicked. Note: use the setter when creating a checkable menu item
|
||||
:param shortcut: the shortcut, e.g. 'Ctrl+X'
|
||||
:param icon_from_theme: environment specific name of an icon. On Linux: /usr/share/icons
|
||||
:param icon_file: path to an icon
|
||||
:param checkable: if this menu item behaves like a checkbox
|
||||
:param checked: if it is checked
|
||||
"""
|
||||
super(RAction, self).__init__()
|
||||
self.label = label
|
||||
self.action = action
|
||||
self.shortcut = shortcut
|
||||
self.icon_from_theme = icon_from_theme
|
||||
self.icon_file = icon_file
|
||||
self.checkable = checkable
|
||||
self.checked = checked
|
||||
self._action: QAction = None
|
||||
|
||||
def set_action(self, action):
|
||||
self.action = lambda *args: self.decorated_action(action)
|
||||
|
||||
def decorated_action(self, action):
|
||||
if self.checkable:
|
||||
self.checked = not self.checked
|
||||
self._update_check_state()
|
||||
action()
|
||||
|
||||
def set_icon_from_theme(self, icon_from_theme: str):
|
||||
self.icon_from_theme = icon_from_theme
|
||||
|
||||
def set_icon_file(self, icon_file: str):
|
||||
self.icon_file = icon_file
|
||||
|
||||
def set_shortcut(self, shortcut: str):
|
||||
self.shortcut = shortcut
|
||||
|
||||
def set_checkable(self, checkable: bool):
|
||||
self.checkable = checkable
|
||||
|
||||
def set_checked(self, checked: bool):
|
||||
self.checked = checked
|
||||
self._update_check_state()
|
||||
|
||||
def _update_check_state(self):
|
||||
if self._action:
|
||||
if self.checked:
|
||||
self._action.setIcon(QIcon("icons/ionicons/checkbox-outline.svg"))
|
||||
else:
|
||||
self._action.setIcon(QIcon("icons/ionicons/square-outline.svg"))
|
||||
|
||||
def set_label(self, label: str):
|
||||
if self._action:
|
||||
self._action.setText(label)
|
||||
|
||||
def to_qaction(self, qmenu: QMenu) -> QAction:
|
||||
action = QAction(self.label, qmenu)
|
||||
self._action = action
|
||||
if self.icon_from_theme:
|
||||
action.setIcon(QIcon.fromTheme(self.icon_from_theme))
|
||||
if self.icon_file:
|
||||
action.setIcon(QIcon(self.icon_file))
|
||||
if self.shortcut:
|
||||
action.setShortcut(self.shortcut)
|
||||
if self.action:
|
||||
action.triggered.connect(self.action)
|
||||
if self.checkable:
|
||||
self._update_check_state()
|
||||
|
||||
return action
|
||||
27
src/plugins/domain/rmenu.py
Normal file
27
src/plugins/domain/rmenu.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Callable
|
||||
|
||||
from src.plugins.domain.raction import RAction
|
||||
|
||||
|
||||
class RMenu():
|
||||
def __init__(self, label: str, icon_from_theme: str = ""):
|
||||
super(RMenu, self).__init__()
|
||||
self.label = label
|
||||
self.actions = []
|
||||
self.listeners = []
|
||||
self.icon_from_theme = icon_from_theme;
|
||||
|
||||
def add_action(self, action: RAction):
|
||||
self.actions.append(action)
|
||||
self._notify()
|
||||
|
||||
def clear(self):
|
||||
self.actions.clear()
|
||||
self._notify()
|
||||
|
||||
def _notify(self):
|
||||
for listener in self.listeners:
|
||||
listener()
|
||||
|
||||
def add_change_listener(self, listener: Callable[[], None]):
|
||||
self.listeners.append(listener)
|
||||
45
src/plugins/domain/testmenucontribution.py
Normal file
45
src/plugins/domain/testmenucontribution.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import unittest
|
||||
from random import shuffle
|
||||
|
||||
from src.plugins.domain.menucontribution import MenuContribution, sort_menu_contributions
|
||||
|
||||
|
||||
class MyTestCase(unittest.TestCase):
|
||||
def test_sort(self):
|
||||
items = [
|
||||
MenuContribution("menuId", action_id="a", after=None),
|
||||
MenuContribution("menuId", action_id="b", after="a"),
|
||||
MenuContribution("menuId", action_id="c", after="a"),
|
||||
MenuContribution("menuId", action_id="d", after="b"),
|
||||
MenuContribution("menuId", action_id="e", after="d"),
|
||||
]
|
||||
shuffle(items)
|
||||
|
||||
actual = sort_menu_contributions(items)
|
||||
ordered_ids = ""
|
||||
for a in actual:
|
||||
ordered_ids = ordered_ids + a.action_id
|
||||
self.assertEqual("abcde", ordered_ids)
|
||||
|
||||
def test_sort_with_cycle(self):
|
||||
"""
|
||||
There is a cycle between a and b. This is resolved, because neither is set in the recursive
|
||||
part of the method. After the recursive part the remaining items are added in order of
|
||||
their action_id.
|
||||
:return:
|
||||
"""
|
||||
items = [
|
||||
MenuContribution("menuId", action_id="a", after="b"),
|
||||
MenuContribution("menuId", action_id="b", after="a"),
|
||||
]
|
||||
shuffle(items)
|
||||
|
||||
actual = sort_menu_contributions(items)
|
||||
ordered_ids = ""
|
||||
for a in actual:
|
||||
ordered_ids = ordered_ids + a.action_id
|
||||
self.assertEqual("ab", ordered_ids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
23
src/plugins/krowlog/Tab.py
Normal file
23
src/plugins/krowlog/Tab.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
|
||||
class Tab(QWidget):
|
||||
|
||||
def __init__(self, unique_id: str, title: str):
|
||||
super(Tab, self).__init__()
|
||||
self.unique_id = unique_id
|
||||
self.title = title
|
||||
|
||||
@abstractmethod
|
||||
def get_status_text(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_file(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def destruct(self):
|
||||
pass
|
||||
0
src/plugins/krowlog/__init__.py
Normal file
0
src/plugins/krowlog/__init__.py
Normal file
86
src/plugins/krowlog/aboutdialog.py
Normal file
86
src/plugins/krowlog/aboutdialog.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import textwrap
|
||||
|
||||
import PySide6
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QPixmap
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import constants
|
||||
from src.ui.label import Label
|
||||
from src.ui.vbox import VBox
|
||||
from src.i18n import _
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
"""Dialog for showing info about KrowLog"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(AboutDialog, self).__init__(parent)
|
||||
self.setWindowTitle(_("About KrowLog"))
|
||||
self.setModal(True)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
heading_app_name = QLabel(_("KrowLog"))
|
||||
heading_app_name.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
heading_app_name.setFont(QFont("default", 25))
|
||||
heading_app_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
version = QLabel(_("Version: {0}").format(self._version()))
|
||||
version.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
app_icon = QLabel()
|
||||
app_icon.setPixmap(QPixmap(constants.krow_icon))
|
||||
heading = QWidget(self)
|
||||
hbox = QHBoxLayout(heading)
|
||||
hbox.addWidget(app_icon)
|
||||
hbox.addWidget(VBox(heading_app_name, version))
|
||||
hbox.addSpacerItem(QSpacerItem(1, 1, hData=QSizePolicy.Policy.Expanding))
|
||||
|
||||
heading.layout = hbox
|
||||
self.layout.addWidget(heading)
|
||||
|
||||
tabs = QTabWidget()
|
||||
tabs.addTab(self._about(), _("About"))
|
||||
tabs.addTab(self._license(), _("License"))
|
||||
|
||||
self.layout.addWidget(tabs)
|
||||
|
||||
buttons = QDialogButtonBox(self)
|
||||
buttons.setStandardButtons(QDialogButtonBox.StandardButton.Close)
|
||||
buttons.rejected.connect(self.close)
|
||||
self.layout.addWidget(buttons)
|
||||
|
||||
def _about(self) -> QWidget:
|
||||
result = QWidget()
|
||||
result.layout = QVBoxLayout(result)
|
||||
label = Label("{0}<br>{1}<br>{2}".format(
|
||||
_("Log file viewer"),
|
||||
_("(c) 2022 Andreas Huber"),
|
||||
_("License: LGPL v3")
|
||||
))
|
||||
result.layout.addWidget(label)
|
||||
return result
|
||||
|
||||
def _license(self) -> QWidget:
|
||||
dependencies = """
|
||||
<ul>
|
||||
<li>Ionicons (MIT) - <a href="https://github.com/ionic-team/ionicons">https://github.com/ionic-team/ionicons</a></li>
|
||||
<li>PySide6 {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>urllib3 (MIT) - <a href="https://github.com/urllib3/urllib3">https://github.com/urllib3/urllib3</a></li>
|
||||
<li>watchdog 2.16 (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__)
|
||||
label = textwrap.dedent(dependencies)
|
||||
|
||||
result = QWidget()
|
||||
result.layout = QVBoxLayout(result)
|
||||
result.layout.addWidget(Label(label))
|
||||
return result
|
||||
|
||||
def _version(self):
|
||||
with open('VERSION.info', "rt") as f:
|
||||
line = f.readline()
|
||||
version = line.strip()
|
||||
return version
|
||||
|
||||
115
src/plugins/krowlogplugin.py
Normal file
115
src/plugins/krowlogplugin.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QDockWidget, QMessageBox
|
||||
|
||||
import constants
|
||||
from src.plugins.krowlog.aboutdialog import AboutDialog
|
||||
from src.mainwindow import MainWindow
|
||||
from src.pluginbase import PluginBase
|
||||
from src.plugins.domain.menucontribution import MenuContribution
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.domain.rmenu import RMenu
|
||||
from src.plugins.krowlog.Tab import Tab
|
||||
from src.i18n import _, locale
|
||||
from src.settings.settings import Settings
|
||||
|
||||
|
||||
class KrowLogPlugin(PluginBase):
|
||||
def __init__(self):
|
||||
super(KrowLogPlugin, self).__init__()
|
||||
self.main_window = None
|
||||
self._locale = locale
|
||||
self._locale_actions = {}
|
||||
self.settings = None
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
|
||||
def create_main_window(self):
|
||||
if not self.main_window:
|
||||
self.main_window = MainWindow()
|
||||
return self.main_window
|
||||
|
||||
def get_menu_contributions(self) -> [MenuContribution]:
|
||||
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("settings", menu=self._sub_menu_languages(), action_id="recent files menu"),
|
||||
]
|
||||
|
||||
def _sub_menu_languages(self) -> RMenu:
|
||||
menu = RMenu(_("&Languages"))
|
||||
self._locale_actions[''] = RAction(_("&Default"), lambda: self._change_locale(''), checkable=True)
|
||||
self._locale_actions['en'] = RAction(_("&English"), lambda: self._change_locale('en'), checkable=True)
|
||||
self._locale_actions['de'] = RAction(_("&German"), lambda: self._change_locale('de'), checkable=True)
|
||||
|
||||
for (key, action) in self._locale_actions.items():
|
||||
action.checked = self._locale == key
|
||||
menu.add_action(action)
|
||||
|
||||
if not self._locale in self._locale_actions.keys():
|
||||
self._locale_actions[''].checked = True
|
||||
return menu
|
||||
|
||||
def _change_locale(self, locale: str):
|
||||
if self._locale != locale:
|
||||
if self._locale in self._locale_actions:
|
||||
self._locale_actions[self._locale].set_checked(False)
|
||||
|
||||
self._locale_actions[locale].set_checked(True)
|
||||
self._locale = locale
|
||||
if locale == '':
|
||||
self.settings.session.remove_option('general', 'lang')
|
||||
else:
|
||||
self.settings.session.set('general', 'lang', locale)
|
||||
|
||||
info = QMessageBox(
|
||||
QMessageBox.Icon.Information,
|
||||
_("Language Changed"),
|
||||
_("The language for this application has been changed. The change will take effect the next time the application is started."))
|
||||
info.setStandardButtons(QMessageBox.Ok)
|
||||
info.exec()
|
||||
|
||||
def current_file(self) -> Optional[str]:
|
||||
return self.main_window.tabs.current_file()
|
||||
|
||||
def get_open_files(self) -> [str]:
|
||||
return self.main_window.tabs.open_files();
|
||||
|
||||
def update_window_title(self, title: str):
|
||||
if len(title) > 0:
|
||||
self.main_window.setWindowTitle(_("{0} - KrowLog").format(title))
|
||||
else:
|
||||
self.main_window.setWindowTitle(_("KrowLog"))
|
||||
|
||||
def update_status_bar(self, text: str):
|
||||
if not self.main_window:
|
||||
return
|
||||
self.main_window.status_bar.showMessage(text)
|
||||
|
||||
def update_ui(self):
|
||||
self.main_window.update()
|
||||
|
||||
def add_tab(self, tab: Tab):
|
||||
self.main_window.tabs.add_tab(tab)
|
||||
|
||||
def add_dock(self, area: Qt.DockWidgetArea, widget: Tab):
|
||||
dock_widget = QDockWidget(widget.title, self.main_window)
|
||||
dock_widget.setWidget(widget)
|
||||
self.main_window.addDockWidget(area, dock_widget)
|
||||
|
||||
def _action_about(self) -> RAction:
|
||||
about_action = RAction(
|
||||
_("&About"),
|
||||
action=lambda: AboutDialog().exec(),
|
||||
icon_file=constants.krow_icon
|
||||
)
|
||||
return about_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',
|
||||
icon_from_theme=icon)
|
||||
return close_action
|
||||
0
src/plugins/logfile/__init__.py
Normal file
0
src/plugins/logfile/__init__.py
Normal file
21
src/plugins/logfile/filterviewsyncer.py
Normal file
21
src/plugins/logfile/filterviewsyncer.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
|
||||
|
||||
class FilterViewSyncer:
|
||||
|
||||
def __init__(self, sync_view: BigText):
|
||||
self._matches = {}
|
||||
self._sync_view = sync_view
|
||||
|
||||
def click_listener(self, byte_offset: int):
|
||||
source_byte_offset = self._matches[byte_offset] if byte_offset in self._matches else None
|
||||
# print("click %d -> %d (total hits %d)" % (byte_offset, source_byte_offset, len(self._matches)))
|
||||
if source_byte_offset is not None:
|
||||
self._sync_view.scroll_to_byte(source_byte_offset)
|
||||
|
||||
def match_found(self, match_byte_offset: int, source_byte_offset: int):
|
||||
# print("match %d" % match_byte_offset)
|
||||
if match_byte_offset >= 0:
|
||||
self._matches[match_byte_offset] = source_byte_offset
|
||||
else:
|
||||
self._matches = {}
|
||||
246
src/plugins/logfile/filterwidget.py
Normal file
246
src/plugins/logfile/filterwidget.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide6.QtCore import QRunnable, QThreadPool, Signal, QThread, QObject
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QPushButton, QLabel, QProgressBar
|
||||
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
from src.ui.bigtext.logFileModel import LogFileModel
|
||||
|
||||
from src.i18n import _
|
||||
from src.pluginregistry import PluginRegistry
|
||||
|
||||
log = logging.getLogger("filterwidget")
|
||||
|
||||
|
||||
class FilterTask(QThread):
|
||||
aborted = False
|
||||
|
||||
on_before = Signal()
|
||||
on_finish = Signal()
|
||||
filter_progress = Signal(float)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_model: LogFileModel,
|
||||
filter_model: LogFileModel,
|
||||
regex: re.Pattern,
|
||||
lock: threading.RLock,
|
||||
filter_match_found_listeners: Callable[[int], None]
|
||||
):
|
||||
super(FilterTask, self).__init__()
|
||||
self.source_model = source_model
|
||||
self.filter_model = filter_model
|
||||
self.regex = regex
|
||||
self.lock = lock
|
||||
self.filter_match_found_listeners = filter_match_found_listeners
|
||||
|
||||
def run(self):
|
||||
# print("writing to tmp file", self.filter_model.get_file())
|
||||
|
||||
# the lock ensures that we only start a new search when the previous search already ended
|
||||
with self.lock:
|
||||
print("starting thread ", threading.current_thread())
|
||||
self.on_before.emit()
|
||||
|
||||
if self.aborted:
|
||||
self.on_finish.emit()
|
||||
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(-1, -1) # notify listeners that a new search started
|
||||
|
||||
self.filter_progress.emit(0.0)
|
||||
|
||||
try:
|
||||
source_file = self.source_model.get_file()
|
||||
file_size = os.stat(source_file).st_size
|
||||
start = time.time()
|
||||
with open(source_file, "rb") as source:
|
||||
with open(self.filter_model.get_file(), "w+b") as target:
|
||||
line_count = 0
|
||||
lines_written = 0
|
||||
last_bytes_read = 0
|
||||
bytes_read = 0
|
||||
while l := source.readline():
|
||||
bytes_read = bytes_read + len(l)
|
||||
line_count = line_count + 1
|
||||
line = l.decode("utf8", errors="ignore")
|
||||
|
||||
if self.regex.findall(line):
|
||||
# time.sleep(0.5)
|
||||
lines_written = lines_written + 1
|
||||
source_line_offset = source.tell() - len(l)
|
||||
target_line_offset = target.tell()
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(target_line_offset, source_line_offset)
|
||||
target.write(l)
|
||||
|
||||
# sometime buffering can hide results for a while
|
||||
# We force a flush periodically.
|
||||
if line_count % 10000 == 0:
|
||||
now = time.time()
|
||||
time_diff = (now - start)
|
||||
if time_diff > 0.5:
|
||||
read_speed = ((bytes_read - last_bytes_read) / time_diff) / (1024 * 1024)
|
||||
# todo progress disabled because of its detrimental effect on UI responsibility
|
||||
# print("emit %f" % (bytes_read / file_size))
|
||||
self.filter_progress.emit(bytes_read / file_size)
|
||||
# self._progress_updater(bytes_read / file_size, read_speed)
|
||||
last_bytes_read = bytes_read
|
||||
start = time.time()
|
||||
|
||||
if lines_written > 0:
|
||||
target.flush()
|
||||
lines_written = 0
|
||||
|
||||
if self.aborted:
|
||||
print("aborted ", time.time())
|
||||
break
|
||||
finally:
|
||||
self.on_finish.emit()
|
||||
print("dome thread ", threading.current_thread())
|
||||
|
||||
|
||||
class FilterWidget(QWidget):
|
||||
filter_model: LogFileModel
|
||||
filter_task: Optional[FilterTask] = None
|
||||
|
||||
def __init__(self, source_model: LogFileModel):
|
||||
super(FilterWidget, self).__init__()
|
||||
self.source_model = source_model
|
||||
|
||||
self._lock = threading.RLock()
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.query_field = QLineEdit()
|
||||
self.query_field.textChanged.connect(self.filter_changed)
|
||||
|
||||
self._lbl_search_progress = QLabel("0%")
|
||||
self._lbl_search_progress.setVisible(False)
|
||||
|
||||
self._progress_bar = QProgressBar();
|
||||
self._progress_bar.setVisible(False)
|
||||
self._progress_bar.setMinimum(0)
|
||||
self._progress_bar.setMaximum(100)
|
||||
self._progress_bar.setMaximumWidth(50)
|
||||
|
||||
self.btn_cancel_search = QPushButton(_("Cancel"))
|
||||
self.btn_cancel_search.setVisible(False)
|
||||
self.btn_cancel_search.pressed.connect(self._cancel_search)
|
||||
|
||||
self.ignore_case = QCheckBox(_("ignore case"))
|
||||
self.ignore_case.setChecked(True)
|
||||
self.ignore_case.stateChanged.connect(self.filter_changed)
|
||||
|
||||
self.is_regex = QCheckBox(_("regex"))
|
||||
self.is_regex.setChecked(True)
|
||||
self.is_regex.stateChanged.connect(self.filter_changed)
|
||||
|
||||
filter_bar = QWidget()
|
||||
filter_bar.layout = QHBoxLayout(filter_bar)
|
||||
filter_bar.layout.setContentsMargins(0, 0, 0, 0)
|
||||
filter_bar.layout.addWidget(self.query_field)
|
||||
filter_bar.layout.addWidget(self._lbl_search_progress)
|
||||
filter_bar.layout.addWidget(self._progress_bar)
|
||||
filter_bar.layout.addWidget(self.btn_cancel_search)
|
||||
filter_bar.layout.addWidget(self.ignore_case)
|
||||
filter_bar.layout.addWidget(self.is_regex)
|
||||
|
||||
(handle, self.tmpfilename) = tempfile.mkstemp()
|
||||
os.close(handle)
|
||||
self.filter_model = LogFileModel(self.tmpfilename, self.source_model.settings)
|
||||
self.hits_view = BigText(self.filter_model)
|
||||
|
||||
self.layout.addWidget(filter_bar)
|
||||
self.layout.addWidget(self.hits_view)
|
||||
|
||||
self.filter_match_found_listeners: [Callable[[int], None]] = []
|
||||
|
||||
def add_line_click_listener(self, listener: Callable[[int], None]):
|
||||
self.hits_view.add_line_click_listener(listener)
|
||||
|
||||
def add_filter_match_found_listener(self, listener: Callable[[int], None]):
|
||||
self.filter_match_found_listeners.append(listener)
|
||||
|
||||
def destruct(self):
|
||||
# print("cleanup: ", self.tmpfilename)
|
||||
self._cancel_search()
|
||||
os.remove(self.tmpfilename)
|
||||
|
||||
def _cancel_search(self):
|
||||
if self.filter_task:
|
||||
# print("cancel started ", time.time())
|
||||
self.filter_task.aborted = True
|
||||
# wait until the previous search is aborted
|
||||
with self._lock:
|
||||
pass
|
||||
|
||||
def reset_filter(self):
|
||||
self.filter_model.truncate()
|
||||
self.source_model.clear_query_highlight()
|
||||
self.filter_model.clear_query_highlight()
|
||||
PluginRegistry.execute("update_ui")
|
||||
|
||||
def filter_changed(self):
|
||||
query = self.query_field.text()
|
||||
ignore_case = self.ignore_case.isChecked()
|
||||
is_regex = self.is_regex.isChecked()
|
||||
|
||||
# cancel previous search
|
||||
self._cancel_search()
|
||||
|
||||
if len(query) == 0:
|
||||
self.reset_filter()
|
||||
return
|
||||
|
||||
try:
|
||||
flags = re.IGNORECASE if ignore_case else 0
|
||||
if is_regex:
|
||||
regex = re.compile(query, flags=flags)
|
||||
else:
|
||||
regex = re.compile(re.escape(query), flags=flags)
|
||||
except:
|
||||
# query was not a valid regex -> clear search hits, then abort
|
||||
self.filter_model.truncate()
|
||||
return
|
||||
|
||||
self.source_model.set_query_highlight(query, ignore_case, is_regex)
|
||||
self.filter_model.set_query_highlight(query, ignore_case, is_regex)
|
||||
|
||||
self.filter_task = FilterTask(
|
||||
self.source_model,
|
||||
self.filter_model,
|
||||
regex,
|
||||
self._lock,
|
||||
self.filter_match_found_listeners,
|
||||
)
|
||||
self.filter_task.on_before.connect(self._on_before)
|
||||
self.filter_task.on_finish.connect(self._on_finish)
|
||||
self.filter_task.filter_progress.connect(self._update_progress)
|
||||
# self.filter_task.finished.connect(self.filter_task.deleteLater)
|
||||
# super().connect(self.filter_task, FilterTask.filter_progress, self, self._update_progress)
|
||||
# super().connect(self.filter_task, FilterTask.finished, self.filter_task, QObject.deleteLater)
|
||||
self.filter_task.start()
|
||||
# QThreadPool.globalInstance().start(self.filter_task)
|
||||
|
||||
def _on_before(self):
|
||||
print("on_before")
|
||||
self.btn_cancel_search.setVisible(True)
|
||||
self._progress_bar.setVisible(True)
|
||||
|
||||
def _on_finish(self):
|
||||
print("on_finish")
|
||||
self.btn_cancel_search.setVisible(False)
|
||||
self._progress_bar.setVisible(False)
|
||||
self.filter_task.deleteLater()
|
||||
|
||||
def _update_progress(self, progress: float):
|
||||
print("progress %f" % (progress))
|
||||
self._progress_bar.setValue(progress * 100)
|
||||
47
src/plugins/logfile/fulltabwidget.py
Normal file
47
src/plugins/logfile/fulltabwidget.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
|
||||
from src.ui.bigtext.bigtext import BigText
|
||||
from src.plugins.logfile.filterviewsyncer import FilterViewSyncer
|
||||
from src.plugins.logfile.filterwidget import FilterWidget
|
||||
from src.ui.bigtext.logFileModel import LogFileModel
|
||||
from src.plugins.krowlog.Tab import Tab
|
||||
from src.util.conversion import humanbytes
|
||||
|
||||
|
||||
class FullTabWidget(Tab):
|
||||
|
||||
def __init__(self, model: LogFileModel, unique_id: str, title: str):
|
||||
super(FullTabWidget, self).__init__(unique_id, title)
|
||||
self._model = model
|
||||
self.file_view = BigText(model)
|
||||
self.filter_hit_view = FilterWidget(self._model)
|
||||
self.filter_view_syncer = FilterViewSyncer(self.file_view)
|
||||
self.filter_hit_view.add_line_click_listener(self.filter_view_syncer.click_listener)
|
||||
self.filter_hit_view.add_filter_match_found_listener(self.filter_view_syncer.match_found)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
splitter = QSplitter()
|
||||
splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
splitter.setHandleWidth(5)
|
||||
# splitter.setStyleSheet("QSplitter::handle{background: #cccccc;}")
|
||||
splitter.addWidget(self.file_view)
|
||||
splitter.addWidget(self.filter_hit_view)
|
||||
|
||||
self.layout.addWidget(splitter)
|
||||
|
||||
def get_file(self) -> str:
|
||||
return self.file_view.get_file()
|
||||
|
||||
# overriding abstract method
|
||||
def destruct(self):
|
||||
self.file_view.destruct()
|
||||
self.filter_hit_view.destruct()
|
||||
|
||||
# overriding abstract method
|
||||
def get_status_text(self) -> str:
|
||||
file = self._model.get_file()
|
||||
file_size = humanbytes(self._model.byte_count())
|
||||
return "%s - %s" % (file_size, file)
|
||||
36
src/plugins/logfileplugin.py
Normal file
36
src/plugins/logfileplugin.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import os.path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
from src.plugins.logfile.fulltabwidget import FullTabWidget
|
||||
from src.ui.bigtext.logFileModel import LogFileModel
|
||||
from src.pluginbase import PluginBase
|
||||
from src.plugins.krowlog.Tab import Tab
|
||||
from src.settings.settings import Settings
|
||||
|
||||
from src.i18n import _
|
||||
|
||||
|
||||
class LogFilePlugin(PluginBase):
|
||||
def __init__(self):
|
||||
super(LogFilePlugin, self).__init__()
|
||||
self.settings = None
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
|
||||
def create_tab(self, file: str) -> Optional[Tab]:
|
||||
if not os.path.isfile(file):
|
||||
message = QMessageBox(QMessageBox.Icon.Warning, _("File not found"),
|
||||
_("'{0}' is not a file or cannot be opened").format(file))
|
||||
message.exec()
|
||||
return None
|
||||
|
||||
realpath = os.path.realpath(file)
|
||||
filename = os.path.basename(realpath)
|
||||
|
||||
model = LogFileModel(file, self.settings)
|
||||
tab = FullTabWidget(model, unique_id=realpath, title=filename)
|
||||
|
||||
return tab
|
||||
0
src/plugins/notes/__init__.py
Normal file
0
src/plugins/notes/__init__.py
Normal file
14
src/plugins/notes/noteswidget.py
Normal file
14
src/plugins/notes/noteswidget.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from src.plugins.krowlog.Tab import Tab
|
||||
from PySide6.QtWidgets import QTextEdit, QVBoxLayout
|
||||
|
||||
|
||||
class NotesWidget(Tab):
|
||||
|
||||
def __init__(self, unique_id: str, title: str):
|
||||
super(NotesWidget, self).__init__(unique_id, title)
|
||||
|
||||
self.text_area = QTextEdit(self)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.text_area)
|
||||
35
src/plugins/notesplugin.py
Normal file
35
src/plugins/notesplugin.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Callable
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from src.pluginbase import PluginBase
|
||||
from src.pluginregistry import PluginRegistry
|
||||
from src.plugins.domain.menucontribution import MenuContribution
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.notes.noteswidget import NotesWidget
|
||||
from src.i18n import _
|
||||
|
||||
|
||||
class NotesPlugin(PluginBase):
|
||||
|
||||
def __init__(self):
|
||||
super(NotesPlugin, self).__init__()
|
||||
self.settings = None
|
||||
self.tab_counter = 0
|
||||
|
||||
def get_menu_contributions(self) -> [MenuContribution]:
|
||||
return [
|
||||
MenuContribution("window", action=self._add_notes_tab_action(), action_id="add notes tab", after="<last>"),
|
||||
]
|
||||
|
||||
def _add_notes_tab_action(self) -> RAction:
|
||||
open_file = RAction(_("Add &Notes"), self._add_notes_tab, shortcut='Ctrl+Shift+N',
|
||||
icon_from_theme="filenew")
|
||||
return open_file
|
||||
|
||||
def _add_notes_tab(self):
|
||||
self.tab_counter = self.tab_counter + 1
|
||||
notes = NotesWidget(
|
||||
"notes_tab_%d" % self.tab_counter,
|
||||
_("Notes {0}").format(self.tab_counter))
|
||||
PluginRegistry.execute_single("add_dock", Qt.DockWidgetArea.RightDockWidgetArea, notes)
|
||||
99
src/plugins/openfileplugin.py
Normal file
99
src/plugins/openfileplugin.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from src.pluginbase import PluginBase
|
||||
from src.pluginregistry import PluginRegistry
|
||||
from src.plugins.domain.menucontribution import MenuContribution
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.domain.rmenu import RMenu
|
||||
from src.settings.settings import Settings
|
||||
|
||||
from src.i18n import _
|
||||
|
||||
|
||||
class OpenFilePlugin(PluginBase):
|
||||
def __init__(self):
|
||||
super(OpenFilePlugin, self).__init__()
|
||||
self.settings = None
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
|
||||
def _action_open_file(self) -> RAction:
|
||||
open_file = RAction(_("&Open..."), self._open_file_dialog, shortcut='Ctrl+O',
|
||||
icon_from_theme="document-open")
|
||||
return open_file
|
||||
|
||||
def _sub_menu_recent_files(self) -> RMenu:
|
||||
self._menu_recent_files = RMenu(_("Open &Recent"), icon_from_theme="document-open-recent")
|
||||
self._update_recent_files_menu()
|
||||
return self._menu_recent_files
|
||||
|
||||
def get_menu_contributions(self) -> [MenuContribution]:
|
||||
return [
|
||||
MenuContribution("file", action=self._action_open_file(), action_id="open file"),
|
||||
MenuContribution("file", menu=self._sub_menu_recent_files(), action_id="recent files menu"),
|
||||
]
|
||||
|
||||
def _open_file_dialog(self) -> None:
|
||||
current_file = PluginRegistry.execute_single("current_file")
|
||||
directory = os.path.dirname(current_file) if current_file else os.path.join(Path.home())
|
||||
|
||||
dialog = QFileDialog()
|
||||
(selected_file, _filter) = dialog.getOpenFileName(
|
||||
caption=_("Open File"),
|
||||
dir=directory
|
||||
)
|
||||
# directory=directory
|
||||
if selected_file:
|
||||
self.open_file(selected_file)
|
||||
|
||||
def open_file(self, selected_file: str):
|
||||
tab = PluginRegistry.execute_single("create_tab", selected_file)
|
||||
if tab:
|
||||
PluginRegistry.execute_single("add_tab", tab)
|
||||
PluginRegistry.execute("after_open_file", selected_file)
|
||||
|
||||
def _get_recent_files(self) -> [str]:
|
||||
recent_files = self.settings.session.get('general', 'recent_files', fallback='')
|
||||
# print(recent_files)
|
||||
files = recent_files.split(os.pathsep)
|
||||
if "" in files:
|
||||
files.remove("")
|
||||
return files
|
||||
|
||||
def _update_recent_files_menu(self):
|
||||
self._menu_recent_files.clear()
|
||||
files = self._get_recent_files()
|
||||
for file in files:
|
||||
action = RAction(os.path.basename(file))
|
||||
action.set_action(lambda _="", f=file: self.open_file(f))
|
||||
self._menu_recent_files.add_action(action)
|
||||
|
||||
def _remember_recent_file(self, file: str):
|
||||
files = self._get_recent_files()
|
||||
if file in files:
|
||||
files.remove(file)
|
||||
files.insert(0, file)
|
||||
recent_files = os.pathsep.join(files[:10])
|
||||
self.settings.set_session('general', 'recent_files', recent_files)
|
||||
self._update_recent_files_menu()
|
||||
|
||||
def after_open_file(self, file: str):
|
||||
self._remember_recent_file(file)
|
||||
|
||||
def after_start(self):
|
||||
open_files_as_string = self.settings.get_session('general', 'open_files', fallback='')
|
||||
files = open_files_as_string.split(os.pathsep)
|
||||
if "" in files:
|
||||
files.remove("")
|
||||
for file in files:
|
||||
self.open_file(file)
|
||||
|
||||
def before_shutdown(self):
|
||||
open_files = PluginRegistry.execute_single("get_open_files")
|
||||
if open_files:
|
||||
open_files_as_string = os.pathsep.join(open_files)
|
||||
self.settings.set_session('general', 'open_files', open_files_as_string)
|
||||
Reference in New Issue
Block a user