mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 23:19:33 +00:00
Minimal working version of config editor
This commit is contained in:
@@ -1,19 +1,36 @@
|
||||
import pickle
|
||||
from ast import literal_eval
|
||||
from functools import partial
|
||||
from copy import deepcopy
|
||||
|
||||
import pickle
|
||||
|
||||
import config_editor
|
||||
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
|
||||
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, label, value="", is_section=False, state='default', parent=None):
|
||||
self.itemData = [label, value]
|
||||
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
|
||||
@@ -27,13 +44,8 @@ class ConfigModelItem:
|
||||
|
||||
def addChildren(self, items, row):
|
||||
if row == -1:
|
||||
self.childItems.extend(items)
|
||||
else:
|
||||
#row -= 1
|
||||
print('row', row)
|
||||
self.childItems[row:row] = items
|
||||
|
||||
print(self.childItems)
|
||||
row = 0
|
||||
self.childItems[row:row] = items
|
||||
|
||||
for item in items:
|
||||
item.parentItem = self
|
||||
@@ -45,7 +57,7 @@ class ConfigModelItem:
|
||||
return len(self.childItems)
|
||||
|
||||
def columnCount(self):
|
||||
return 2
|
||||
return len(self.itemData)
|
||||
|
||||
def data(self, column):
|
||||
try:
|
||||
@@ -54,6 +66,9 @@ class ConfigModelItem:
|
||||
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:
|
||||
@@ -67,41 +82,53 @@ class ConfigModelItem:
|
||||
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
|
||||
print('removing', position)
|
||||
child = self.childItems.pop(position)
|
||||
child.parentItem = None
|
||||
return True
|
||||
|
||||
def removeChildren(self, row, count):
|
||||
print(range(row, row+count))
|
||||
for pos in range(row, row+count):
|
||||
self.removeChild(pos)
|
||||
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.itemData)
|
||||
|
||||
|
||||
class ConfigModel(QtCore.QAbstractItemModel):
|
||||
def __init__(self, data, parent=None):
|
||||
super(ConfigModel, self).__init__(parent)
|
||||
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)
|
||||
|
||||
self.rootItem = ConfigModelItem("Option", "Value")
|
||||
self.setup(data)
|
||||
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 2
|
||||
return self.rootItem.columnCount()
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.column() > 0:
|
||||
@@ -154,14 +181,19 @@ class ConfigModel(QtCore.QAbstractItemModel):
|
||||
|
||||
return None
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
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,))
|
||||
|
||||
@@ -179,7 +211,7 @@ class ConfigModel(QtCore.QAbstractItemModel):
|
||||
if item.is_section:
|
||||
flags |= int(QtCore.Qt.ItemIsDropEnabled)
|
||||
|
||||
if index.column() == 1 and not item.is_section:
|
||||
if not (index.column() == 1 and item.is_section):
|
||||
flags |= Qt.ItemIsEditable
|
||||
|
||||
return flags
|
||||
@@ -188,45 +220,39 @@ class ConfigModel(QtCore.QAbstractItemModel):
|
||||
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
|
||||
|
||||
def mimeTypes(self):
|
||||
return ['bstream', 'text/xml']
|
||||
return ['app/configitem', 'text/xml']
|
||||
|
||||
def mimeData(self, indexes):
|
||||
mimedata = QtCore.QMimeData()
|
||||
index = indexes[0]
|
||||
mimedata.setData('bstream', pickle.dumps(self.nodeFromIndex(index)))
|
||||
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
|
||||
return mimedata
|
||||
|
||||
def dropMimeData(self, mimedata, action, row, column, parentIndex):
|
||||
print(action)
|
||||
print('mim', row)
|
||||
if action == Qt.IgnoreAction:
|
||||
return True
|
||||
|
||||
parentNode = self.nodeFromIndex(parentIndex)
|
||||
droppedNode = deepcopy(pickle.loads(mimedata.data('bstream')))
|
||||
print(droppedNode.itemData, 'node')
|
||||
#droppedNode = pickle.loads(mimedata.data('bstream')) #
|
||||
#self.removeRow()#self.index(row, column, parentIndex))#parentNode.child(row))
|
||||
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 removeRows1(self, row, count, parent):
|
||||
print('rem', row, count)
|
||||
self.beginRemoveRows(parent, row, row+count-1)
|
||||
def removeRows(self, row, count, parent):
|
||||
self.beginRemoveRows(parent, row, row + count - 1)
|
||||
parentItem = self.nodeFromIndex(parent)
|
||||
print(parentItem, parentItem.itemData)
|
||||
|
||||
#parentItem.removeChild(row)
|
||||
parentItem.removeChildren(row, count)
|
||||
print(parentItem.childItems)
|
||||
for x in range(count):
|
||||
parentItem.removeChild(row)
|
||||
|
||||
self.endRemoveRows()
|
||||
print('removed')
|
||||
return True
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def removeRow(self, index):
|
||||
parent = index.parent()
|
||||
self.beginRemoveRows(parent, index.row(), index.row())
|
||||
@@ -238,27 +264,61 @@ class ConfigModel(QtCore.QAbstractItemModel):
|
||||
return True
|
||||
|
||||
def insertItems(self, row, items, parentIndex):
|
||||
print('ins', row)
|
||||
parent = self.nodeFromIndex(parentIndex)
|
||||
self.beginInsertRows(parentIndex, row, row+len(items)-1)
|
||||
self.beginInsertRows(parentIndex, row, row + len(items) - 1)
|
||||
|
||||
parent.addChildren(items, row)
|
||||
print(parent.childItems)
|
||||
|
||||
self.endInsertRows()
|
||||
self.dataChanged.emit(parentIndex, parentIndex)
|
||||
return True
|
||||
|
||||
def setup(self, data: dict, parent=None):
|
||||
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.setup(value, parent=item)
|
||||
item = ConfigModelItem((key,), parent=parent, is_section=True)
|
||||
self.dict_setup(value, parent=item)
|
||||
else:
|
||||
parent.appendChild(ConfigModelItem(key, value))
|
||||
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:
|
||||
@@ -266,81 +326,147 @@ class ConfigModel(QtCore.QAbstractItemModel):
|
||||
|
||||
data = {}
|
||||
for item in parent.childItems:
|
||||
item_name, item_data = item.itemData
|
||||
if item.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(config_editor.Ui_config_dialog):
|
||||
def __init__(self, data):
|
||||
class ConfigDialog(QtWidgets.QDialog):
|
||||
def __init__(self):
|
||||
super(ConfigDialog, self).__init__()
|
||||
self.model = ConfigModel(data)
|
||||
self.ui = config_editor.Ui_config_dialog()
|
||||
self.model = ConfigModel(widget=self)
|
||||
self.setupUi()
|
||||
|
||||
def setupUi(self, config_dialog):
|
||||
super(ConfigDialog, self).setupUi(config_dialog)
|
||||
def setupModel(self, data, pure_dict=False):
|
||||
if pure_dict:
|
||||
self.model.dict_setup(data)
|
||||
else:
|
||||
self.model.config_dict_setup(data)
|
||||
|
||||
#self.config_view = Tree()
|
||||
self.ui.config_view.expandAll()
|
||||
|
||||
self.config_view = Tree()
|
||||
self.config_view.setObjectName("config_view")
|
||||
self.config_view.setModel(self.model)
|
||||
self.gridLayout.addWidget(self.config_view, 0, 0, 1, 1)
|
||||
def setupUi(self):
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.config_view.expandAll()
|
||||
#self.config_view.setDragDropMode(True)
|
||||
#self.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
#self.setDragEnabled(True)
|
||||
#self.setAcceptDrops(True)
|
||||
#self.setDropIndicatorShown(True)
|
||||
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.delete_button.pressed.connect(self.remove_selected)
|
||||
# self.ui.delete_button.pressed.connect(self.remove_selected)
|
||||
|
||||
def remove_selected(self):
|
||||
index = self.config_view.selectedIndexes()[0]
|
||||
self.model.removeRow(index)\
|
||||
# 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)
|
||||
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, "...": ""}}
|
||||
#model = ConfigModel(data)
|
||||
|
||||
#self.setModel(model)
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.open_menu)
|
||||
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
self.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
# self.setSelectionBehavior(self.SelectItems)
|
||||
|
||||
self.setDragDropMode(QAbstractItemView.DragDrop)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
|
||||
# def dropEvent(self, e):
|
||||
# print(e.dropAction()==QtCore.Qt.MoveAction)
|
||||
# if e.keyboardModifiers() & QtCore.Qt.AltModifier:
|
||||
# e.setDropAction()
|
||||
# print('copy')
|
||||
# else:
|
||||
# e.setDropAction(QtCore.Qt.MoveAction)
|
||||
# print("drop")
|
||||
# print(e)
|
||||
# e.accept()
|
||||
# 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()
|
||||
|
||||
def open_menu(self):
|
||||
menu = QMenu()
|
||||
menu.addAction("Create new folder")
|
||||
|
||||
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
|
||||
|
||||
@@ -348,22 +474,47 @@ if __name__ == '__main__':
|
||||
def except_hook(cls, exception, traceback):
|
||||
sys.__excepthook__(cls, exception, traceback)
|
||||
|
||||
|
||||
sys.excepthook = except_hook
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
Dialog = QtWidgets.QDialog()
|
||||
|
||||
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 = {"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(data)
|
||||
ui.setupUi(Dialog)
|
||||
ui = ConfigDialog()
|
||||
ui.setupModel(data)
|
||||
ui.show()
|
||||
|
||||
print(Qt.DisplayRole)
|
||||
Dialog.show()
|
||||
print(app.exec_())
|
||||
|
||||
print(Dialog.result())
|
||||
print(ui.result())
|
||||
print(ui.model.to_dict())
|
||||
|
||||
sys.exit()
|
||||
|
||||
Reference in New Issue
Block a user