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:
2022-02-12 10:22:47 +01:00
parent 38e14d6042
commit a640b35c87
62 changed files with 380 additions and 362 deletions

25
src/plugins/__init__.py Normal file
View 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))

View File

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

View 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

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

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

View 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

View File

View 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

View 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

View File

View 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 = {}

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

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

View 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

View File

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

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

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