mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 23:19:33 +00:00
521 lines
16 KiB
Python
521 lines
16 KiB
Python
import pickle
|
|
from ast import literal_eval
|
|
from functools import partial
|
|
from copy import deepcopy
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
from PyQt5.QtCore import Qt as Qt
|
|
from PyQt5.QtGui import QCursor, QStandardItemModel
|
|
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu, QAction, QMessageBox, QInputDialog
|
|
|
|
import config_editor
|
|
|
|
|
|
def dict_walk(d: dict, keys):
|
|
current = d
|
|
for key in keys:
|
|
try:
|
|
current = current[key]
|
|
except KeyError:
|
|
return None
|
|
return current
|
|
|
|
|
|
class ConfigModelItem:
|
|
def __init__(self, values=(), is_section=False, state='normal', default=None, parent=None):
|
|
values = list(values)
|
|
if is_section:
|
|
values[1:1] = ('<section>',)
|
|
|
|
self.itemData = values
|
|
self.is_section = is_section
|
|
self.state = state
|
|
self.default = default
|
|
|
|
self.childItems = []
|
|
self.parentItem = parent
|
|
|
|
if self.parentItem is not None:
|
|
self.parentItem.appendChild(self)
|
|
|
|
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):
|
|
if self.data(column) is None:
|
|
data = literal_eval(data) if data else None
|
|
|
|
try:
|
|
self.itemData[column] = data
|
|
except IndexError:
|
|
return False
|
|
|
|
return True
|
|
|
|
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)
|
|
|
|
print(siblings_names, 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')):
|
|
super(ConfigModel, self).__init__(parent)
|
|
self.widget = widget
|
|
|
|
self.rootItem = ConfigModelItem(headers)
|
|
|
|
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 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()
|
|
parentItem = childItem.parent()
|
|
|
|
if parentItem == self.rootItem or parentItem is None:
|
|
return QtCore.QModelIndex()
|
|
|
|
return self.createIndex(parentItem.row(), 0, parentItem)
|
|
|
|
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())
|
|
|
|
return None
|
|
|
|
def setData(self, index, value, role=Qt.EditRole):
|
|
if not index.isValid():
|
|
return False
|
|
|
|
item = index.internalPointer()
|
|
if role == Qt.EditRole:
|
|
if index.column() == 0 and (self.widget is not None) \
|
|
and not self.widget.edit_caution():
|
|
return False
|
|
|
|
item.set_data(value, index.column())
|
|
if index.column() == 0:
|
|
ensure_unique_names(item)
|
|
|
|
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:
|
|
flags |= int(QtCore.Qt.ItemIsDragEnabled)
|
|
if item.is_section:
|
|
flags |= int(QtCore.Qt.ItemIsDropEnabled)
|
|
|
|
if not (index.column() == 1 and item.is_section):
|
|
flags |= Qt.ItemIsEditable
|
|
|
|
return flags
|
|
|
|
def supportedDropActions(self):
|
|
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
|
|
|
|
def mimeTypes(self):
|
|
return ['app/configitem', 'text/xml']
|
|
|
|
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 x 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)
|
|
|
|
parent.addChildren(items, row)
|
|
|
|
self.endInsertRows()
|
|
self.dataChanged.emit(parentIndex, parentIndex)
|
|
return True
|
|
|
|
def get_key_sequence(self, index):
|
|
item = index.internalPointer()
|
|
keys = []
|
|
while item is not None:
|
|
key = item.data(0)
|
|
keys.append(key)
|
|
item = item.parent()
|
|
return list(reversed(keys[:-1]))
|
|
|
|
def dict_setup(self, data: dict, parent=None):
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
|
|
for key, value in data.items():
|
|
if isinstance(value, dict):
|
|
item = ConfigModelItem((key,), parent=parent, is_section=True)
|
|
self.dict_setup(value, parent=item)
|
|
else:
|
|
parent.appendChild(ConfigModelItem((key, value)))
|
|
|
|
def config_dict_setup(self, data: dict, parent=None):
|
|
if parent is None:
|
|
data.pop('initial_comment', [''])
|
|
data.pop('final_comment', [''])
|
|
parent = self.rootItem
|
|
|
|
for key, item in data.items():
|
|
if item.get('__option__', False):
|
|
# {'__option__': True, 'value': 'Copter config', 'default': 'Copter config', 'unchanged': False, 'comments': [], 'inline_comment': None}
|
|
value = item['value']
|
|
default = item['default']
|
|
comments = '\n'.join(item['comments'])
|
|
inline_comment = item['inline_comment']
|
|
|
|
if item['unchanged']:
|
|
state = 'unchanged'
|
|
elif value == default:
|
|
state = 'default'
|
|
else:
|
|
state = 'normal'
|
|
|
|
parent.appendChild(ConfigModelItem((key, value, comments, inline_comment), state=state))
|
|
|
|
else:
|
|
section = ConfigModelItem((key,), parent=parent, is_section=True)
|
|
self.config_dict_setup(item, parent=section)
|
|
|
|
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:
|
|
if parent is None:
|
|
parent = self.rootItem
|
|
|
|
for item in parent.childItems:
|
|
pass
|
|
|
|
data = {}
|
|
|
|
return data
|
|
|
|
@property
|
|
def dict(self):
|
|
return self.to_dict()
|
|
|
|
|
|
class ConfigDialog(QtWidgets.QDialog):
|
|
def __init__(self):
|
|
super(ConfigDialog, self).__init__()
|
|
self.ui = config_editor.Ui_config_dialog()
|
|
self.model = ConfigModel(widget=self)
|
|
self.setupUi()
|
|
|
|
def setupModel(self, data, pure_dict=False):
|
|
if pure_dict:
|
|
self.model.dict_setup(data)
|
|
else:
|
|
self.model.config_dict_setup(data)
|
|
|
|
self.ui.config_view.expandAll()
|
|
|
|
def setupUi(self):
|
|
self.ui.setupUi(self)
|
|
|
|
self.ui.config_view = Tree()
|
|
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.delete_button.pressed.connect(self.remove_selected)
|
|
|
|
# index = self.config_view.selectedIndexes()[0]
|
|
|
|
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
|
|
|
|
|
|
class Tree(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.header()
|
|
# header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
|
# header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
|
# self.resizeColumnToContents(1)
|
|
# header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
|
|
|
self.setAnimated(True)
|
|
|
|
def open_menu(self, point):
|
|
index = self.indexAt(point)
|
|
item = index.internalPointer()
|
|
|
|
menu = QMenu()
|
|
|
|
duplicate = QAction("Duplicate")
|
|
duplicate.triggered.connect(partial(self.duplicate, index))
|
|
menu.addAction(duplicate)
|
|
|
|
remove = QAction("Remove from config")
|
|
remove.triggered.connect(partial(self.remove, index))
|
|
menu.addAction(remove)
|
|
|
|
menu.addSeparator()
|
|
|
|
add_option = QAction("Add option")
|
|
add_option.triggered.connect(partial(self.add_item, index, False))
|
|
menu.addAction(add_option)
|
|
|
|
add_section = QAction("Add section")
|
|
add_section.triggered.connect(partial(self.add_item, index, True))
|
|
|
|
menu.addAction(add_section)
|
|
|
|
if item is None:
|
|
duplicate.setDisabled(True)
|
|
remove.setDisabled(True)
|
|
|
|
menu.exec_(QCursor.pos())
|
|
|
|
def duplicate(self, index):
|
|
item = deepcopy(index.internalPointer())
|
|
ensure_unique_names(item)
|
|
self.model().insertItems(index.row() + 1, [item], index.parent())
|
|
self.expandAll() # fix not expanded duplicated section
|
|
|
|
def remove(self, index):
|
|
self.model().removeRow(index)
|
|
|
|
def add_item(self, index, is_section):
|
|
prompt = 'Enter {} name'.format('section' if is_section else 'option')
|
|
text, ok = QInputDialog.getText(self, prompt, prompt)
|
|
if not ok:
|
|
return
|
|
|
|
item = ConfigModelItem((text,), is_section=is_section)
|
|
row = index.row()
|
|
if row == -1: # to append at last position
|
|
parentItem = self.model().nodeFromIndex(index)
|
|
row = parentItem.childCount() - 1
|
|
|
|
self.model().insertItems(row + 1, [item], index.parent())
|
|
ensure_unique_names(item, include_self=False)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
|
|
|
|
def except_hook(cls, exception, traceback):
|
|
sys.__excepthook__(cls, exception, traceback)
|
|
|
|
|
|
sys.excepthook = except_hook
|
|
|
|
app = QtWidgets.QApplication(sys.argv)
|
|
|
|
# data = {"section 1": {"opt1": "str", "opt2": 123, "opt3": 1.23, "opt4": False, "...": {'subopt': 'bal'}},
|
|
# "section 2": {"opt1": "str", "opt2": [1.1, 2.3, 34], "opt3": 1.23, "opt4": False, "...": ""}}
|
|
data = {
|
|
'config_name': {'__option__': True, 'value': 'Copter config', 'default': 'Copter config', 'unchanged': False,
|
|
'comments': [], 'inline_comment': None},
|
|
'config_version': {'__option__': True, 'value': 0.0, 'default': 0.0, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None}, 'SERVER': {
|
|
'port': {'__option__': True, 'value': 25000, 'default': 25000, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None},
|
|
'host': {'__option__': True, 'value': '192.168.1.103', 'default': '192.168.1.101', 'unchanged': False,
|
|
'comments': [], 'inline_comment': None},
|
|
'buffer_size': {'__option__': True, 'value': 1024, 'default': 1024, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None}}, 'BROADCAST': {
|
|
'use': {'__option__': True, 'value': True, 'default': True, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None},
|
|
'port': {'__option__': True, 'value': 8181, 'default': 8181, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None}}, 'NTP': {
|
|
'use': {'__option__': True, 'value': False, 'default': False, 'unchanged': False, 'comments': [],
|
|
'inline_comment': None},
|
|
'port': {'__option__': True, 'value': 123, 'default': 123, 'unchanged': False,
|
|
'comments': ['#host = ntp1.stratum2.ru'], 'inline_comment': None},
|
|
'host': {'__option__': True, 'value': 'ntp1.stratum2.ru', 'default': 'ntp1.stratum2.ru', 'unchanged': True,
|
|
'comments': [], 'inline_comment': ''}}, 'PRIVATE': {
|
|
'id': {'__option__': True, 'value': '/hostname', 'default': '/hostname', 'unchanged': False,
|
|
'comments': ['# avialiable options: /hostname ; /default ; /ip ; any string 63 characters lengh'],
|
|
'inline_comment': None}},
|
|
'initial_comment': ['# This is generated config_attrs with defaults', '# Modify to configure'],
|
|
'final_comment': []}
|
|
|
|
ui = ConfigDialog()
|
|
ui.setupModel(data)
|
|
ui.show()
|
|
|
|
print(app.exec_())
|
|
|
|
print(ui.result())
|
|
print(ui.model.to_dict())
|
|
|
|
sys.exit()
|