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