diff --git a/README.md b/README.md index b204b20..3b01953 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ This will create "log namespaces" which allow you to filter out messages from va ## Planned features * [ ] Presets for colors -* [ ] Presets for the logger header (with option to add columns for extra data) * [ ] Modify how rows are arranged in the detail table (like the header dialog) * [ ] Fix double-search on the last matched result (or indicate that the last result was reached) * [ ] Ability to save and load logs (as text or as full records) diff --git a/cutelog/config.py b/cutelog/config.py index 12658bf..5d2e223 100644 --- a/cutelog/config.py +++ b/cutelog/config.py @@ -37,6 +37,7 @@ class Exc_Indication(enum.IntEnum): # There must be a better way to do this, right? Option = namedtuple('Option', ['name', 'type', 'default']) OPTION_SPEC = ( + # SETTINGS WINDOW: # Appearance ('dark_theme_default', bool, False), ('logger_table_font', str, DEFAULT_FONT), @@ -63,6 +64,10 @@ class Exc_Indication(enum.IntEnum): ('benchmark', bool, False), ('benchmark_interval', float, 0.0005), ('light_theme_is_native', bool, False), + + # NON-SETTINGS OPTIONS: + # Header + ('default_header_preset', str, "Default"), ) @@ -115,6 +120,9 @@ def __setitem__(self, name, value): def set_option(self, name, value): self[name] = value + self.qsettings.beginGroup('Configuration') + self.qsettings.setValue(name, value) + self.qsettings.endGroup() @staticmethod def get_resource_path(name, directory='ui'): @@ -183,13 +191,14 @@ def update_attributes(self, options=None): self.logger_table_font_size = options.get('logger_table_font_size', self.logger_table_font_size) self.set_logging_level(options.get('console_logging_level', ROOT_LOG.level)) - def save_options(self): + def save_options(self, sync=False): self.log.debug('Saving options') self.qsettings.beginGroup('Configuration') for option in self.option_spec: self.qsettings.setValue(option.name, self.options[option.name]) self.qsettings.endGroup() - self.sync() + if sync: # syncing is probably not necessary here, so the default is False + self.sync() def sync(self): self.log.debug('Syncing QSettings') @@ -242,6 +251,12 @@ def load_header_preset(self, name): s.endGroup() return result + def delete_header_preset(self, name): + s = self.qsettings + s.beginGroup('Header_Presets') + s.remove(name) + s.endGroup() + def save_geometry(self, geometry): s = self.qsettings s.beginGroup('Geometry') diff --git a/cutelog/listener.py b/cutelog/listener.py index 00ebb8d..d01d376 100644 --- a/cutelog/listener.py +++ b/cutelog/listener.py @@ -174,7 +174,8 @@ def run(self): 'relativeCreated': 4951865.670204163, 'stack_info': None, 'thread': 140062538003776, - 'threadName': 'MainThread'} + 'threadName': 'MainThread', + 'extra_column': 'hey there'} c = 0 while True: if self.need_to_stop(): diff --git a/cutelog/logger_tab.py b/cutelog/logger_tab.py index 782cd0c..5d6f5b2 100644 --- a/cutelog/logger_tab.py +++ b/cutelog/logger_tab.py @@ -131,8 +131,7 @@ def __init__(self, parent, levels, header, max_capacity=0): super().__init__(parent) self.parent_widget = parent self.levels = levels - # maxlen isn't needed here, because of how on_record has to handle max_capacity - self.records = deque(maxlen=max_capacity if max_capacity > 0 else None) + self.records = deque() self.font = parent.font() self.date_formatter = logging.Formatter('%(asctime)s') # to format unix timestamp as a date self.dark_theme = False @@ -158,7 +157,7 @@ def data(self, index, role=Qt.DisplayRole): if role == Qt.DisplayRole: column = self.table_header[index.column()] - result = getattr(record, column.name) + result = getattr(record, column.name, None) elif role == Qt.DecorationRole: if self.headerData(index.column()) == 'Message': if record.exc_text: @@ -308,14 +307,14 @@ def filterAcceptsRow(self, sourceRow, sourceParent): result = True if path: name = record.name - # name is None for record added by method add_conn_closed_record(). + # name is None for record added by method add_conn_closed_record(). if name is None: result = False elif name == path: result = True elif not self.selection_includes_children and name == path: result = True - elif self.selection_includes_children and name.startswith('{}.'.format(path)): + elif self.selection_includes_children and name.startswith(path + '.'): result = True else: result = False @@ -718,21 +717,16 @@ def open_header_menu(self, position): menu.popup(self.table_header_view.viewport().mapToGlobal(position)) def open_header_dialog(self): - d = HeaderEditDialog(self.main_window, self.table_header.columns) + d = HeaderEditDialog(self.main_window, self.table_header) d.header_changed.connect(self.header_changed) d.setWindowTitle('Header editor') d.open() - def header_changed(self, action, data): - if action == 'rearrange': - self.table_header.replace_columns(data) - elif action == 'load': - loaded = CONFIG.load_columns_preset(data) - self.table_header.replace_columns(loaded) - elif action == 'save': - CONFIG.save_columns_preset(data, self) - elif action == 'save new': - pass + def header_changed(self, preset_name, set_as_default, columns): + self.table_header.preset_name = preset_name + if set_as_default: + CONFIG.set_option('default_header_preset', preset_name) + self.table_header.replace_columns(columns) self.set_columns_sizes() def merge_with_records(self, new_records): diff --git a/cutelog/logger_table_header.py b/cutelog/logger_table_header.py index 4e9a926..05d62e5 100644 --- a/cutelog/logger_table_header.py +++ b/cutelog/logger_table_header.py @@ -1,11 +1,14 @@ import copy import json +from functools import partial -from PyQt5.QtCore import Qt, pyqtSignal, QObject, QEvent -from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QListWidget, - QListWidgetItem, QVBoxLayout) +from PyQt5.QtCore import QEvent, QObject, Qt, pyqtSignal +from PyQt5.QtWidgets import (QCheckBox, QDialog, QDialogButtonBox, + QInputDialog, QLabel, QLineEdit, QListWidget, + QListWidgetItem, QMenu, QVBoxLayout) from .config import CONFIG +from .utils import show_warning_dialog class Column: @@ -55,18 +58,14 @@ def __repr__(self): class LoggerTableHeader(QObject): def __init__(self, header_view): super().__init__() - columns = CONFIG.load_header_preset('Default') + self.preset_name = CONFIG['default_header_preset'] + columns = CONFIG.load_header_preset(self.preset_name) if not columns: columns = DEFAULT_COLUMNS self.columns = copy.deepcopy(columns) self.visible_columns = [c for c in self.columns if c.visible] self.header_view = header_view self.table_model = None # will be set from within the model immediately - self.preset_name = 'Default' - # self.ignore_resizing = False - - def load_columns(self): - pass def eventFilter(self, object, event): """ @@ -83,22 +82,22 @@ def eventFilter(self, object, event): return True return False - def reset_columns(self): - self.replace_columns(copy.deepcopy(DEFAULT_COLUMNS)) - - # def column_resized(self, index, old_size, new_size): def mouse_released(self): for section in range(self.header_view.count()): col = self.visible_columns[section] col.width = self.header_view.sectionSize(section) CONFIG.save_header_preset(self.preset_name, self.columns) - def replace_columns(self, new_columns, preset_name='Default'): - self.preset_name = preset_name + def reset_columns(self): + self.replace_columns(copy.deepcopy(DEFAULT_COLUMNS), save=False) + self.preset_name = 'Stock' + + def replace_columns(self, new_columns, save=True): self.columns = new_columns self.visible_columns = [c for c in self.columns if c.visible] self.table_model.modelReset.emit() - CONFIG.save_header_preset(self.preset_name, self.columns) + if save: + CONFIG.save_header_preset(self.preset_name, self.columns) def __getitem__(self, index): return self.visible_columns[index] @@ -116,6 +115,8 @@ def __init__(self, parent, column): def data(self, role): if role == Qt.DisplayRole: return self.column.title + elif role == Qt.ToolTipRole: + return self.column.name elif role == Qt.CheckStateRole: if self.column.visible: return Qt.Checked @@ -130,35 +131,47 @@ def setData(self, role, value): class HeaderEditDialog(QDialog): - header_changed = pyqtSignal(str, list) + # name of the current preset, whether to set this preset as default, list of Columns + header_changed = pyqtSignal(str, bool, list) - def __init__(self, parent, columns): + def __init__(self, parent, table_header): super().__init__(parent) - self.columns = copy.deepcopy(columns) + self.table_header = table_header + self.default_preset_name = None + self.preset_name = table_header.preset_name + self.columns = copy.deepcopy(table_header.columns) self.setupUi() def setupUi(self): self.resize(200, 400) self.vbox = QVBoxLayout(self) + self.presetLabel = QLabel("Preset: {}".format(self.preset_name), self) self.columnList = QListWidget(self) + self.setAsDefaultCheckbox = QCheckBox("Set as default preset", self) + self.vbox.addWidget(self.presetLabel) self.vbox.addWidget(self.columnList) + self.vbox.addWidget(self.setAsDefaultCheckbox) self.columnList.setDragDropMode(QListWidget.InternalMove) self.columnList.setDefaultDropAction(Qt.MoveAction) self.columnList.setSelectionMode(QListWidget.ExtendedSelection) self.columnList.setAlternatingRowColors(True) self.columnList.installEventFilter(self) + self.columnList.setContextMenuPolicy(Qt.CustomContextMenu) + self.columnList.customContextMenuRequested.connect(self.open_menu) + self.columnList.model().rowsMoved.connect(self.read_columns_from_list) + + # for a dumb qss hack to make selected checkboxes not white on a light theme self.columnList.setObjectName("ColumnList") - # self.columnList.setContextMenuPolicy(Qt.CustomContextMenu) - # self.columnList.customContextMenuRequested.connect(self.open_menu) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, self) self.vbox.addWidget(self.buttonBox) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.fill_column_list() + self.set_default_checkbox() def eventFilter(self, object, event): if event.type() == QEvent.KeyPress: @@ -168,20 +181,27 @@ def eventFilter(self, object, event): return False def fill_column_list(self): + self.columnList.clear() for column in self.columns: ColumnListItem(self.columnList, column) def accept(self): - result = [] - for i in range(self.columnList.count()): - item = self.columnList.item(i) - result.append(item.column) - self.header_changed.emit('rearrange', result) + self.read_columns_from_list() + self.header_changed.emit(self.preset_name, + self.setAsDefaultCheckbox.isChecked(), + self.columns) self.done(0) def reject(self): self.done(0) + def read_columns_from_list(self): + new_columns = [] + for i in range(self.columnList.count()): + item = self.columnList.item(i) + new_columns.append(item.column) + self.columns = new_columns + def toggle_selected_columns(self): selected = self.columnList.selectedItems() for item in selected: @@ -189,59 +209,129 @@ def toggle_selected_columns(self): item.setData(Qt.CheckStateRole, not value_now) self.columnList.reset() # @Improvement: is there a better way to update QListWidget? - # def load_preset(self, action): - # name = action.text() - # self.result_future(('load', name)) - # - # def save_preset(self, action): - # result = [] - # for i in range(self.columnList.count()): # column list has to be generated here because if - # item = self.columnList.item(i) # you rearrange and save, then what gets saved is - # result.append(item.column) # the un-rearranged list from the table header - # - # name = action.text() - # if action.property('new'): - # self.result_future(('save new', (name, result))) - # else: - # self.result_future(('save', (name, result))) - - # def open_menu(self, position): - # return # @TODO: implement header presets - # menu = QMenu(self) - # - # load_menu = QMenu('Load preset', self) - # save_menu = QMenu('Save preset as', self) - # save_new_action = save_menu.addAction('New') - # save_new_action.setProperty('new', True) - # save_menu.addSeparator() - # - # presets = CONFIG.get_columns_presets() - # for preset in presets: - # load_menu.addAction(preset) - # save_menu.addAction(preset) - # - # load_menu.triggered.connect(self.load_preset) - # save_menu.triggered.connect(self.save_preset) - # - # menu.addMenu(load_menu) - # menu.addMenu(save_menu) - # - # menu.popup(self.columnList.viewport().mapToGlobal(position)) - - def get_selected_items(self): - result = [] - selected = self.columnList.selectedIndexes() - for index in selected: - item = self.columnList.itemFromIndex(index) - result.append(item) - return result - - def enable_selected(self): - selected = self.get_selected_items() - for item in selected: - item.setData(Qt.CheckStateRole, Qt.Checked) + def open_menu(self, position): + menu = QMenu(self) + + preset_menu = menu.addMenu('Presets') + preset_menu.addAction('New preset', self.new_preset_dialog) + preset_menu.addSeparator() + + preset_names = CONFIG.get_header_presets() + + if len(preset_names) == 0: + action = preset_menu.addAction('No presets') + action.setEnabled(False) + else: + delete_menu = menu.addMenu('Delete preset') + for name in preset_names: + preset_menu.addAction(name, partial(self.load_preset, name)) + delete_menu.addAction(name, partial(self.delete_preset, name)) + + menu.addSeparator() + menu.addAction('New column...', self.create_new_column_dialog) - def disable_selected(self): - selected = self.get_selected_items() + if len(self.columnList.selectedIndexes()) > 0: + menu.addAction('Delete selected', self.delete_selected) + + menu.popup(self.columnList.viewport().mapToGlobal(position)) + + def load_preset(self, name): + new_columns = CONFIG.load_header_preset(name) + if not new_columns: + return + + self.columns = new_columns + self.preset_name = name + self.fill_column_list() + self.presetLabel.setText("Preset: {}".format(name)) + self.set_default_checkbox() + + def new_preset_dialog(self): + d = QInputDialog(self) + d.setLabelText('Enter the new name for the new preset:') + d.setWindowTitle('Create new preset') + d.textValueSelected.connect(self.create_new_preset) + d.open() + + def create_new_preset(self, name): + if name in CONFIG.get_header_presets(): + show_warning_dialog(self, "Preset creation error", + 'Preset named "{}" already exists.'.format(name)) + return + if len(name.strip()) == 0: + show_warning_dialog(self, "Preset creation error", + 'This preset name is not allowed.'.format(name)) + return + + self.preset_name = name + self.presetLabel.setText("Preset: {}".format(name)) + CONFIG.save_header_preset(name, self.columns) + self.setAsDefaultCheckbox.setChecked(False) + + def delete_preset(self, name): + CONFIG.delete_header_preset(name) + if name == self.preset_name: + self.columns = copy.deepcopy(DEFAULT_COLUMNS) + self.fill_column_list() + + def create_new_column_dialog(self): + d = CreateNewColumnDialog(self) + d.add_new_column.connect(self.add_new_column) + d.setWindowTitle('Create new column') + d.open() + + def add_new_column(self, name, title): + new_column = Column(name, title) + # if the last column is message, insert this column before it (i think it makes sense?) + if self.columns[-1].name == 'message': + self.columns.insert(-1, new_column) + else: + self.columns.append(new_column) + self.fill_column_list() + + def set_default_checkbox(self): + self.setAsDefaultCheckbox.setChecked(CONFIG['default_header_preset'] == self.preset_name) + + def delete_selected(self): + selected = self.columnList.selectedItems() for item in selected: - item.setData(Qt.CheckStateRole, Qt.Unchecked) + self.columnList.takeItem(self.columnList.row(item)) + self.read_columns_from_list() + self.fill_column_list() + + +class CreateNewColumnDialog(QDialog): + + # name, title + add_new_column = pyqtSignal(str, str) + + def __init__(self, parent): + super().__init__(parent) + + self.setupUi() + + def setupUi(self): + self.resize(300, 120) + self.vbox = QVBoxLayout(self) + self.nameLabel = QLabel("Name of the column:", self) + self.nameLine = QLineEdit(self) + self.nameLine.setPlaceholderText('threadName') + self.titleLabel = QLabel("Title of the column:", self) + self.titleLine = QLineEdit(self) + self.titleLine.setPlaceholderText('Thread name') + self.vbox.addWidget(self.nameLabel) + self.vbox.addWidget(self.nameLine) + self.vbox.addWidget(self.titleLabel) + self.vbox.addWidget(self.titleLine) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + self.vbox.addWidget(self.buttonBox) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + def accept(self): + self.add_new_column.emit(self.nameLine.text(), self.titleLine.text()) + self.done(0) + + def reject(self): + self.done(0) diff --git a/cutelog/resources/light_theme.qss b/cutelog/resources/light_theme.qss index 466ac5f..0670db0 100644 --- a/cutelog/resources/light_theme.qss +++ b/cutelog/resources/light_theme.qss @@ -123,14 +123,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { QScrollBar:horizontal { - background-color: #2A2929; + background-color: #E6E6E6; height: 10px; margin: 0px 10px 0px 10px; border: 1px transparent #2A2929; } QScrollBar::handle:horizontal { - background-color: #605F5F; + background-color: #B6B6B6; min-width: 10px; } diff --git a/cutelog/resources/ui/settings_dialog.ui b/cutelog/resources/ui/settings_dialog.ui index 3a8b41c..627ff27 100644 --- a/cutelog/resources/ui/settings_dialog.ui +++ b/cutelog/resources/ui/settings_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 600 - 260 + 614 + 392 @@ -73,53 +73,40 @@ - - - - - 1 - 0 - - - - 1 - - - - - + + 10 + + + - Font in te&xt edit windows + Lo&gger table row height - textViewFont + loggerTableRowHeight - - - - Qt::Vertical - - - - 0 - 0 - + + + + + 5 + 0 + - + - + - Indicate exce&ption with + Indica&te exception with excIndicationComboBox - + @@ -144,17 +131,30 @@ - - - - - 0 - 0 - + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + Font in te&xt edit windows + + + textViewFont - + @@ -164,30 +164,27 @@ - - - - - 1 - 0 - + + + + Font in &logger tables - - 1 + + loggerTableFont - - + + - 5 + 0 0 - + @@ -200,7 +197,7 @@ - + Dark &theme by default @@ -210,23 +207,29 @@ - - - - Logger table row height + + + + + 1 + 0 + - - loggerTableRowHeight + + 1 - - - - Font in &logger tables + + + + + 1 + 0 + - - loggerTableFont + + 1 @@ -234,6 +237,9 @@ + + 10 + @@ -247,7 +253,7 @@ - O&pen by default + Open by defa&ult searchOpenDefaultCheckBox @@ -343,6 +349,9 @@ + + 10 + @@ -411,6 +420,9 @@ + + 10 + @@ -489,7 +501,7 @@ - Light theme is nati&ve style + Li&ght theme is native style lightThemeNativeCheckBox @@ -517,13 +529,6 @@ listWidget - darkThemeDefaultCheckBox - loggerTableFont - loggerTableFontSize - textViewFont - textViewFontSize - loggerTableRowHeight - excIndicationComboBox searchOpenDefaultCheckBox searchRegexDefaultCheckBox searchCaseSensitiveDefaultCheckBox diff --git a/cutelog/settings_dialog.py b/cutelog/settings_dialog.py index fedc143..cf43198 100644 --- a/cutelog/settings_dialog.py +++ b/cutelog/settings_dialog.py @@ -37,8 +37,8 @@ def setup_tooltips(self): 'for testing purposes only.') self.singleTabCheckBox.setToolTip("Forces all connections into one tab. " - "Useful for when you're restarting one " - "program very often.") + "Useful for when you're restarting one " + "program very often.") self.singleTabLabel.setBuddy(self.singleTabCheckBox) # @Hmmm: why doesn't this work? def load_from_config(self): @@ -108,7 +108,6 @@ def save_to_config(self): CONFIG.update_options(o) def accept(self): - # print('accepting') self.save_to_config() if self.server_restart_needed: show_info_dialog(self.parent_widget, 'Warning', @@ -116,7 +115,6 @@ def accept(self): self.done(0) def reject(self): - # print('rejecting') self.done(0) def server_options_changed(self): diff --git a/setup.py b/setup.py index b046e48..11ab6e4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools.command.install import install -VERSION = '1.1.6' +VERSION = '1.1.7' def build_qt_resources():