add dialog to manage highlighters

- no support for "no color"
- settings not saved to disk
This commit is contained in:
2021-10-30 16:25:34 +02:00
parent 43e85d2863
commit aee0ff9968
9 changed files with 325 additions and 62 deletions

View File

@@ -228,15 +228,19 @@ class InnerBigText(QWidget):
if self.selection_highlight.start_byte != self.selection_highlight.end_byte: if self.selection_highlight.start_byte != self.selection_highlight.end_byte:
start = min(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) end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
bytes_human_readable = humanbytes(end - start)
if end - start > (1024 ** 2) * 5: if end - start > (1024 ** 2) * 5:
you_sure = QMessageBox( you_sure = QMessageBox(
QMessageBox.Icon.Warning, QMessageBox.Icon.Warning,
self.tr("copy to clipboard"), self.tr("data selection"),
self.tr( self.tr(
"You are about to copy <b>%s</b> to the clipboard. Are you sure you want to do that?" % humanbytes( "You have selected <b>{0}</b> of data.").format(bytes_human_readable))
end - start))) you_sure.setStandardButtons(QMessageBox.StandardButton.Cancel)
you_sure.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) you_sure.addButton(QPushButton(self.tr("Copy {0} to Clipboard").format(bytes_human_readable)),
you_sure.setDefaultButton(QMessageBox.StandardButton.No) 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() result = you_sure.exec()
if result != QMessageBox.StandardButton.Yes: if result != QMessageBox.StandardButton.Yes:
# abort # abort
@@ -267,7 +271,7 @@ class InnerBigText(QWidget):
for l in self.lines: for l in self.lines:
self.update_longest_line(len(l.line())) self.update_longest_line(len(l.line()))
highlighters = self.model.highlights highlighters = self.model.highlighters()
if self.model.get_query_highlight(): if self.model.get_query_highlight():
highlighters = highlighters + [self.model.get_query_highlight()] highlighters = highlighters + [self.model.get_query_highlight()]
highlighters = highlighters + [self.selection_highlight] # selection highlight should be last highlighters = highlighters + [self.selection_highlight] # selection highlight should be last

47
colorbutton.py Normal file
View File

@@ -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

View File

@@ -129,6 +129,7 @@ class FilterWidget(QWidget):
def filter_changed(self): def filter_changed(self):
query = self.query_field.text() query = self.query_field.text()
ignore_case = self.ignore_case.isChecked() ignore_case = self.ignore_case.isChecked()
is_regex = self.is_regex.isChecked()
if len(query) == 0: if len(query) == 0:
self.reset_filter() self.reset_filter()
return return
@@ -138,7 +139,7 @@ class FilterWidget(QWidget):
try: try:
flags = re.IGNORECASE if ignore_case else 0 flags = re.IGNORECASE if ignore_case else 0
if self.is_regex.isChecked(): if is_regex:
regex = re.compile(query, flags=flags) regex = re.compile(query, flags=flags)
else: else:
regex = re.compile(re.escape(query), flags=flags) regex = re.compile(re.escape(query), flags=flags)
@@ -147,8 +148,8 @@ class FilterWidget(QWidget):
self.filter_model.truncate() self.filter_model.truncate()
return return
self.source_model.set_query_highlight(regex) self.source_model.set_query_highlight(query, ignore_case, is_regex)
self.filter_model.set_query_highlight(regex) self.filter_model.set_query_highlight(query, ignore_case, is_regex)
self.filter_task = FilterTask( self.filter_task = FilterTask(
self.source_model, self.source_model,

9
hbox.py Normal file
View File

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

View File

@@ -14,28 +14,48 @@ import re
class HighlightRegex(Highlight): class HighlightRegex(Highlight):
def __init__(self, regex: re.Pattern, brush: QBrush = QBrush(), pen: QPen = Qt.PenStyle.NoPen, def __init__(self, query: str, ignore_case: bool, is_regex: bool, hit_background_color: str = "None",
brush_full_line=QBrush()): line_background_color: str = "None"):
self.regex = regex self.query = query
self.brush = brush self.ignore_case = ignore_case
self.pen = pen self.is_regex = is_regex
self.brush_full_line = brush_full_line 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]]: def compute_highlight(self, line: Line) -> Optional[List[HighlightedRange]]:
result = [] 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()) match_iter = re.finditer(self.regex, line.line())
for match in match_iter: for match in match_iter:
#print("%s" % match) # print("%s" % match)
start = match.start(0) start = match.start(0)
end = match.end(0) end = match.end(0)
result.append(HighlightedRange( result.append(HighlightedRange(
start, start,
end-start, end - start,
highlight_full_line=True, highlight_full_line=True,
brush=self.brush, brush=self.brush(self.hit_background_color),
pen=self.pen, brush_full_line=self.brush(self.line_background_color)
brush_full_line=self.brush_full_line
)) ))
return result 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()

View File

@@ -1,5 +1,6 @@
import logging import logging
import re import re
import uuid
from PyQt6.QtGui import QBrush, QColor from PyQt6.QtGui import QBrush, QColor
@@ -9,11 +10,10 @@ from settings import Settings
log = logging.getLogger("highlighting") log = logging.getLogger("highlighting")
class Highlighting: class Highlighting:
@staticmethod @staticmethod
def read_config(settings: Settings) -> [Highlight]: def read_config(settings: Settings) -> [HighlightRegex]:
result = [] result = []
config = settings.config config = settings.config
@@ -26,32 +26,42 @@ class Highlighting:
continue continue
ignore_case = config.getboolean(section, "ignore-case", fallback=True) ignore_case = config.getboolean(section, "ignore-case", fallback=True)
is_regex = config.getboolean(section, "is-regex", fallback=False) is_regex = config.getboolean(section, "is-regex", fallback=False)
line_background_color = Highlighting.brush(config.get(section, "line.background.color", fallback="None")) line_background_color = config.get(section, "line.background.color", fallback="None")
hit_background_color = Highlighting.brush(config.get(section, "hit.background.color", fallback="None")) hit_background_color = config.get(section, "hit.background.color", fallback="None")
try: try:
flags = re.IGNORECASE if ignore_case else 0 highlight = HighlightRegex(
if is_regex: query=query,
regex = re.compile(query, flags=flags) ignore_case=ignore_case,
else: is_regex=is_regex,
regex = re.compile(re.escape(query), flags=flags) hit_background_color=hit_background_color,
line_background_color=line_background_color
)
result.append(highlight)
except: except:
log.exception("failed to parse query for highlighter: %s" % section) log.exception("failed to parse query for highlighter: %s" % section)
continue continue
highlight = HighlightRegex(
regex=regex,
brush=hit_background_color,
brush_full_line=line_background_color
)
result.append(highlight)
return result return result
@staticmethod @staticmethod
def brush(color: str) -> QBrush: def write_config(settings: Settings, highlighters: [HighlightRegex]):
if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE): Highlighting.remove_highlighting_sections(settings)
red = int(color[0:2], 16) section_counter = 0
green = int(color[2:4], 16) for highlighter in highlighters:
blue = int(color[4:6], 16) highlighter: HighlightRegex = highlighter
return QBrush(QColor(red, green, blue)) section = "highlighting.%d" % section_counter
return QBrush() 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)

170
highlightingdialog.py Normal file
View File

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

View File

@@ -21,15 +21,8 @@ class LogFileModel:
self._file = os.path.realpath(file) self._file = os.path.realpath(file)
self._lock = threading.RLock() self._lock = threading.RLock()
self.highlights = Highlighting.read_config(settings) def highlighters(self):
# [ return Highlighting.read_config(self.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 get_file(self): def get_file(self):
return self._file return self._file
@@ -40,15 +33,16 @@ class LogFileModel:
def get_query_highlight(self): def get_query_highlight(self):
return self._query_highlight return self._query_highlight
def set_query_highlight(self, regex: Optional[re.Pattern] = None): def clear_query_highlight(self):
if regex:
self._query_highlight = HighlightRegex(
regex,
brush=QBrush(QColor(255, 255, 0))
)
else:
self._query_highlight = None 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): def get_tab_name(self):
file_name = os.path.basename(self._file) file_name = os.path.basename(self._file)
if len(file_name) > 35: if len(file_name) > 35:

View File

@@ -12,6 +12,7 @@ import urlutils
from aboutdialog import AboutDialog from aboutdialog import AboutDialog
from ravenui import RavenUI from ravenui import RavenUI
from settingsstore import SettingsStore from settingsstore import SettingsStore
from highlightingdialog import HighlightingDialog
from tabs import Tabs from tabs import Tabs
from urlutils import url_is_file from urlutils import url_is_file
@@ -64,6 +65,13 @@ class MainWindow(QMainWindow):
file_menu.addAction(close_action) file_menu.addAction(close_action)
return file_menu 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: def help_menu(self) -> QMenu:
help_menu = QMenu(self.tr("&Help", "name of the help menu"), self) help_menu = QMenu(self.tr("&Help", "name of the help menu"), self)