diff --git a/bigtext.py b/bigtext.py index 0c7d5fa..5c75fa9 100644 --- a/bigtext.py +++ b/bigtext.py @@ -228,15 +228,19 @@ class InnerBigText(QWidget): if self.selection_highlight.start_byte != self.selection_highlight.end_byte: start = min(self.selection_highlight.start_byte, self.selection_highlight.end_byte) end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte) + bytes_human_readable = humanbytes(end - start) if end - start > (1024 ** 2) * 5: you_sure = QMessageBox( QMessageBox.Icon.Warning, - self.tr("copy to clipboard"), + self.tr("data selection"), self.tr( - "You are about to copy %s to the clipboard. Are you sure you want to do that?" % humanbytes( - end - start))) - you_sure.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - you_sure.setDefaultButton(QMessageBox.StandardButton.No) + "You have selected {0} of data.").format(bytes_human_readable)) + you_sure.setStandardButtons(QMessageBox.StandardButton.Cancel) + you_sure.addButton(QPushButton(self.tr("Copy {0} to Clipboard").format(bytes_human_readable)), + QMessageBox.ButtonRole.AcceptRole) + # TODO add save dialog + # you_sure.addButton(QPushButton(self.tr("Write to File")), QMessageBox.ButtonRole.YesRole) + you_sure.setDefaultButton(QMessageBox.StandardButton.Cancel) result = you_sure.exec() if result != QMessageBox.StandardButton.Yes: # abort @@ -267,7 +271,7 @@ class InnerBigText(QWidget): for l in self.lines: self.update_longest_line(len(l.line())) - highlighters = self.model.highlights + highlighters = self.model.highlighters() if self.model.get_query_highlight(): highlighters = highlighters + [self.model.get_query_highlight()] highlighters = highlighters + [self.selection_highlight] # selection highlight should be last diff --git a/colorbutton.py b/colorbutton.py new file mode 100644 index 0000000..4b66692 --- /dev/null +++ b/colorbutton.py @@ -0,0 +1,47 @@ +import re + +from PyQt6.QtGui import QColor +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QColorDialog, QSizePolicy + + +class ColorButton(QPushButton): + def __init__(self, color: str): + super(QPushButton, self).__init__() + self.color = color + self.setStyleSheet("background-color: #%s" % color) + self.pressed.connect(self._update_color) + self.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)) + + def set_color(self, color: str): + if self._is_hex_color(color): + self.setStyleSheet("background-color: #%s" % color) + self.setText(color) + self.color = color + else: + self.setStyleSheet("background-color: none") + self.setText(self.tr("not set")) + self.color = "None" + + def _update_color(self): + new_color = QColorDialog.getColor(self._to_qcolor(self.color)) + if new_color.isValid(): + color = self._to_hex(new_color) + self.set_color(color) + + @staticmethod + def _is_hex_color(color: str): + return re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE) + + def _to_qcolor(self, color: str): + if self._is_hex_color(color): + red = int(color[0:2], 16) + green = int(color[2:4], 16) + blue = int(color[4:6], 16) + return QColor(red, green, blue) + return QColor(255, 255, 255) + + def _to_hex(self, color: QColor) -> str: + red = "{0:0{1}x}".format(color.red(), 2) + green = "{0:0{1}x}".format(color.green(), 2) + blue = "{0:0{1}x}".format(color.blue(), 2) + return red + green + blue diff --git a/filterwidget.py b/filterwidget.py index e3051fa..ff30d57 100644 --- a/filterwidget.py +++ b/filterwidget.py @@ -129,6 +129,7 @@ class FilterWidget(QWidget): def filter_changed(self): query = self.query_field.text() ignore_case = self.ignore_case.isChecked() + is_regex = self.is_regex.isChecked() if len(query) == 0: self.reset_filter() return @@ -138,7 +139,7 @@ class FilterWidget(QWidget): try: flags = re.IGNORECASE if ignore_case else 0 - if self.is_regex.isChecked(): + if is_regex: regex = re.compile(query, flags=flags) else: regex = re.compile(re.escape(query), flags=flags) @@ -147,8 +148,8 @@ class FilterWidget(QWidget): self.filter_model.truncate() return - self.source_model.set_query_highlight(regex) - self.filter_model.set_query_highlight(regex) + 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, diff --git a/hbox.py b/hbox.py new file mode 100644 index 0000000..17e91e6 --- /dev/null +++ b/hbox.py @@ -0,0 +1,9 @@ +from PyQt6.QtWidgets import QWidget, QHBoxLayout + + +class HBox(QWidget): + def __init__(self, *widgets: QWidget): + super(HBox, self).__init__() + self.layout = QHBoxLayout(self) + for widget in widgets: + self.layout.addWidget(widget) diff --git a/highlight_regex.py b/highlight_regex.py index cc2e4dd..a609cce 100644 --- a/highlight_regex.py +++ b/highlight_regex.py @@ -14,28 +14,48 @@ import re class HighlightRegex(Highlight): - def __init__(self, regex: re.Pattern, brush: QBrush = QBrush(), pen: QPen = Qt.PenStyle.NoPen, - brush_full_line=QBrush()): - self.regex = regex - self.brush = brush - self.pen = pen - self.brush_full_line = brush_full_line + def __init__(self, query: str, ignore_case: bool, is_regex: bool, hit_background_color: str = "None", + line_background_color: str = "None"): + self.query = query + self.ignore_case = ignore_case + self.is_regex = is_regex + self.regex = self._get_regex() + self.hit_background_color = hit_background_color + self.line_background_color = line_background_color + + def _get_regex(self): + flags = re.IGNORECASE if self.ignore_case else 0 + if self.is_regex: + return re.compile(self.query, flags=flags) + else: + return re.compile(re.escape(self.query), flags=flags) def compute_highlight(self, line: Line) -> Optional[List[HighlightedRange]]: result = [] - #print("execute regex: %s in %s" % (self.regex, line.line())) + # print("execute regex: %s in %s" % (self.regex, line.line())) match_iter = re.finditer(self.regex, line.line()) for match in match_iter: - #print("%s" % match) + # print("%s" % match) start = match.start(0) end = match.end(0) result.append(HighlightedRange( start, - end-start, + end - start, highlight_full_line=True, - brush=self.brush, - pen=self.pen, - brush_full_line=self.brush_full_line + brush=self.brush(self.hit_background_color), + brush_full_line=self.brush(self.line_background_color) )) - return result \ No newline at end of file + return result + + def hit_background_brush(self): + return self.brush(self.hit_background_color) + + @staticmethod + def brush(color: str) -> QBrush: + if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE): + red = int(color[0:2], 16) + green = int(color[2:4], 16) + blue = int(color[4:6], 16) + return QBrush(QColor(red, green, blue)) + return QBrush() diff --git a/highlighting.py b/highlighting.py index 02ad35c..2e2e9f3 100644 --- a/highlighting.py +++ b/highlighting.py @@ -1,5 +1,6 @@ import logging import re +import uuid from PyQt6.QtGui import QBrush, QColor @@ -9,11 +10,10 @@ from settings import Settings log = logging.getLogger("highlighting") - class Highlighting: @staticmethod - def read_config(settings: Settings) -> [Highlight]: + def read_config(settings: Settings) -> [HighlightRegex]: result = [] config = settings.config @@ -26,32 +26,42 @@ class Highlighting: continue ignore_case = config.getboolean(section, "ignore-case", fallback=True) is_regex = config.getboolean(section, "is-regex", fallback=False) - line_background_color = Highlighting.brush(config.get(section, "line.background.color", fallback="None")) - hit_background_color = Highlighting.brush(config.get(section, "hit.background.color", fallback="None")) + line_background_color = config.get(section, "line.background.color", fallback="None") + hit_background_color = config.get(section, "hit.background.color", fallback="None") + 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) + highlight = HighlightRegex( + query=query, + ignore_case=ignore_case, + is_regex=is_regex, + hit_background_color=hit_background_color, + line_background_color=line_background_color + ) + result.append(highlight) except: log.exception("failed to parse query for highlighter: %s" % section) continue - highlight = HighlightRegex( - regex=regex, - brush=hit_background_color, - brush_full_line=line_background_color - ) - result.append(highlight) - return result @staticmethod - def brush(color: str) -> QBrush: - if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE): - red = int(color[0:2], 16) - green = int(color[2:4], 16) - blue = int(color[4:6], 16) - return QBrush(QColor(red, green, blue)) - return QBrush() + def write_config(settings: Settings, highlighters: [HighlightRegex]): + Highlighting.remove_highlighting_sections(settings) + section_counter = 0 + for highlighter in highlighters: + highlighter: HighlightRegex = highlighter + section = "highlighting.%d" % section_counter + section_counter = section_counter + 1 + settings.config.add_section(section) + settings.config.set(section, "query", highlighter.query) + settings.config.set(section, "ignore-case", str(highlighter.ignore_case)) + settings.config.set(section, "is-regex", str(highlighter.is_regex)) + settings.config.set(section, "line.background.color", highlighter.line_background_color) + settings.config.set(section, "hit.background.color", highlighter.hit_background_color) + + @staticmethod + def remove_highlighting_sections(settings: Settings): + for section in settings.config.sections(): + if not section.startswith("highlighting."): + continue + settings.config.remove_section(section) diff --git a/highlightingdialog.py b/highlightingdialog.py new file mode 100644 index 0000000..eb79afa --- /dev/null +++ b/highlightingdialog.py @@ -0,0 +1,170 @@ +import re + +from PyQt6.QtWidgets import QDialog, QLineEdit, QLabel, QGridLayout, QCheckBox, QListWidget, QListWidgetItem, \ + QPushButton, QDialogButtonBox, QMessageBox + +from colorbutton import ColorButton +from hbox import HBox +from highlight_regex import HighlightRegex +from highlighting import Highlighting +from settings import Settings + + +class PayloadItem(QListWidgetItem): + def __init__(self, text: str, payload=None): + super(PayloadItem, self).__init__(text) + self.payload = payload + + +class HighlightingDialog(QDialog): + def __init__(self, settings: Settings): + super(HighlightingDialog, self).__init__() + self.setWindowTitle(self.tr("Manage Highlighting")) + self.setModal(True) + self._settings = settings + + form_grid = QGridLayout(self) + self.layout = form_grid + + row = 0 + self.list = QListWidget(self) + form_grid.addWidget(self.list, row, 0, 1, 2) + + row = row + 1 + self.btn_add = QPushButton(self.tr("Add")) + self.btn_add.pressed.connect(self._add) + self.btn_update = QPushButton(self.tr("Update")) + self.btn_update.pressed.connect(self._update) + self.btn_delete = QPushButton(self.tr("Delete")) + self.btn_delete.pressed.connect(self._delete) + self.btn_move_up = QPushButton(self.tr("Move Up")) + self.btn_move_up.pressed.connect(self._move_up) + self.btn_move_down = QPushButton(self.tr("Move Down")) + self.btn_move_down.pressed.connect(self._move_down) + form_grid.addWidget(HBox(self.btn_add, self.btn_update, self.btn_delete, self.btn_move_up, self.btn_move_down), + row, 0, 1, 2) + + row = row + 1 + self.query = QLineEdit(self) + form_grid.addWidget(QLabel(self.tr("Query:")), row, 0) + form_grid.addWidget(self.query, row, 1) + + row = row + 1 + self.ignore_case = QCheckBox(self.tr("Ignore Case")) + self.ignore_case.setChecked(True) + form_grid.addWidget(self.ignore_case, row, 0, 1, 2) + + row = row + 1 + self.is_regex = QCheckBox(self.tr("Regular Expression")) + self.is_regex.setChecked(True) + form_grid.addWidget(self.is_regex, row, 0, 1, 2) + + row = row + 1 + form_grid.addWidget(QLabel(self.tr("Hit Background:")), row, 0) + self.hit_background_color = ColorButton("ff00ff") + form_grid.addWidget(self.hit_background_color, row, 1) + + row = row + 1 + form_grid.addWidget(QLabel(self.tr("Line Background:")), row, 0) + self.line_background_color = ColorButton("ff00ff0d") + form_grid.addWidget(self.line_background_color, row, 1) + + row = row + 1 + self.buttons = QDialogButtonBox() + self.buttons.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Save) + self.buttons.accepted.connect(self._save) + self.buttons.rejected.connect(self.close) + form_grid.addWidget(self.buttons, row, 0, 1, 2) + + self.list.currentItemChanged.connect(self._item_changed) + self.list.itemSelectionChanged.connect(self._selection_changed) + self._load_existing_hightlighters() + + def _add(self): + highlighter = HighlightRegex( + self.query.text(), + self.ignore_case.isChecked(), + self.is_regex.isChecked(), + self.hit_background_color.color, + self.line_background_color.color + ) + item = PayloadItem(self.query.text(), highlighter) + item.setBackground(highlighter.hit_background_brush()) + self.list.addItem(item) + self.list.setCurrentItem(item) + + def _update(self): + item: PayloadItem = self.list.currentItem() + highlighter: HighlightRegex = item.payload + + highlighter.query = self.query.text() + highlighter.ignore_case = self.ignore_case.isChecked() + highlighter.is_regex = self.is_regex.isChecked() + highlighter.hit_background_color = self.hit_background_color.color + highlighter.line_background_color = self.line_background_color.color + + item.setText(self.query.text()) + item.setBackground(highlighter.hit_background_brush()) + + def _delete(self): + index = self.list.selectedIndexes()[0] + selected_index = index.row() + self.list.takeItem(selected_index) + + def _move_up(self): + index = self.list.currentIndex() + selected_index = index.row() + item = self.list.takeItem(selected_index) + self.list.insertItem(selected_index - 1, item) + self.list.setCurrentIndex(index.siblingAtRow(selected_index - 1)) + + def _move_down(self): + index = self.list.selectedIndexes()[0] + selected_index = index.row() + item = self.list.takeItem(selected_index) + self.list.insertItem(selected_index + 1, item) + self.list.setCurrentIndex(index.sibling(selected_index + 1, 0)) + + def _save(self): + highlighters = [] + for index in range(0, self.list.count()): + item: PayloadItem = self.list.item(index) + highlighters.append(item.payload) + Highlighting.write_config(self._settings, highlighters) + self.close() + + def _selection_changed(self): + if len(self.list.selectedIndexes()) == 0: + self.btn_update.setDisabled(True) + self.btn_delete.setDisabled(True) + self.btn_move_up.setDisabled(True) + self.btn_move_down.setDisabled(True) + if len(self.list.selectedIndexes()) == 1: + selected_index = self.list.selectedIndexes()[0].row() + self.btn_update.setDisabled(False) + self.btn_delete.setDisabled(False) + self.btn_move_up.setDisabled(selected_index == 0) + self.btn_move_down.setDisabled(selected_index + 1 >= self.list.count()) + + def _item_changed(self, current, _previous): + item: PayloadItem = current + if item: + highlighter: HighlightRegex = item.payload + self.query.setText(highlighter.query) + self.ignore_case.setChecked(highlighter.ignore_case) + self.is_regex.setChecked(highlighter.is_regex) + self.hit_background_color.set_color(highlighter.hit_background_color) + self.line_background_color.set_color(highlighter.line_background_color) + + def _load_existing_hightlighters(self): + highlighters: [HighlightRegex] = Highlighting.read_config(self._settings) + first_item = None + for highlighter in highlighters: + item = PayloadItem(str(highlighter.query)) + item.payload = highlighter + item.setBackground(highlighter.hit_background_brush()) + self.list.addItem(item) + if not first_item: + first_item = item + if first_item: + self.list.setCurrentItem(first_item) diff --git a/logFileModel.py b/logFileModel.py index f69b424..b195605 100644 --- a/logFileModel.py +++ b/logFileModel.py @@ -21,15 +21,8 @@ class LogFileModel: self._file = os.path.realpath(file) self._lock = threading.RLock() - self.highlights = Highlighting.read_config(settings) - # [ - # HighlightRegex( - # re.compile("ERROR"), - # brush=QBrush(QColor(220, 112, 122)), - # pen=QPen(QColor(0, 0, 0)), - # brush_full_line=QBrush(QColor(255, 112, 122)) - # ) - # ] + def highlighters(self): + return Highlighting.read_config(self.settings) def get_file(self): return self._file @@ -40,14 +33,15 @@ class LogFileModel: def get_query_highlight(self): return self._query_highlight - def set_query_highlight(self, regex: Optional[re.Pattern] = None): - if regex: - self._query_highlight = HighlightRegex( - regex, - brush=QBrush(QColor(255, 255, 0)) - ) - else: - self._query_highlight = None + def clear_query_highlight(self): + self._query_highlight = None + + def set_query_highlight(self, query: str, ignore_case: bool, is_regex: bool): + self._query_highlight = HighlightRegex( + query=query, + ignore_case=ignore_case, + is_regex=is_regex, + hit_background_color="ffff00") def get_tab_name(self): file_name = os.path.basename(self._file) diff --git a/main.py b/main.py index 691d397..9f469bd 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ import urlutils from aboutdialog import AboutDialog from ravenui import RavenUI from settingsstore import SettingsStore +from highlightingdialog import HighlightingDialog from tabs import Tabs from urlutils import url_is_file @@ -64,6 +65,13 @@ class MainWindow(QMainWindow): file_menu.addAction(close_action) return file_menu + def highlight_menu(self) -> QMenu: + result = QMenu(self.tr("&Highlighting"), self) + manage = QAction(self.tr("&Manage"), self) + manage.triggered.connect(lambda: HighlightingDialog(self.settings).exec()) + result.addAction(manage) + return result + def help_menu(self) -> QMenu: help_menu = QMenu(self.tr("&Help", "name of the help menu"), self)