frappy/secop/gui/cfg_editor/widgets.py
Markus Zolliker 41ce909172 update to changes from mlz repo
- bug fixes in error.py
- add from * to raise statements
- fix py35 compatibility
- finalize omit_unchanged_within feature
- fix follwup bug (missing Param.override) in proxy.py

Change-Id: I621c01a0d5e1ec6696fb06f39666f3316fb53649
2021-11-10 13:44:14 +01:00

480 lines
19 KiB
Python

# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Sandra Seger <sandra.seger@frm2.tum.de>
#
# *****************************************************************************
import os
from secop.gui.cfg_editor.config_file import read_config, write_config
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
from secop.gui.cfg_editor.utils import get_all_items, \
get_file_paths, get_interface_class_from_name, get_interfaces, \
get_module_class_from_name, get_modules, get_params, \
get_props, loadUi, set_name_edit_style, setActionIcon
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, pyqtSignal
NODE = 'node'
MODULE = 'module'
INTERFACE = 'interface'
PARAMETER = 'parameter'
PROPERTY = 'property'
COMMENT = 'comment'
class TreeWidget(QTreeWidget):
save_status_changed = pyqtSignal(bool)
add_canceled = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.file_path = None
self.setIconSize(QSize(24, 24))
self.setSelectionMode(QTreeWidget.SingleSelection)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.context_pos = QPoint(0, 0)
self.menu = QMenu()
self.context_actions = []
self.invalid_context_actions = {}
self.setup_context_actions()
self.customContextMenuRequested.connect(self.on_context_menu_requested)
def setup_context_actions(self):
edit = self.menu.addAction('rename')
a_m = self.menu.addAction('add module')
a_i = self.menu.addAction('add interface')
a_pa = self.menu.addAction('add parameter')
a_pr = self.menu.addAction('add property')
a_c = self.menu.addAction('add comment')
dup = self.menu.addAction('duplicate')
dele = self.menu.addAction('delete')
self.context_actions = [edit, a_m, a_i, a_pa, a_pr, a_c, dup, dele]
setActionIcon(edit, 'edit.png')
setActionIcon(a_m, 'add_module.png')
setActionIcon(a_i, 'add_interface.png')
setActionIcon(a_pa, 'add_parameter.png')
setActionIcon(a_pr, 'add_property.png')
setActionIcon(a_c, 'add_comment.png')
setActionIcon(dup, 'duplicate.png')
setActionIcon(dele, 'delete.png')
self.invalid_context_actions = {NODE: [a_pa, dup, dele],
MODULE: [],
INTERFACE: [a_pa],
PARAMETER: [edit, a_pa, dup],
PROPERTY: [edit, a_pa, a_pr, dup],
COMMENT: [edit, a_pa, a_pr, a_c, dup],
None: [edit, a_pa, a_pr, a_c, dup,
dele]}
edit.triggered.connect(self.change_name_via_context)
a_m.triggered.connect(self.add_module)
a_i.triggered.connect(self.add_interface)
a_pa.triggered.connect(self.add_parameter)
a_pr.triggered.connect(self.add_property)
a_c.triggered.connect(self.add_comment)
dup.triggered.connect(self.duplicate)
dele.triggered.connect(self.delete)
def emit_save_status_changed(self, status):
self.save_status_changed.emit(status)
def set_file(self, file_path):
self.file_path = file_path
if self.file_path:
if os.path.isfile(file_path):
self.set_tree(read_config(self.file_path))
self.emit_save_status_changed(True)
return True
self.file_path = None
return self.new_tree()
def new_tree(self):
dialog = AddDialog(NODE)
values = dialog.get_values()
if values:
sec_node = self.get_tree_widget_item(NODE, values[0], values[1],
None)
self.addTopLevelItem(sec_node)
sec_node.setExpanded(True)
self.mods = self.get_tree_widget_item(name='modules')
self.ifs = self.get_tree_widget_item(name='interfaces')
sec_node.addChild(self.ifs)
sec_node.addChild(self.mods)
self.emit_save_status_changed(False)
self.setCurrentItem(sec_node)
return True
return False
def set_tree(self, tree_items):
self.clear()
self.addTopLevelItem(tree_items[0])
self.ifs = tree_items[1]
self.mods = tree_items[2]
self.expandAll()
self.setCurrentItem(tree_items[0])
for item in get_all_items(self):
item.widget.save_status_changed.connect(self.emit_save_status_changed)
def add_module(self):
dialog = AddDialog(MODULE, get_modules(),
self.mods.get_children_names())
values = dialog.get_values()
if values:
module_class = get_module_class_from_name(values[1])
params = get_params(module_class)
props = get_props(module_class)
mod = self.get_tree_widget_item(MODULE, values[0], values[1],
module_class, params, props)
self.mods.addChild(mod)
# add all mandatory properties
for pr, pr_o in props.items():
# TODO: mandatory to must_be_configured
if pr_o.mandatory is True:
pr_i = self.get_tree_widget_item(PROPERTY, pr,
pr_o.default, pr_o)
mod.addChild(pr_i)
# add all mandatory properties of parameter
for pa, pa_o in params.items():
pa_i = None
for pr, pr_o in pa_o[1].items():
# TODO: mandatory to must_be_configured
if pr_o.mandatory is True:
pr_i = self.get_tree_widget_item(PROPERTY, pr,
pr_o.default, pr_o)
if not pa_i:
pa_i = self.get_tree_widget_item(PARAMETER, pa,
class_object=pa_o[0],
properties=pa_o[1])
mod.addChild(pa_i)
pa_i.addChild(pr_i)
self.setCurrentItem(mod)
self.emit_save_status_changed(False)
def add_interface(self):
dialog = AddDialog(INTERFACE, get_interfaces(),
self.ifs.get_children_names())
values = dialog.get_values()
if values:
interface_class = get_interface_class_from_name(values[1])
props = get_props(interface_class)
interface = self.get_tree_widget_item(INTERFACE, values[0],
values[1], interface_class,
properties=props)
self.ifs.addChild(interface)
# add all mandatory properties
for pr, pr_o in props.items():
# TODO: mandatory to must_be_configured
if pr_o.mandatory is True:
pr_i = self.get_tree_widget_item(PROPERTY, pr,
pr_o.default, pr_o)
interface.addChild(pr_i)
self.setCurrentItem(interface)
self.emit_save_status_changed(False)
def add_parameter(self):
selected_item = self.get_selected_item()
if not selected_item or self.is_heading(selected_item) or \
self.is_not_extendable(selected_item) or \
selected_item.kind in [PARAMETER, INTERFACE]:
return
params = selected_item.parameters
if params:
dialog = AddDialog(PARAMETER, params.keys())
values = dialog.get_values()
current_children = selected_item.get_children_names()
if values:
if values[0] in current_children:
self.setCurrentItem(selected_item.child(current_children.index(
values[0])))
else:
param = self.get_tree_widget_item(PARAMETER, values[0],
class_object=params[values[0]][0],
properties=params[values[0]][1])
selected_item.insertChild(0, param)
self.setCurrentItem(param)
self.emit_save_status_changed(False)
else:
# TODO: warning
pass
def add_property(self):
selected_item = self.get_selected_item()
if not selected_item or self.is_heading(selected_item) or \
self.is_not_extendable(selected_item):
return
props = selected_item.properties
if props:
dialog = AddDialog(PROPERTY, props.keys())
values = dialog.get_values()
current_children = selected_item.get_children_names()
if values:
if values[0] in current_children:
self.setCurrentItem(selected_item.child(current_children.index(
values[0])))
else:
prop = self.get_tree_widget_item(PROPERTY, values[0],
props[values[0]].default,
props[values[0]])
selected_item.insertChild(0, prop)
self.setCurrentItem(prop)
self.emit_save_status_changed(False)
else:
# TODO: warning
pass
def add_comment(self):
selected_item = self.get_selected_item()
if not selected_item or self.is_heading(selected_item) or \
selected_item.kind == COMMENT:
return
dialog = AddDialog(COMMENT)
values = dialog.get_values()
if values:
comm = self.get_tree_widget_item(COMMENT, '#', values[0])
selected_item.insertChild(0, comm)
self.setCurrentItem(comm)
self.emit_save_status_changed(False)
def duplicate(self):
selected_item = self.get_selected_item()
if not selected_item or selected_item.kind not in [MODULE, INTERFACE]:
return
selected_item.duplicate()
# TODO set duplicated selected
self.emit_save_status_changed(False)
def delete(self):
selected_item = self.get_selected_item()
if not selected_item or self.is_heading(selected_item) \
or selected_item == NODE:
return
selected_item.parent().removeChild(selected_item)
self.emit_save_status_changed(False)
def save(self, save_as=False):
file_name = self.file_path
if not self.file_path or save_as:
file_name = get_file_paths(self, False)[-1]
if file_name[-4:] == '.cfg':
self.file_path = file_name
write_config(self.file_path, self)
self.emit_save_status_changed(True)
return True
return False
def is_heading(self, item):
return item is self.ifs or item is self.mods
def is_not_extendable(self, item):
return item.kind in [PROPERTY, COMMENT]
def get_selected_item(self):
selected_item = self.selectedItems()
if not selected_item:
return None
return selected_item[-1]
def is_valid_name(self, name, kind):
if kind == MODULE:
if name in self.mods.get_children_names():
return False
elif kind == INTERFACE:
if name in self.ifs.get_children_names():
return False
else:
selected_item = self.get_selected_item()
if not selected_item:
return False
if name in selected_item.get_children_names():
return False
return True
def get_tree_widget_item(self, kind=None, name='', value=None,
class_object=None, parameters=None, properties=None):
item = TreeWidgetItem(kind, name, value, class_object, parameters or {}, properties or {})
item.widget.save_status_changed.connect(self.emit_save_status_changed)
return item
def change_name_via_context(self):
self.itemAt(self.context_pos).change_name()
def on_context_menu_requested(self, pos):
self.context_pos = pos
self.menu.move(self.mapToGlobal(pos))
self.menu.show()
for action in self.context_actions:
action.setEnabled(True)
for action in self.invalid_context_actions[self.itemAt(self.context_pos).kind]:
action.setEnabled(False)
class AddDialog(QDialog):
def __init__(self, kind, possible_values=None, invalid_names=None,
parent=None):
"""Notes:
self.get_value: is mapped to the specific method for getting
the value from self.value"""
super().__init__(parent)
loadUi(self, 'add_dialog.ui')
self.setWindowTitle('add %s' % kind)
self.kind = kind
self.invalid_names = invalid_names
if self.invalid_names:
for i, name in enumerate(self.invalid_names):
self.invalid_names[i] = name.lower()
if kind in [NODE, MODULE, INTERFACE]:
self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)
self.name = QLineEdit()
# TODO: input mask
self.name.textChanged.connect(self.check_input)
self.add_layout.addWidget(QLabel('name:'), 0, 0)
self.add_layout.addWidget(self.name, 0, 1)
if kind == NODE:
label_text = 'description:'
self.value = QTextEdit()
self.get_value = self.value.toPlainText
self.value.text = self.value.toPlainText
else:
label_text = 'kind:'
self.value = QComboBox()
self.get_value = self.value.currentText
if type(possible_values) == dict:
# TODO disable OK Button if TreeComboBox is empty
self.value = TreeComboBox(possible_values)
self.get_value = self.value.get_value
else:
self.value.addItems(possible_values)
self.value.setCurrentIndex(len(possible_values)-1)
self.add_layout.addWidget(QLabel(label_text), 1, 0)
self.add_layout.addWidget(self.value, 1, 1)
else:
if kind in [PARAMETER, PROPERTY]:
label_text = 'kind:'
self.value = QComboBox()
self.get_value = self.value.currentText
self.value.addItems(possible_values)
else:
label_text = 'comment:'
self.value = QTextEdit()
self.get_value = self.value.toPlainText
self.add_layout.addWidget(QLabel(label_text), 0, 0)
self.add_layout.addWidget(self.value, 0, 1)
if self.add_layout.rowCount() == 2:
self.setTabOrder(self.name, self.value)
self.setTabOrder(self.value, self.button_box.button(QDialogButtonBox.Ok))
self.setTabOrder(self.button_box.button(QDialogButtonBox.Ok),
self.button_box.button(QDialogButtonBox.Cancel))
def get_values(self):
if self.exec_() == QDialog.Accepted:
if self.kind in [NODE, MODULE, INTERFACE]:
return [self.name.text(), self.get_value()]
if self.kind in [PARAMETER, PROPERTY, COMMENT]:
return [self.get_value()]
return None
def check_input(self, name):
set_name_edit_style((self.kind in [MODULE, INTERFACE] and
name.lower() in self.invalid_names) or name == '',
self.name, self.button_box)
class TabBar(QTabBar):
def __init__(self, parent=None):
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.context_pos = QPoint(0, 0)
self.menu = QMenu()
close = self.menu.addAction('&Close')
close_all = self.menu.addAction('&Close all')
close.triggered.connect(self.close_tab_via_context)
close_all.triggered.connect(self.close_all)
self.customContextMenuRequested.connect(self.on_context_menu_requested)
self.setMovable(True)
def mouseReleaseEvent(self, event):
if event.button() == Qt.MidButton:
self.close_tab_at_pos(event.pos())
QTabBar.mouseReleaseEvent(self, event)
def on_context_menu_requested(self, pos):
self.context_pos = pos
self.menu.move(self.mapToGlobal(pos))
self.menu.show()
def close_tab_via_context(self):
self.close_tab_at_pos(self.context_pos)
def close_all(self):
for i in range(self.count()-1, -1, -1):
self.tabCloseRequested.emit(i)
def close_tab_at_pos(self, pos):
self.tabCloseRequested.emit(self.tabAt(pos))
class TreeComboBox(QComboBox):
def __init__(self, value_dict, parent=None):
super().__init__(parent)
self.tree_view = QTreeView()
self.tree_view.setHeaderHidden(True)
self.tree_view.expanded.connect(self.resize_length)
self.tree_view.collapsed.connect(self.resize_length)
self.model = QStandardItemModel()
self.insert_dict(value_dict)
self.setModel(self.model)
self.setView(self.tree_view)
self.setStyleSheet('QTreeView::item:has-children{color: black;'
'font: bold;}')
def insert_dict(self, value_dict, parent=None):
for not_selectable in value_dict:
act_item = QStandardItem(not_selectable)
act_item.setEnabled(False)
font = act_item.font()
font.setBold(True)
act_item.setFont(font)
if parent:
parent.appendRow([act_item])
else:
self.model.appendRow([act_item])
if type(value_dict[not_selectable]) == dict:
self.insert_dict(value_dict[not_selectable], act_item)
else:
for item in value_dict[not_selectable]:
act_item.appendRow([QStandardItem(item)])
def get_value(self):
value = ''
act_index = self.tree_view.selectedIndexes()[0]
act_item = act_index.model().itemFromIndex(act_index)
value += act_item.text()
while act_item.parent():
value = '%s.%s' % (act_item.parent().text(), value)
act_item = act_item.parent()
return value
def resize_length(self):
self.showPopup()