mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 15:13:26 +00:00
955 lines
31 KiB
Python
955 lines
31 KiB
Python
import pickle
|
|
import logging
|
|
from ast import literal_eval
|
|
from functools import partial
|
|
from copy import deepcopy
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
from PyQt5.QtCore import Qt, pyqtSlot
|
|
from PyQt5.QtGui import QCursor, QKeySequence
|
|
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu, QAction, QMessageBox, QInputDialog, QFileDialog, \
|
|
QShortcut
|
|
|
|
import config_editor
|
|
|
|
import sys
|
|
import os, inspect # Add parent dir to PATH to import messaging_lib
|
|
|
|
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
parent_dir = os.path.dirname(current_dir)
|
|
sys.path.insert(0, parent_dir)
|
|
|
|
import config
|
|
|
|
|
|
states_colors = {
|
|
'normal': Qt.white,
|
|
'unchanged': Qt.blue,
|
|
'default': Qt.cyan,
|
|
'edited': Qt.yellow,
|
|
'added': Qt.green,
|
|
'deleted': Qt.red,
|
|
}
|
|
|
|
StateRole = 999
|
|
TypeRole = 998
|
|
|
|
|
|
def convert_type(data):
|
|
try:
|
|
data = literal_eval(data) if data else None
|
|
except (SyntaxError, ValueError):
|
|
data = str(data)
|
|
return data
|
|
|
|
|
|
class ConfigModelItem:
|
|
def __init__(self, values=(None, None, None, None), item_type='option',
|
|
state='normal', default=None, parent=None):
|
|
self.spec_default = default
|
|
self.itemData = list(values)
|
|
self.state = state
|
|
self.type = item_type
|
|
|
|
if isinstance(self.data(1), (list, tuple)):
|
|
self.type = 'list'
|
|
|
|
self.default_values = deepcopy(self.itemData)
|
|
self.default_state = state
|
|
|
|
self.childItems = []
|
|
self.parentItem = parent
|
|
|
|
self.setup_type()
|
|
|
|
if self.parentItem is not None:
|
|
self.parentItem.appendChild(self)
|
|
|
|
def setup_type(self):
|
|
if self.type == 'section':
|
|
self.itemData[1:1] = ('<section>',)
|
|
self.spec_default = self.data(1)
|
|
|
|
elif self.type == 'list':
|
|
self._setup_list(self.get_list_items())
|
|
|
|
def _get_list_spec(self):
|
|
data = self.data(1)
|
|
comments = self.data(2)
|
|
if comments:
|
|
try:
|
|
raw_spec = comments.split('\n')[-1].split()[1:]
|
|
if raw_spec[0] == '__list__': # and len(raw_spec[1:]) == len(data):
|
|
return raw_spec[1:]
|
|
except IndexError:
|
|
pass
|
|
return list(map(str, range(len(data))))
|
|
|
|
def get_list_items(self):
|
|
spec = self._get_list_spec()
|
|
values = self.data(1)
|
|
if isinstance(self.spec_default, list):
|
|
defaults = self.spec_default
|
|
else:
|
|
defaults = (None, )*len(spec)
|
|
|
|
self.itemData[1] = '<list: {}>'.format(' '.join(spec))
|
|
# self.spec_default = self.itemData[1]
|
|
|
|
for key, value, default in zip(spec, values, defaults):
|
|
yield ConfigModelItem((key, value, None, None), item_type='list_item',
|
|
state=self.state, default=default)
|
|
|
|
def _setup_list(self, items): # use only at initialization
|
|
for child in items:
|
|
self.appendChild(child)
|
|
|
|
@property
|
|
def is_section(self): # probably deprecated
|
|
return self.type == 'section'
|
|
|
|
def appendChild(self, item):
|
|
self.childItems.append(item)
|
|
item.parentItem = self
|
|
|
|
def addChildren(self, items, row):
|
|
if row == -1:
|
|
row = 0
|
|
self.childItems[row:row] = items
|
|
|
|
for item in items:
|
|
item.parentItem = self
|
|
|
|
def child(self, row):
|
|
return self.childItems[row]
|
|
|
|
def childCount(self):
|
|
return len(self.childItems)
|
|
|
|
def columnCount(self):
|
|
return len(self.itemData)
|
|
|
|
def data(self, column):
|
|
try:
|
|
return self.itemData[column]
|
|
except IndexError:
|
|
return None
|
|
|
|
def set_data(self, data, column):
|
|
old_data = self.data(column)
|
|
if old_data is None:
|
|
data = convert_type(data)
|
|
|
|
if data == '<list>':
|
|
data = []
|
|
|
|
try:
|
|
self.itemData[column] = data
|
|
except IndexError:
|
|
return False
|
|
|
|
if old_data != data:
|
|
self.set_state('edited')
|
|
self.check_state()
|
|
|
|
return True
|
|
|
|
def check_state(self):
|
|
if self.spec_default is not None and self.data(1) == self.spec_default \
|
|
and self.data(0) == self.default_values[0] and self.type != 'section':
|
|
self.set_state('default')
|
|
# print('def', self.data(1), self.data(0), self.spec_default)
|
|
|
|
child_states = [child.state for child in self.childItems]
|
|
if any(state in child_states for state in ['edited', 'added', 'deleted']):
|
|
self.state = 'edited'
|
|
if len(set(child_states)) == 1: # if all states equal
|
|
self.set_state(child_states[0], set_children=False)
|
|
# print(child_states)
|
|
|
|
if self.parentItem is not None:
|
|
self.parentItem.check_state()
|
|
|
|
def set_state(self, state, set_children=True):
|
|
if self.state == 'unchanged' and state == 'default':
|
|
return
|
|
|
|
if self.state == 'added' and state in ('edited', 'unchanged', 'default', 'normal'):
|
|
return
|
|
|
|
self.state = state
|
|
|
|
if set_children: # to prevent cycle state set
|
|
for child in self.childItems:
|
|
child.set_state(state)
|
|
|
|
# if state == 'edited':
|
|
# self.parentItem.state = state
|
|
|
|
def set_type(self, item_type):
|
|
self.type = item_type
|
|
|
|
def parent(self):
|
|
return self.parentItem
|
|
|
|
def row(self):
|
|
if self.parentItem is not None:
|
|
return self.parentItem.childItems.index(self)
|
|
return 0
|
|
|
|
def removeChild(self, position):
|
|
if position < 0 or position > len(self.childItems):
|
|
return False
|
|
child = self.childItems.pop(position)
|
|
child.parentItem = None
|
|
return True
|
|
|
|
def __repr__(self):
|
|
return str(self.itemData)
|
|
|
|
|
|
def ensure_unique_names(item, include_self=True):
|
|
name = item.data(0)
|
|
siblings_names = [child.data(0) for child in item.parent().childItems]
|
|
if not include_self:
|
|
siblings_names.remove(name)
|
|
|
|
while name in siblings_names:
|
|
if '_copy' in name:
|
|
spl = name.split('_copy')
|
|
num = int(spl[1]) if spl[1] else 0
|
|
num += 1
|
|
name = spl[0] + '_copy' + str(num)
|
|
else:
|
|
name = name + '_copy'
|
|
|
|
item.set_data(name, 0)
|
|
|
|
|
|
class ConfigModel(QtCore.QAbstractItemModel):
|
|
def __init__(self, parent=None, widget=None,
|
|
headers=("Option", "Value", 'Comment', 'Inline Comment')):
|
|
self.rootItem = ConfigModelItem(headers)
|
|
super(ConfigModel, self).__init__(parent)
|
|
self.widget = widget
|
|
|
|
self.do_color = True
|
|
|
|
self.initial_comment = ''
|
|
self.final_comment = ''
|
|
|
|
@QtCore.pyqtSlot(int)
|
|
def enable_color(self, value):
|
|
self.do_color = value
|
|
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex(), (Qt.BackgroundRole, ))
|
|
|
|
def headerData(self, section, orientation, role):
|
|
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
|
return self.rootItem.data(section)
|
|
|
|
def columnCount(self, parent):
|
|
return self.rootItem.columnCount()
|
|
|
|
def rowCount(self, parent):
|
|
if parent.column() > 0:
|
|
return 0
|
|
|
|
if not parent.isValid():
|
|
parentItem = self.rootItem
|
|
else:
|
|
parentItem = parent.internalPointer()
|
|
|
|
return parentItem.childCount()
|
|
|
|
def childrenIndexes(self, parent):
|
|
column = parent.column()
|
|
parent = self.index(parent.row(), 0, parent.parent())
|
|
for i in range(self.rowCount(parent)):
|
|
yield self.index(i, column, parent)
|
|
|
|
def index(self, row, column, parent):
|
|
if not self.hasIndex(row, column, parent):
|
|
return QtCore.QModelIndex()
|
|
|
|
parentItem = self.nodeFromIndex(parent)
|
|
childItem = parentItem.child(row)
|
|
|
|
if childItem:
|
|
return self.createIndex(row, column, childItem)
|
|
else:
|
|
return QtCore.QModelIndex()
|
|
|
|
def parent(self, index):
|
|
if not index.isValid():
|
|
return QtCore.QModelIndex()
|
|
|
|
childItem = index.internalPointer()
|
|
if not isinstance(childItem, ConfigModelItem):
|
|
# print(childItem, index.column()), # index.row(), index.parent().internalPointer())
|
|
return QtCore.QModelIndex()
|
|
parentItem = childItem.parent()
|
|
|
|
if parentItem == self.rootItem: #or parentItem is None:
|
|
return QtCore.QModelIndex()
|
|
|
|
return self.createIndex(parentItem.row(), 0, parentItem)
|
|
|
|
def modifyCol(self, index, col):
|
|
return self.index(index.row(), col, index.parent())
|
|
|
|
def nodeFromIndex(self, index):
|
|
if index.isValid():
|
|
return index.internalPointer()
|
|
return self.rootItem
|
|
|
|
def data(self, index, role):
|
|
if not index.isValid():
|
|
return None
|
|
|
|
item = index.internalPointer()
|
|
|
|
if role == Qt.DisplayRole or role == Qt.EditRole:
|
|
return item.data(index.column())
|
|
|
|
if role == Qt.BackgroundRole and self.do_color:
|
|
return QtGui.QBrush(states_colors[item.state])
|
|
|
|
if role == StateRole:
|
|
return item.state
|
|
if role == TypeRole:
|
|
return item.type
|
|
|
|
return None
|
|
|
|
def setData(self, index, value, role=Qt.EditRole):
|
|
if not index.isValid():
|
|
return False
|
|
|
|
item = index.internalPointer()
|
|
|
|
if role == Qt.EditRole:
|
|
column = index.column()
|
|
|
|
if column == 0 and value != item.data(column):
|
|
if not self.widget.edit_caution():
|
|
return False
|
|
|
|
item.set_data(value, column)
|
|
|
|
if column == 0:
|
|
ensure_unique_names(item, include_self=False)
|
|
|
|
elif column == 1 and isinstance(item.data(1), (list, tuple)) \
|
|
and item.type not in ('list', 'list_item'):
|
|
|
|
item.set_type('list')
|
|
self.insertItems(0, list(item.get_list_items()), index)
|
|
self.widget.ui.config_view.expandAll()
|
|
|
|
elif role == StateRole:
|
|
item.set_state(value)
|
|
|
|
elif role == TypeRole:
|
|
# if value != item.type and value == 'list': # when list is created:
|
|
# pass
|
|
item.set_type(value)
|
|
|
|
self.dataChanged.emit(index, index, (role,))
|
|
|
|
return True
|
|
|
|
def flags(self, index):
|
|
if not index.isValid():
|
|
return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # Qt.NoItemFlags
|
|
item = index.internalPointer()
|
|
|
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
|
|
|
if index.column() == 0:
|
|
if item.type != 'list_item':
|
|
flags |= int(Qt.ItemIsDragEnabled)
|
|
|
|
if item.type == 'section':
|
|
flags |= int(Qt.ItemIsDropEnabled)
|
|
|
|
not_section = not (index.column() > 0 and item.type == 'section')
|
|
not_list_item = not (index.column() > 1 and item.type == 'list_item')
|
|
not_list_val = not (index.column() == 1 and item.type == 'list')
|
|
|
|
if not_section and not_list_item and not_list_val:
|
|
flags |= Qt.ItemIsEditable
|
|
|
|
return flags
|
|
|
|
def supportedDropActions(self):
|
|
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
|
|
|
|
def mimeTypes(self):
|
|
return ['app/configitem']
|
|
|
|
def mimeData(self, indexes):
|
|
mimedata = QtCore.QMimeData()
|
|
index = indexes[0]
|
|
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
|
|
return mimedata
|
|
|
|
def dropMimeData(self, mimedata, action, row, column, parentIndex):
|
|
if action == Qt.IgnoreAction:
|
|
return True
|
|
|
|
droppedNode = deepcopy(pickle.loads(mimedata.data('app/configitem')))
|
|
|
|
self.insertItems(row, [droppedNode], parentIndex)
|
|
self.dataChanged.emit(parentIndex, parentIndex)
|
|
|
|
self.widget.ui.config_view.expandAll()
|
|
|
|
if action & Qt.CopyAction:
|
|
return False # to not delete original item
|
|
return True
|
|
|
|
def removeRows(self, row, count, parent):
|
|
self.beginRemoveRows(parent, row, row + count - 1)
|
|
parentItem = self.nodeFromIndex(parent)
|
|
for _ in range(count):
|
|
parentItem.removeChild(row)
|
|
|
|
self.endRemoveRows()
|
|
return True
|
|
|
|
def removeRow(self, index):
|
|
parent = index.parent()
|
|
self.beginRemoveRows(parent, index.row(), index.row())
|
|
|
|
parentItem = self.nodeFromIndex(parent)
|
|
parentItem.removeChild(index.row())
|
|
|
|
self.endRemoveRows()
|
|
return True
|
|
|
|
def insertItems(self, row, items, parentIndex):
|
|
parent = self.nodeFromIndex(parentIndex)
|
|
self.beginInsertRows(parentIndex, row, row + len(items) - 1) # parentIndex or QtCore.QModelIndex()
|
|
|
|
parent.addChildren(items, row)
|
|
|
|
self.endInsertRows()
|
|
self.update_all()
|
|
|
|
return True
|
|
|
|
def update_all(self):
|
|
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
|
|
|
|
def dict_setup(self, data: dict, parent=None, convert_types=False):
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
|
|
for key, value in data.items():
|
|
if isinstance(value, dict):
|
|
item = ConfigModelItem((key,), parent=parent, item_type='section')
|
|
self.dict_setup(value, parent=item)
|
|
else:
|
|
if convert_types:
|
|
value = convert_type(value)
|
|
parent.appendChild(ConfigModelItem((key, value, '', '')))
|
|
|
|
def config_dict_setup(self, data: dict, convert_types=False, parent=None):
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
|
|
self.initial_comment = '\n'.join(data.pop('initial_comment', ['']))
|
|
self.final_comment = '\n'.join(data.pop('final_comment', ['']))
|
|
|
|
for key, item in data.items():
|
|
if '__value__' in item:
|
|
value = item.get('__value__')
|
|
if convert_types:
|
|
value = convert_type(value)
|
|
|
|
default = item['default']
|
|
comments = '\n'.join(item.get('comments', '')) or ''
|
|
inline_comment = item.get('inline_comment', '') or ''
|
|
|
|
if item['unchanged']:
|
|
state = 'unchanged'
|
|
elif value == default:
|
|
state = 'default'
|
|
else:
|
|
state = 'normal'
|
|
|
|
parent.appendChild(ConfigModelItem((key, value, comments, inline_comment),
|
|
state=state, default=default))
|
|
|
|
else:
|
|
section = ConfigModelItem((key,), parent=parent, item_type='section')
|
|
self.config_dict_setup(item, convert_types=convert_types, parent=section)
|
|
section.check_state()
|
|
|
|
def to_dict(self, parent=None) -> dict:
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
|
|
data = {}
|
|
for item in parent.childItems:
|
|
item_name, item_data = item.data(0), item.data(1)
|
|
if item.is_section:
|
|
data[item_name] = self.to_dict(item)
|
|
else:
|
|
data[item_name] = item_data
|
|
|
|
return data
|
|
|
|
def to_config_dict(self, parent=None) -> dict:
|
|
data = {}
|
|
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
data['initial_comment'] = self.initial_comment.split('\n')
|
|
data['final_comment'] = self.final_comment.split('\n')
|
|
|
|
for item in parent.childItems:
|
|
key = item.data(0)
|
|
|
|
if item.is_section:
|
|
d = self.to_config_dict(item)
|
|
if d: # to prevent empty sections
|
|
data[key] = d
|
|
|
|
elif item.state not in ('unchanged', 'deleted'):
|
|
if item.type == 'list':
|
|
value = [child.data(1) for child in item.childItems]
|
|
else:
|
|
value = item.data(1)
|
|
|
|
d = {'__value__': value}
|
|
|
|
comment = item.data(2)
|
|
if comment:
|
|
d.update({'comments': comment.split('\n')})
|
|
|
|
inline_comment = item.data(3)
|
|
if inline_comment:
|
|
d.update({'inline_comment': inline_comment})
|
|
|
|
data[key] = d
|
|
|
|
return data
|
|
|
|
@property
|
|
def dict(self):
|
|
return self.to_dict()
|
|
|
|
|
|
class ConfigTreeWidget(QTreeView):
|
|
def __init__(self):
|
|
QTreeView.__init__(self)
|
|
|
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self.open_menu)
|
|
|
|
self.setSelectionMode(self.SingleSelection)
|
|
# self.setSelectionBehavior(self.SelectItems)
|
|
|
|
self.setDragDropMode(QAbstractItemView.DragDrop)
|
|
self.setDefaultDropAction(Qt.MoveAction)
|
|
self.setDragEnabled(True)
|
|
self.setAcceptDrops(True)
|
|
self.setDropIndicatorShown(True)
|
|
self.setAnimated(True)
|
|
|
|
self.duplicate_shortcut = QShortcut(QKeySequence('Shift+D'), self)
|
|
self.duplicate_shortcut.activated.connect(self.with_selected(self.duplicate))
|
|
self.exclude_shortcut = QShortcut(QKeySequence('Alt+Del'), self)
|
|
self.exclude_shortcut.activated.connect(self.with_selected(self.exclude))
|
|
self.remove_shortcut = QShortcut(QKeySequence('Del'), self)
|
|
self.remove_shortcut.activated.connect(self.with_selected(self.remove))
|
|
self.clear_shortcut = QShortcut(QKeySequence('Shift+R'), self)
|
|
self.clear_shortcut.activated.connect(self.with_selected(self.reset_item, 'clear_value'))
|
|
self.default_shortcut = QShortcut(QKeySequence('Ctrl+R'), self)
|
|
self.default_shortcut.activated.connect(self.with_selected(self.reset_item, 'default'))
|
|
self.reset_shortcut = QShortcut(QKeySequence('Alt+R'), self)
|
|
self.reset_shortcut.activated.connect(self.with_selected(self.reset_item, 'all'))
|
|
self.item_shortcut = QShortcut(QKeySequence('Shift+A'), self)
|
|
self.item_shortcut.activated.connect(self.with_selected(self.add_item, False))
|
|
self.section_shortcut = QShortcut(QKeySequence('Ctrl+A'), self)
|
|
self.section_shortcut.activated.connect(self.with_selected(self.add_item, True))
|
|
|
|
def with_selected(self, f, *args, **kwargs):
|
|
def decorated():
|
|
index = self.selectedIndexes()[0]
|
|
return f(index, *args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
def open_menu(self, point):
|
|
index = self.indexAt(point)
|
|
item = index.internalPointer()
|
|
|
|
menu = QMenu()
|
|
|
|
duplicate = QAction("Duplicate")
|
|
duplicate.setShortcut(self.duplicate_shortcut.key())
|
|
duplicate.triggered.connect(partial(self.duplicate, index))
|
|
menu.addAction(duplicate)
|
|
|
|
exclude = QAction("Toggle exclude")
|
|
exclude.setShortcut(self.exclude_shortcut.key())
|
|
exclude.triggered.connect(partial(self.exclude, index))
|
|
menu.addAction(exclude)
|
|
|
|
remove = QAction("Remove from config")
|
|
remove.setShortcut(self.remove_shortcut.key())
|
|
remove.triggered.connect(partial(self.remove, index))
|
|
menu.addAction(remove)
|
|
|
|
menu.addSeparator()
|
|
|
|
clear = QAction("Clear item value")
|
|
clear.setShortcut(self.clear_shortcut.key())
|
|
clear.triggered.connect(partial(self.reset_item, index, 'clear_value'))
|
|
menu.addAction(clear)
|
|
|
|
reset_default = QAction("Reset value to default")
|
|
reset_default.setShortcut(self.default_shortcut.key())
|
|
reset_default.triggered.connect(partial(self.reset_item, index, 'default'))
|
|
menu.addAction(reset_default)
|
|
|
|
reset_all = QAction("Reset all changes")
|
|
reset_all.setShortcut(self.reset_shortcut.key())
|
|
reset_all.triggered.connect(partial(self.reset_item, index, 'all'))
|
|
menu.addAction(reset_all)
|
|
|
|
menu.addSeparator()
|
|
|
|
add_option = QAction("Add option")
|
|
add_option.setShortcut(self.item_shortcut.key())
|
|
add_option.triggered.connect(partial(self.add_item, index, False))
|
|
menu.addAction(add_option)
|
|
|
|
add_section = QAction("Add section")
|
|
add_section.setShortcut(self.section_shortcut.key())
|
|
add_section.triggered.connect(partial(self.add_item, index, True))
|
|
menu.addAction(add_section)
|
|
|
|
if item is None:
|
|
clear.setDisabled(True)
|
|
reset_all.setDisabled(True)
|
|
reset_default.setDisabled(True)
|
|
|
|
duplicate.setDisabled(True)
|
|
remove.setDisabled(True)
|
|
exclude.setDisabled(True)
|
|
else:
|
|
if item.type in ('list', 'list_item'):
|
|
add_section.setDisabled(True)
|
|
|
|
if item.type == 'list':
|
|
clear.setDisabled(True) # Temporary, cuz buggg
|
|
|
|
# if item.type == 'section':
|
|
# clear.setDisabled(True)
|
|
|
|
menu.exec_(QCursor.pos())
|
|
|
|
def duplicate(self, index):
|
|
item = deepcopy(index.internalPointer())
|
|
item.set_state('added')
|
|
ensure_unique_names(item)
|
|
self.model().insertItems(index.row() + 1, [item], index.parent())
|
|
self.expandAll() # fixes not expanded duplicated section
|
|
|
|
def remove(self, index):
|
|
self.model().removeRow(index)
|
|
|
|
def exclude(self, index):
|
|
item = self.model().nodeFromIndex(index)
|
|
if item.state == 'deleted':
|
|
self.model().setData(index, item.previous_state, StateRole)
|
|
else:
|
|
self.model().setData(index, 'deleted', StateRole)
|
|
|
|
def add_item(self, index, is_section):
|
|
parentItem = self.model().nodeFromIndex(index)
|
|
|
|
if parentItem.type in ('list', 'list_item'):
|
|
if is_section:
|
|
return
|
|
item_type = 'list_item'
|
|
else:
|
|
item_type = 'section' if is_section else 'option'
|
|
|
|
prompt = 'Enter {} name'.format(item_type.replace('_', ' '))
|
|
text, ok = QInputDialog.getText(self, prompt, prompt)
|
|
if not ok:
|
|
return
|
|
|
|
if parentItem.type in ('list', 'section'): # to append at first index in section or list
|
|
row = 0
|
|
parent = index
|
|
else:
|
|
row = index.row()
|
|
parent = index.parent()
|
|
if row == -1: # to append at last position e.g. at root
|
|
row = parentItem.childCount()
|
|
else:
|
|
row += 1 # to append under current position
|
|
|
|
item = ConfigModelItem((text, None, '', ''), item_type=item_type, state='added')
|
|
self.model().insertItems(row, [item], parent)
|
|
|
|
ensure_unique_names(item, include_self=False)
|
|
# parent.internalPointer().set_state('edited')
|
|
self.expandAll()
|
|
|
|
def reset_item(self, index, reset_type):
|
|
item = index.internalPointer()
|
|
model = self.model()
|
|
itemdataindex = model.modifyCol(index, 1)
|
|
|
|
if reset_type == 'all':
|
|
for i, default in enumerate(item.default_values):
|
|
model.setData(model.modifyCol(index, i), default)
|
|
|
|
model.setData(index, item.default_state, role=StateRole)
|
|
|
|
elif reset_type == 'default':
|
|
# if item.type == 'list' and \
|
|
# not isinstance(item.spec_default, (list, tuple)):
|
|
# self.reset_item(item, 'clear_value')
|
|
|
|
model.setData(itemdataindex, item.spec_default)
|
|
|
|
if item.default_state == 'unchanged':
|
|
model.setData(index, 'unchanged', role=StateRole)
|
|
|
|
elif reset_type == 'clear_value':
|
|
item_type = model.data(itemdataindex, TypeRole)
|
|
if item_type == 'list':
|
|
return
|
|
if item_type != 'section':
|
|
model.setData(itemdataindex, None)
|
|
|
|
# if model.data(itemdataindex, TypeRole) == 'list': # TODO
|
|
# model.removeRows(0, item.childCount(), index)
|
|
# model.setData(index, 'option', role=TypeRole)
|
|
# return
|
|
|
|
for child in model.childrenIndexes(index):
|
|
self.reset_item(child, reset_type)
|
|
|
|
|
|
class ConfigDialog(QtWidgets.QDialog):
|
|
copter_editor_signal = QtCore.pyqtSignal(object, object)
|
|
|
|
def __init__(self, parent=None):
|
|
super(ConfigDialog, self).__init__(parent)
|
|
self.ui = config_editor.Ui_config_dialog()
|
|
self.model = ConfigModel(widget=self)
|
|
self._filename = None
|
|
self.unsaved = False
|
|
self.setupUi()
|
|
self.copter_editor_signal.connect(self._call_copter_dialog)
|
|
|
|
@property
|
|
def filename(self):
|
|
return self._filename or 'Untitled.ini'
|
|
|
|
def setupModel(self, data, pure_dict=False, convert_types=False):
|
|
if pure_dict:
|
|
self.model.dict_setup(data, convert_types=convert_types)
|
|
else:
|
|
self.model.config_dict_setup(data, convert_types=convert_types)
|
|
|
|
self.ui.config_view.expandAll()
|
|
self.ui.config_view.resizeColumnToContents(0)
|
|
self.ui.config_view.resizeColumnToContents(1)
|
|
|
|
self.model.dataChanged.connect(self.unsaved_call) # connect after setup
|
|
|
|
def setupUi(self):
|
|
self.ui.setupUi(self)
|
|
|
|
self.ui.config_view = ConfigTreeWidget()
|
|
self.ui.config_view.setObjectName("config_view")
|
|
self.ui.config_view.setModel(self.model)
|
|
self.ui.gridLayout.addWidget(self.ui.config_view, 0, 0, 1, 1)
|
|
self.ui.config_view.expandAll()
|
|
|
|
self.ui.do_coloring.stateChanged.connect(self.model.enable_color)
|
|
self.ui.save_as_button.clicked.connect(self.save_as)
|
|
|
|
# self.ui.delete_button.pressed.connect(self.remove_selected)
|
|
|
|
def update_title(self):
|
|
self.setWindowTitle(f"Config editor - {self.filename}" + "*"*self.unsaved)
|
|
|
|
def unsaved_call(self):
|
|
self.unsaved = True
|
|
self.update_title()
|
|
self.model.dataChanged.disconnect(self.unsaved_call)
|
|
|
|
def closeEvent(self, event):
|
|
if not self.unsaved or self.result():
|
|
event.accept()
|
|
return
|
|
|
|
reply = QMessageBox.question(self, "Confirm exit", "There are unsaved changes in config file. "
|
|
"Are you sure you want to exit?",
|
|
QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
|
|
|
|
if reply != QMessageBox.Yes:
|
|
event.ignore()
|
|
else:
|
|
event.accept()
|
|
|
|
def edit_caution(self):
|
|
reply = QMessageBox().warning(self, "Editing caution",
|
|
"Are you sure you want to edit section/option name? "
|
|
"Proceed with caution!",
|
|
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
|
)
|
|
return reply == QMessageBox.Yes
|
|
|
|
def save_as(self):
|
|
save_path = QFileDialog.getSaveFileName(self, "Save as configuration file (.ini)",
|
|
directory=self.filename+'.ini',
|
|
options=QFileDialog.DontConfirmOverwrite,
|
|
filter="Config files (*.ini);;All files (*.*)")[0]
|
|
if not save_path:
|
|
return
|
|
|
|
split_path = save_path.split('.')
|
|
|
|
if not (len(split_path) > 1 and split_path[-1] == 'ini'):
|
|
save_path += '.ini'
|
|
|
|
cfg = config.ConfigManager()
|
|
cfg.load_from_dict(self.model.to_config_dict())
|
|
cfg.config.filename = save_path
|
|
cfg.write()
|
|
|
|
@pyqtSlot()
|
|
def run(self):
|
|
self.show()
|
|
self.exec()
|
|
return self.result()
|
|
|
|
def validation_loop(self, cfg, configspec=None): # modifies cfg object
|
|
filename = cfg.config.filename
|
|
while True:
|
|
if not self.run():
|
|
return False
|
|
|
|
try:
|
|
cfg.load_from_dict(self.model.to_config_dict(), configspec=configspec)
|
|
except config.ValidationError as error:
|
|
msg = "Can not validate. Proceed with editing? Errors: \n" + "\n".join(error.flatten_errors())
|
|
reply = QMessageBox.warning(self, "Validation error!", msg, QMessageBox.Yes | QMessageBox.Cancel)
|
|
|
|
if reply == QMessageBox.Cancel:
|
|
return False
|
|
|
|
else:
|
|
return True
|
|
|
|
finally:
|
|
if filename is not None:
|
|
cfg.config.filename = filename
|
|
|
|
def call_copter_dialog(self, client, value):
|
|
self.copter_editor_signal.emit(client, value)
|
|
|
|
@pyqtSlot(object, object)
|
|
def _call_copter_dialog(self, client, value):
|
|
logging.info("Opening copter config dialog")
|
|
config_dict, spec_dict = value["config"], value["configspec"]
|
|
cfg = config.ConfigManager()
|
|
cfg.load_from_dict(config_dict, spec_dict)
|
|
|
|
def save_callback():
|
|
edited_dict = cfg.full_dict(include_defaults=False)
|
|
client.send_message("config", kwargs={"config": edited_dict, "mode": "rewrite"})
|
|
|
|
def restart_callback():
|
|
client.send_message("service_restart", kwargs={"name": "clever-show"})
|
|
|
|
if not self.call_config_dialog(cfg, save_callback, restart_callback, f"{client.copter_id}"):
|
|
return False
|
|
return True
|
|
|
|
def call_config_dialog(self, cfg: config.ConfigManager, on_save=None, on_restart=None, name="Untitled.ini"):
|
|
self.setupModel(cfg.full_dict(include_defaults=True), convert_types=(not cfg.validated))
|
|
self.ui.do_restart.setEnabled(on_restart is not None)
|
|
self._filename = name
|
|
self.update_title()
|
|
|
|
if not self.validation_loop(cfg, cfg.config.configspec):
|
|
return False
|
|
|
|
if on_save is not None:
|
|
on_save()
|
|
|
|
if on_restart is not None and self.ui.do_restart.isChecked():
|
|
on_restart()
|
|
return True
|
|
|
|
@classmethod
|
|
def call_standalone_dialog(cls):
|
|
dialog = cls()
|
|
dialog._call_standalone_dialog()
|
|
|
|
def _call_standalone_dialog(self):
|
|
path = QFileDialog.getOpenFileName(self, "Select configuration or specification file",
|
|
filter="Config and spec files (*.ini)")[0]
|
|
if not path:
|
|
return False
|
|
|
|
cfg = config.ConfigManager()
|
|
try:
|
|
cfg.load_from_file(path)
|
|
except ValueError as error: # When file do not exist or not validated properly
|
|
QMessageBox.warning(self, "Error while opening file!",
|
|
"Config cannot be opened or validated: {}".format(error))
|
|
return False
|
|
|
|
def save_callback():
|
|
if cfg.config.filename is None:
|
|
save_path = QFileDialog.getSaveFileName(self, "Save configuration file",
|
|
directory=self.filename,
|
|
filter="Config files (*.ini)")[0]
|
|
if not save_path:
|
|
return False
|
|
else:
|
|
save_path = cfg.config.filename
|
|
|
|
cfg.config.filename = save_path
|
|
cfg.write()
|
|
|
|
if cfg.config.filename is not None:
|
|
name = os.path.split(cfg.config.filename)[1]
|
|
else: # when editing only configspec-based file
|
|
name = os.path.split(path)[1]
|
|
|
|
if not self.call_config_dialog(cfg, on_save=save_callback, name=name):
|
|
return False
|
|
return True
|
|
|
|
|
|
if __name__ == '__main__':
|
|
def except_hook(cls, exception, traceback):
|
|
print(cls, exception, traceback)
|
|
sys.__excepthook__(cls, exception, traceback)
|
|
|
|
sys.excepthook = except_hook
|
|
|
|
app = QtWidgets.QApplication(sys.argv)
|
|
|
|
ui = ConfigDialog()
|
|
ui.call_standalone_dialog()
|
|
# d = {'section': {'opt': 1, "opt222": 'text'}}
|
|
# ui.setupModel(d, pure_dict=True)
|
|
# ui.show()
|
|
# app.exec()
|
|
# print(ui.model.to_config_dict())
|