import os.path
import re
import traceback
from typing import Iterable

from yangke.common.QtImporter import QModelIndex, QMimeData
from PyQt5.QtGui import QDrag
from PyQt5.QtWidgets import QComboBox, QApplication, QLineEdit

from yangke.common.QtImporter import QDir, Qt, QRectF, QSize, QStringListModel, QStandardItemModel, QStandardItem, \
    QIcon, QColor, QBrush, QResizeEvent, QKeyEvent, QFileSystemModel, QListView, QWidget, QGridLayout, QVBoxLayout, \
    QListWidget, QListWidgetItem, QLabel, QLayout, uic, QMainWindow, QColorDialog

from UI.MainWindow_PyQt5 import Ui_MainWindow
from UI.content import Ui_Form as ContentForm
from yangke.base import yield_all_file
from yangke.common.config import logger
from yangke.common.qt import UIWidget, run_app, YkWindow, YkItem, YkFoldableWidget, YkInputPanel, YkEditor, YkConsole, \
    YkVariableWidget
from yangke.ebsilon.constant.constant import default_unit, Unit
from yangke.ebsilon.graphicsview import YkGraphicsScene, YkGraphicsView, YkGraphicsItem, CoordinateItem, SceneGridItem, \
    YkStyledItemDelegate, DisplayPanel, PipeItem, SceneFlagItem, Port
import importlib

from yangke.ebsilon.values import Values


class Content(QWidget):
    def __init__(self, tab_name):
        super(Content, self).__init__()
        self.ui: QWidget = UIWidget(ContentForm)  # 即qt designer设计的ui文件面板
        self.tab_name = tab_name
        self.setLayout(QGridLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        # noinspection all
        self.layout().addWidget(self.ui, 0, 0, 1, 1)
        self.scene = YkGraphicsScene()
        self.view = YkGraphicsView(self.scene)  # self.view.setScene(self.scene)
        self.view.setAcceptDrops(True)
        # self.view.setDragMode(ScrollHandDrag)
        self.view.setAlignment(Qt.AlignCenter)
        self.ui.layout().addWidget(self.view)


class CalResult:
    def __init__(self):
        ...


class MainWindow(YkWindow, QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.choose_component = 0
        self.setWindowTitle("热力性能建模平台")
        self.ui.btn_set_value.clicked.connect(self.set_values_panel2data)
        self.ui.btn_cancel.clicked.connect(self.cancel)
        # # self.ui下有 self.ui.info_tab用于显示组件的详细信息
        self.ui.sfv.setLayout(QVBoxLayout())  # sfv的竖直方向的SizePolicy必须是Fixed
        self.ui.res.setLayout(QVBoxLayout())
        self.ui.mat.setLayout(QVBoxLayout())

        # [<PyQt5.QtGui.QStandardItem object at 0x0227C49C11B0>, <PyQt5.QtGui.QStandardItem object at 0x0227C49C1480>]
        self.items_nav = []  # 组件的QStandardItem实例，是导航面板上的显示对象，长度与self.all_comps相同

        # 组件类对象, {'Comp1': <class 'components.Comp1.Item'>, 'Comp2': <class 'components.Comp2.Item'>}
        self.all_comps = self.load_all_comp(os.path.join(os.path.dirname(__file__), "components"))
        self.init_file_view()
        self.init_compo_view()
        self.content_widget = self.init_content_tab()
        self.update_scene_nav_view()
        self.ui.splitter_3.setStretchFactor(0, 2)
        self.ui.splitter_3.setStretchFactor(1, 4)
        self.current_comp_info_tab = "SpecificationValues"  # 表示self.ui.info_tab显示按个数据面板
        self.ui.content_tab.tabCloseRequested.connect(self.close_project)  # 使单个项目可以关闭

        if self.proj.get("DefaultUnit") is not None:  # 项目默认的单位系统不为空，则加载项目默认单位系统
            pass
        elif self.ini_info.get("DefaultUnit") is not None:  # 如果项目单位系统为空，但软件单位系统不为空，则加载软件单位系统
            self.proj["DefaultUnit"] = self.ini_info.get("DefaultUnit")
        else:  # 如果以上二者都为空，则初始化一个默认的单位系统，并保存到项目中
            self.proj["DefaultUnit"] = None
        if self.proj["DefaultUnit"] is not None:
            default_unit.set(self.proj["DefaultUnit"])

        if self.proj_file is not None and os.path.exists(self.proj_file):  # 如果有项目文件，且该文件存在
            try:
                self.open(self.proj_file)
                self.close_project(0)
            except:
                logger.warning(f"打开{self.proj_file}时出错")

        self.exec_scope = {"self": self}  # 终端代码执行时的变量作用域
        self.tv_var = YkVariableWidget()  # 固定面板-> Terminal -> 变量空间
        self.ui.verticalLayout_12.addWidget(self.tv_var)
        self.init_fixed_window()

        self.ui.info_tab.currentChanged.connect(self.info_tab_changed)
        self.status_bar_coor = QLabel('')
        self.status_bar_coor.setFixedWidth(60)
        self.statusBar1.addPermanentWidget(self.status_bar_coor)
        self.calc_result = CalResult()  # 计算结果信息，如计算是否成功，计算失败原因等

    def keyPressEvent(self, a0: QKeyEvent) -> None:
        if a0.key() == Qt.Key_F9:
            self.calculate()
        elif a0.modifiers() == Qt.ShiftModifier | Qt.ControlModifier and a0.key() == Qt.Key_AsciiTilde:
            if self.ui.dockWidget_ATW.isHidden():
                self.ui.dockWidget_ATW.show()
            else:
                self.ui.dockWidget_ATW.hide()
        super(MainWindow, self).keyPressEvent(a0)

    def close_project(self, sender=None):
        if isinstance(sender, int):
            self.ui.content_tab.removeTab(sender)
        else:
            logger.warning("非法索引！")

    def init_content_tab(self):
        if self.ui.content_tab.count() == 0:
            return self.add_content_tab1("新建项目1")
        else:
            self.content_widget = self.ui.content_tab.widget(0).ui
            self.content_widget.scene.item_changed_signal.connect(self.update_scene_nav_view)
            return self.content_widget

    def add_content_tab1(self, tab_name):
        """
        不能使用父类的add_content_tab方法。
        """
        self._content_tab = self.ui.content_tab
        if tab_name in self.get_content_tab_labels():
            self._content_tab.setCurrentIndex(self.get_content_tab_labels().index(tab_name))
            logger.debug(f"已经存在名为{tab_name}的项目")
            self.content_widget = self._content_tab.currentWidget()
        else:
            content = Content(tab_name)
            self.ui.content_tab.addTab(content, tab_name)
            self.content_widget = content
            self.content_widget.scene.setBackgroundBrush(QBrush(QColor(208, 208, 208)))
            self.content_widget.scene.item_changed_signal.connect(self.update_scene_nav_view)
        return self.content_widget

    def load_all_comp(self, path):
        """
        从yangke.ebsilon.components文件夹中加载所有的组件

        :param path:
        :return:
        """
        if hasattr(self, "all_comps") and self.all_comps is not None:
            return self.all_comps
        all_comps = {}
        for file in yield_all_file(path, ".py"):
            basename = os.path.basename(file).replace(".py", "")
            _ = importlib.import_module(f"components.{basename}")
            all_comps.update({basename: _.Item})
        return all_comps

    def init_compo_view(self):
        """
        组件导航面板，加载及显示所有组件

        :return:
        """
        list_model = QStandardItemModel()
        self.items_nav = []
        for k, comp_cls in self.all_comps.items():
            comp: YkGraphicsItem = comp_cls()
            if comp.icon is None:
                comp.generate_icon()

            item = QStandardItem(comp.icon, comp.NAME)
            item.setData({"ebs_id": comp.EBS_ID, "ebs_name": comp.EBS_NAME, "ebs_type": comp.EBS_TYPE},
                         Qt.UserRole)  # 给item添加自定义数据
            self.items_nav.append(item)
            item.setDragEnabled(True)  # 是列表中的项可以被拖动
            list_model.appendRow(item)

        # list_model.mimeData()  # 实现list_model的mimeData()使之可以
        def _mimeData(indexes: list[QModelIndex]):
            if len(indexes) <= 0:
                return 0
            data = QMimeData()
            data.setData("com_data", str(indexes[0].data(Qt.UserRole)).encode("utf8"))
            return data

        list_model.mimeData = _mimeData

        self.ui.lv_com.setItemDelegate(YkStyledItemDelegate())
        self.ui.lv_com.setSpacing(4)

        self.ui.lv_com.setModel(list_model)
        self.ui.lv_com.setViewMode(QListView.ListMode)
        self.ui.lv_com.setDragEnabled(True)

        self.ui.lv_com.clicked.connect(self.click_com_cls)

    def init_file_view(self):
        """
        文件目录面板
        :return:
        """
        model = QFileSystemModel()
        model.setRootPath(QDir.currentPath())
        self.ui.tv_file.setModel(model)

    def update_scene_nav_view(self):
        """
        场景导航面板

        :return:
        """
        scene_list_model = QStandardItemModel()  # 场景组件列表模型
        for scene_id, 设备组件 in self.content_widget.scene.items_scene_id.items():
            设备组件: YkGraphicsItem = 设备组件
            if scene_id < 0:
                continue
            icon = 设备组件.icon if hasattr(设备组件, "icon") else None
            icon = icon or 设备组件.generate_icon()
            name = 设备组件.NAME if hasattr(设备组件, "NAME") else None
            ebs_id = 设备组件.EBS_ID if hasattr(设备组件, "EBS_ID") else None
            ebs_name = 设备组件.EBS_NAME if hasattr(设备组件, "EBS_NAME") else None
            ebs_type = 设备组件.EBS_TYPE if hasattr(设备组件, "EBS_TYPE") else None

            item = QStandardItem(icon, f"{name}_{设备组件.id} / S{scene_id}")
            item.setData({
                "ebs_id": ebs_id, "ebs_name": ebs_name, "ebs_type": ebs_type, "scene_id": scene_id
            }, role=Qt.UserRole)  # 给item添加自定义数据
            scene_list_model.appendRow(item)
        # self.ui.lv_com.setItemDelegate(YkStyledItemDelegate())
        self.ui.lv_scene.setSpacing(4)

        self.ui.lv_scene.setModel(scene_list_model)
        self.ui.lv_scene.setViewMode(QListView.IconMode)
        self.ui.lv_scene.setDragEnabled(True)

        self.ui.lv_scene.clicked.connect(self.click_com_obj)

    def init_fixed_window(self):
        self.ui.Terminal.setLayout(QVBoxLayout())
        self.ui.Terminal.layout().setContentsMargins(0, 0, 0, 0)
        self.ui.terminal = YkConsole()
        self.ui.Terminal.layout().addWidget(self.ui.terminal)
        self.ui.Log.setLayout(QVBoxLayout())
        self.ui.Log.layout().setContentsMargins(0, 0, 0, 0)
        self.ui.log_editor = YkEditor()
        self.ui.Log.layout().addWidget(self.ui.log_editor)
        self.ui.terminal.deal_cmd = self.deal_cmd
        self.ui.dockWidget_ATW.hide()
        self.update_terminal_var()
        logger.add(self.ui.log_editor)  # 将日志面板加入日志类的sink中

    def update_terminal_var(self):
        """
        更新变量面板上的变量值
        """
        tv = self.tv_var  # 变量的QTreeView实例对象
        try:
            self.exec_scope.update({"items_scene_id": self.content_widget.scene.items_scene_id})
            tv.update_variable(self.exec_scope)
        except:
            self.output_log(traceback.format_exc())

    def deal_cmd(self, cmd):
        """
        处理控制台输入的命令

        :param cmd: 控制台最后一行的文本内容，也就是命令
        :return:
        """
        try:
            if cmd == "refresh" or cmd == "update":  # 更新一下当前数据
                self.update_terminal_var()
                return
            res = eval(cmd, self.exec_scope)  # eval只能执行表达式，不能是语句，如eval("x=1")会报错
            self.exec_scope.update({"_": res})
        except:
            try:
                res = exec(cmd, self.exec_scope)  # exec可以执行语句，但是执行表达式时不返回结果，如exec("2+1")返回None
            except:
                res = traceback.format_exc()
        self.update_terminal_var()
        return res

    def click_com_cls(self, item):
        """
        组件导航面板的点击响应事件

        :return:
        """
        user_info = item.data(Qt.UserRole)
        self.choose_component = user_info.get("ebs_id")

    def insert_components(self, com_name):
        """
        在当前激活的项目中插入组件，一般为text组件
        """
        if com_name == 'text':
            self.choose_component = 'text'

    def click_com_obj(self, item):
        """
        场景导航面板的点击响应事件

        :param item:
        :return:
        """
        user_info = item.data(Qt.UserRole)
        if user_info is None:
            return
        scene_id = user_info.get("scene_id")
        item: YkGraphicsItem = self.content_widget.scene.items_scene_id.get(scene_id)
        item.setFocus(Qt.MouseFocusReason)
        self.display_info(scene_id)

    def draw_content_view(self):
        if self.content_widget is None:
            return
        self.content_widget.scene.setBackgroundBrush(QBrush(QColor(0, 100, 0, 50)))

    def resizeEvent(self, a0: QResizeEvent) -> None:
        super(MainWindow, self).resizeEvent(a0)
        # try:
        #     rect = self.content_widget.rect()
        #     self.content_widget.scene.setSceneRect(QRectF(rect.x(), rect.y(), rect.width() - 24, rect.height() - 24))
        # except:
        #     pass

    def save(self):
        """
        保存项目
        :return:
        """
        self.proj = self.proj or {}
        items = self.content_widget.scene.items_scene_id
        # 将items中无法序列化的Item对象转换为字符串
        items_new = []
        try:
            for scene_id, obj in items.items():
                if scene_id <= 2 or isinstance(obj, CoordinateItem) or isinstance(obj, SceneGridItem):
                    continue  # scene_id=0和1是网格和坐标系组件
                obj: YkGraphicsItem = obj
                bak_info = {}
                if obj.EBS_ID == '-100':
                    obj: PipeItem = obj
                    bak_info.update({
                        "control_points": obj.control_points, "color": obj.color, "connect_items": obj.connect_items,
                    })
                objs_pickable = {"EBS_ID": obj.EBS_ID, "EBS_NAME": obj.EBS_NAME, "id": obj.id, "scene_id": obj.scene_id,
                                 "values": obj.values, "rotation": obj.rotation(),
                                 "scene_pos": obj.scenePos(), "height": obj.height, "width": obj.width,
                                 "ports": obj.ports, "bak_info": bak_info,
                                 }
                items_new.append(objs_pickable)
        except:
            traceback.print_exc()

        self.proj.update({'items': items_new})
        super(MainWindow, self).save()

        self.statusBar1.showMessage(f"项目保存完成")

    def get_projects(self):
        """
        获取当前已经打开的项目名称
        """
        res = [self.ui.content_tab.tabText(i) for i in range(self.ui.content_tab.count())]
        return res

    def new_project(self):
        super().new_project()
        _proj_name = "new"
        _proj_names = self.get_projects()
        if _proj_name in _proj_names:
            i = 1
            while f"new_{i}" in _proj_names:
                i = i + 1
            _proj_name = f"new_{i}"
        _ = self.add_content_tab1(_proj_name)
        self.ui.content_tab.setCurrentWidget(_)
        self._content_tab = self.ui.content_tab

    def open(self, proj=None):
        super(MainWindow, self).open(proj)
        # 添加一个新的content_tab，并切换到新添加的content_tab
        if self.proj_file is None:  # 当新建项目时，打开空项目但proj_file尚未指定
            return
        proj_name = os.path.basename(self.proj_file).split(".")[0]
        self.add_content_tab1(tab_name=proj_name)
        self.ui.content_tab.setCurrentWidget(self.content_widget)
        if self.proj is None:
            return
        # 取出项目中的组件，并逐个绘制到场景中
        proj_items = self.proj.get("items")
        if proj_items is None:
            return
        try:
            for obj in proj_items:
                EBS_ID = obj.get("EBS_ID")
                if EBS_ID == '-100':
                    comp: PipeItem = PipeItem(fluid_type=obj["EBS_NAME"])
                    comp.set_color(obj["bak_info"]["color"])
                    comp.setPos(obj.get("scene_pos"))
                    control_points = obj["bak_info"]["control_points"]
                    comp.update_line(vectors_item=control_points)
                    comp.set_connect_items(obj["bak_info"]["connect_items"])
                    comp.EBS_NAME = obj["EBS_NAME"]
                else:
                    comp: YkGraphicsItem = self.all_comps.get(f"Comp{obj.get('EBS_ID')}")()
                    comp.setPos(obj.get("scene_pos"))
                    comp.values = obj.get("values")
                    comp.ports = obj.get("ports")

                comp.setRotation(obj.get("rotation"))  # 图像的旋转角度
                self.content_widget.scene.addItem(comp, scene_id=obj.get("scene_id"), id=obj.get('id'))  # 添加combobox项时
            logger.debug(f"项目加载完成")
        except:
            traceback.print_exc()

        self.update_scene_nav_view()

    def refresh(self):
        ...

    def get_calc_items(self, sort_by='计算顺序'):
        """
        获取当前激活的项目下需要参与计算的设备组件字典
        """
        components = []
        for scene_id, com in self.content_widget.scene.items_scene_id.items():
            if com.__class__.__name__ in ["SceneGridItem", "SceneFlagItem", "CoordinateItem"]:
                ...
            else:
                components.append(com)
        if sort_by == "计算顺序":
            components.sort(key=lambda x: x.cal_order)
        return components

    def clear_results(self, components=None):
        """
        清空当前激活的项目中的计算结果数据
        """
        components = components or self.get_calc_items()
        for com in components:
            com: YkGraphicsItem = com
            for idx, port in com.ports.items():
                port: Port = port
                port.values = Values(values_steps={0: {}})  # port的初始值为空
            com.values.clear_result()  # 清空端口上初始值以后的迭代步的值
            com.calculated = False  # 重置计算状态为False

    def calculate_real_time(self):
        ...

    def calculate(self):
        """
        计算项目，计算开始时，应确保每个组件的values中的values_steps数组中已经放置了初始的参数设置，即使没有初始参数需要设置，也需要以
        空Values对象占位。

        计算过程中，需要保证时间步0只保存画面上传入的数据，中间计算结果永远不要保存到时间步0。
        计算开始时，所有的Port对象中均没有数据，Port上的数据是在事件步1由边界条件设置组件传递到Port对象上的

        :return:
        """
        cal_type, _info = self.get_calculate_info()
        if cal_type == "real_time":  # 实时计算，每隔指定时间计算一次
            time_interval = float(_info[0])
            from yangke.base import execute_function_by_interval
            execute_function_by_interval(self.calculate_real_time, minute=0, second=time_interval)
        else:
            components = self.get_calc_items()  # 所有参与计算的设备组件字典
            epochs = 100  # 计算迭代总次数
            exit_msg = "计算失败！"
            error_info = {}

            # 正式计算之前，清空所有组件的port中存储的之前的计算结果以及result中保存的计算结果
            self.clear_results(components)

            # 首先计算边界值组件，并将边界值组件的值传递给相应的端口，边界值组件必须可以独立计算，否则会直接退出计算过程
            # 边界值组件上设置的参数也是初始参数，需要传递给port的value_steps第0步
            for com in components:
                if hasattr(com, "EBS_ID") and com.EBS_ID in [1, 46]:
                    msg: str = com.calculate(step=0)
                    if msg != "done":
                        logger.debug(f"{com.scene_id}号组件未设置必要参数！--- {msg}")
                        epochs = 1  # 让整体计算不在进行，直接退出
                        break
                    com.spread_values(step=0)  # 将当前组件设置的边界或起始值扩散到与其端口上

                    com.calculate(step=1)
                    com.spread_values(step=1)  # 将当前组件设置的边界或起始值扩散到与其端口上

            for i in range(1, epochs):
                total_num = len(components)
                calculated = {}
                exit_epochs = False
                self.content_widget.scene.current_step = i  # 设置当前计算步
                for com in components:  # 不加dict，调试经常出现无法解析整形的错误
                    if com.EBS_ID in [1, 46]:  # 边界及初始条件设置组件已经计算完成
                        # 目前来讲，1和46号组件无须重复计算，但是考虑到初始值或参考值或控制值等组件，每个迭代步可能都要计算
                        # 且如果这里不计算，则边界值组件的values.value_steps只有前两步的值
                        com.calculate(step=i)
                        com.spread_values(step=i)  # 将当前组件设置的边界或起始值扩散到与其端口上
                        com.calculated = True
                        calculated.update({com.scene_id: True})
                        continue

                    # if com.calculated:  # 计算过的参数仍需计算，不影响过定义，因为过定义只在第1个时间步判断第0个时间步的值
                    #     calculated.update({scene_id: True})
                    #     continue

                    # 首先将当前面板上的数据读入设备组件的values中
                    com.before_com_calculate(step=i)
                    msg: str = com.calculate(step=i)
                    if msg is None:
                        logger.debug(f"组件{com.scene_id}的calculate方法未返回计算结果信息，请检查组件代码！")
                        return

                    msgs = msg.split("---")
                    _err = None
                    if len(msgs) == 2:
                        msg, _err = msgs[0].strip(), msgs[1].strip()
                    if msg == "done":
                        com.calculated = True
                        calculated.update({com.scene_id: True})
                    elif _err is not None and _err.strip() == "break":  # 引发计算立即退出的错误
                        error_info.update({com.scene_id: msgs})
                        logger.debug(error_info)  # 输出错误原因
                        exit_epochs = True
                        exit_msg = "计算失败！"
                        break
                    else:
                        com.calculated = False
                        if error_info.get(com.scene_id) == msg:  # 当前错误信息与上一次相同，则退出计算
                            logger.debug(error_info)
                            exit_epochs = True
                            exit_msg = "计算失败！"
                            break
                        error_info.update({com.scene_id: msg})  # 更新当前组件的错误信息
                        if len(calculated) >= total_num - 1:  # 其他都已经计算完成，只剩一个未计算，则说明缺少参数，直接退出并报错
                            logger.debug(msg)
                            exit_epochs = True
                            exit_msg = "计算失败！"
                            break
                    com.spread_values(step=i)  # 将当前组件计算出的结果扩散到与其连接的其他组件

                if len(calculated) == total_num:
                    # 如果所有组件都计算完成，则退出计算循环
                    exit_epochs = True
                    exit_msg = "计算成功"
                if exit_epochs:
                    break

            # 获取当前组件信息面板是属于哪个组件的，更新显示结果
            _ = self.ui.detail_tab.tabText(0).split("/ S")
            if len(_) > 1:
                scene_id_current = int(_[1].replace(']', '').strip()) if len(_) > 1 else None
                self.display_info(scene_id=scene_id_current, tab=self.current_comp_info_tab)

            logger.debug(exit_msg)
            self.ui.ActiveToolWindow.setCurrentWidget(self.ui.Log)  # 将FixedToolWindow切换到日志面板
            self.show_active_tool_window()

    def show_active_tool_window(self):
        if self.ui.dockWidget_ATW.isHidden():
            self.ui.dockWidget_ATW.show()

    def display_info(self, scene_id, tab=None, save_data=True):
        """
        当选中组件时，该方法用于显示组件的详细信息

        @param: scene_id : 组件在场景中的id
        @param tab: 需要显示的信息面板标签页
        """
        if save_data:
            self.set_values_panel2data()  # 先保存当前面板的值，后切换到新面板

        if scene_id is None or scene_id < 2:  # 0号组件是场景网格对象，1号组件是场景坐标系对象
            return True
        item = self.content_widget.scene.items_scene_id.get(scene_id)

        if item is None:
            return

        if tab is None:
            self.ui.detail_tab.setTabText(0, f"组件信息[{item.NAME}_{item.id} / S{item.scene_id}]")
            tab = self.ui.info_tab.tabText(self.ui.info_tab.currentIndex())

        self.show_info_tab(tab, item)

        # 更新场景组件导航面板上的组件列表
        self.update_scene_nav_view()
        idx = list(self.content_widget.scene.items_scene_id.keys()).index(scene_id) - 1  # 因为场景中有一个组件不显示
        qm_idx = self.ui.lv_scene.model().index(idx, 0)
        self.ui.lv_scene.setCurrentIndex(qm_idx)

    def show_info_tab(self, tab_name: str, device_item: YkGraphicsItem | None = None):
        """
        根据tab_items的值，构建组件信息面板，并将values中的值显示到信息面板上

        :param tab_name: 信息面板的标签页名
        :param device_item: 当面板上的combobox需要链接事件时，需要传入面板所属的设备组件Item对象，以便从Item中加载相应的事件响应方法
        :return:
        """
        if tab_name == "SpecificationValues" or tab_name == "规格值":
            tab_items = device_item.SpecificationValues or {}
            panel_info_tab, connect_slot = self._form_display_panel(tab_items, device_item)
            if len(self.ui.sfv.children()) == 1 and isinstance(self.ui.sfv.children()[0], QLayout):
                self.ui.sfv.layout().addWidget(panel_info_tab)
            else:
                self.ui.sfv.layout().removeWidget(self.ui.sfv.children()[1])
                if len(self.ui.sfv.children()) > 1:
                    # 必须强制删除一次原组件，才能彻底删除干净，replaceWidget方法也存在删除不干净的问题
                    self.ui.sfv.children()[1].deleteLater()
                self.ui.sfv.layout().addWidget(panel_info_tab)

            values = device_item.values
            _values = values.get_initial(need_unit=True)
            panel_info_tab.set_values(_values)  # 此时，combobox连接事件尚未添加，因此设置值不会触发相应事件

            # 数值设置完成后，再添加combox链接事件
            # 如果在值设置之前连接事件，则会导致多个组件切换时，动态响应参数与响应时间源不匹配的问题
            self._add_combo_box_connect(panel_info_tab, connect_slot, device_item)

            return panel_info_tab
        elif tab_name == "Results" or tab_name == "结果":
            tab_items = device_item.Result or {}
            panel_info_tab, connect_slot = self._form_display_panel(tab_items, device_item)

            if len(self.ui.res.children()) == 1 and isinstance(self.ui.res.children()[0], QLayout):
                self.ui.res.layout().addWidget(panel_info_tab)
            else:
                self.ui.res.layout().removeWidget(self.ui.res.children()[1])
                if len(self.ui.res.children()) > 1:
                    # 必须强制删除一次原组件，才能彻底删除干净，replaceWidget方法也存在删除不干净的问题
                    self.ui.res.children()[1].deleteLater()
                self.ui.res.layout().addWidget(panel_info_tab)

            values = device_item.values
            _values = values.get_results(need_unit=True)
            panel_info_tab.set_values(_values)
        elif tab_name == "材料" or tab_name == "Materials":
            pass

    def _add_combo_box_connect(self, panel_info_tab, connect_slot, device_item):
        # 处理信息面板中combobox值改变时的事件响应
        if len(connect_slot) > 0:
            _ = []
            for connect in connect_slot:
                exist = False
                # 首先合并connect_slot中var和connect相同的信号与槽
                for c in _:
                    if c["var"] == connect["var"] and c["connect"] == connect["connect"]:
                        if isinstance(c["object"], list):
                            c["object"].append(connect["object"])
                            c["object_type"].append(connect["object_type"])
                        else:
                            c["object"] = [c["object"], connect["object"]]
                            c["object_type"] = [c["object_type"], connect["object_type"]]
                        exist = True
                        break
                if not exist:
                    _.append(connect)

            for connect in _:
                connect_func = eval(f"device_item.{connect.get('connect')}")
                var = connect.get("var")
                var_item = panel_info_tab.get_child_yk_items(var)
                value_combobox = var_item.value
                if isinstance(value_combobox, QComboBox):
                    # 多个connect_func方法相同时，则连接的函数传入的参数会相同，这会导致相当于信号触发最后一次绑定的事件多次，而不是各种参数
                    # 各调用一次，需要进行处理
                    value_combobox.currentTextChanged.connect(
                        lambda: self.combox_text_changed(connect_func, connect, panel_info_tab))

    def _form_display_panel(self, tab_items, device_item):
        """
        生成详细信息面板

        :param tab_items: 面板需要显示的数据，一般为item.SpecificationValues或item.Result数据对象
        :return:
        """
        panel_sfv = DisplayPanel()
        connect_slot = []  # 信息面板中各项之间可能有combobox等事件联动，记录下来
        for 参数类别, 参数项 in tab_items.items():
            disabled = False
            panel_参数类别 = QWidget()
            panel_参数类别.setLayout(QVBoxLayout())
            panel_参数类别.layout().setContentsMargins(0, 0, 0, 0)
            panel_参数类别.layout().setSpacing(1)
            for 子参数项 in 参数项:
                size = [100, 80, 60]
                name = 子参数项[0]
                symbol = 子参数项[1]
                value = 子参数项[2]
                if len(子参数项) >= 4:
                    unit = 子参数项[3]
                else:
                    unit = None
                if isinstance(unit, dict):
                    func = unit.get("func")
                    _res = re.findall(r"(.+)[(](.*)[)]", func)[0]
                    func_name, var = _res
                    # symbol的unit需要根据func_name(var)的执行结果确定
                    _slot = {"var": var, "connect": func_name, "object": symbol, "object_type": "unit"}
                    connect_slot.append(_slot)
                    try:
                        unit = eval(f"device_item.{func_name}('{var}')")
                        unit = {"value": unit.allowed_values, "selected_val": unit.unit}
                    except:
                        unit = ["初始化时发生错误！"]
                else:
                    pass
                if len(子参数项) >= 5:
                    var4 = 子参数项[4]
                    if isinstance(var4, str):
                        disabled = True if var4 == "disabled" else False
                    elif isinstance(var4, dict):
                        if var4.get("size") is not None:
                            size = var4.get("size")
                item = YkItem(f"{name}[{symbol}]", value, unit, size=size, draggable=True)
                # if isinstance(item.value, QLineEdit):
                #     item.value.setDragEnabled(True)
                if disabled:
                    item.value.setEnabled(False)
                panel_参数类别.layout().addWidget(item)
            panel_fold = YkFoldableWidget(参数类别, panel_参数类别)
            panel_sfv.layout().addWidget(panel_fold)

        return panel_sfv, connect_slot

    def combox_text_changed(self, func, connect_info, panel_sfv):
        """
        组件信息面板上，combobox组件值改变时，触发的事件处理

        :param func:
        :param connect_info:
        :return:
        """
        # noinspection all
        var, func = connect_info.get("var"), func  # 这两个参数在eval语句中使用
        self.set_values_panel2data()  # 先将当前改变后的值写入self.values中，再调用dynamic_unit方法
        res: Unit = eval(f"func(var)")
        object_yk_items = connect_info.get("object")
        object_types = connect_info.get("object_type")
        if not isinstance(object_yk_items, list):
            object_yk_items = [object_yk_items]
            object_types = [object_types]

        for object_item, object_type in zip(object_yk_items, object_types):
            des = panel_sfv.get_child_yk_items(object_item)  # 设置的目标YkItem对象
            if object_type == "unit":
                des.unit.setModel(QStringListModel(res.allowed_values))
                des.set_unit(res.unit)

    def set_values_panel2data(self):
        """
        将面板上的数值赋值给组件的values对象，点击【确定】按钮时会执行该方法
        """
        # 获取到当前正在设置的组件，根据场景id
        _ = self.ui.detail_tab.tabText(0).split("/ S")
        if len(_) == 1:
            return
        scene_id = int(_[1].replace(']', '').strip())
        item = self.content_widget.scene.items_scene_id.get(scene_id)
        if item is None:  # 说明当前的组件已经被删除，因为显示下一个信息面板时会首先保存上一个信息，所以该方法可能在组件已删除的情况下被调用
            return
        # 获取当前设置的面板名
        tab_name = self.ui.info_tab.tabText(self.ui.info_tab.currentIndex())
        if tab_name == "规格值" or tab_name == "SpecificationValues":
            _layout = self.ui.sfv.layout()  # self.ui.sfv.layout()没有子项
            if len(self.ui.sfv.children()) > 1 and isinstance(self.ui.sfv.children()[1], DisplayPanel):
                panel: DisplayPanel = self.ui.sfv.children()[1]  # 当前sfv上显示的面板
                values: dict = panel.get_values()  # 获取面板上的所有值，包括空值
                item.values.set_values(0, values, set_none=True)  # 将面板上的值赋值给设备组件，包括空值，面板上获取的值都是初始状态的值
                item.after_values_updated()  # 值设置完成后进行的操作
        elif tab_name == "结果":
            pass  # 结果不保存至当前设备组件，结果数据只能是计算出来的

    def set_values_data2panel(self):
        """
        将组件中的参数数值显示到面板上
        """
        # 获取到当前正在设置的组件，根据场景id
        scene_id = int(self.ui.detail_tab.tabText(0).split("/ S")[1].replace(']', '').strip())
        item = self.content_widget.scene.items_scene_id.get(scene_id)

        # 获取当前设置的面板名
        tab_name = self.ui.info_tab.tabText(self.ui.info_tab.currentIndex())
        if tab_name == "规格值" or tab_name == "SpecificationValues":
            panel: DisplayPanel = item.SpecificationValuesPanel
            children = self.ui.sfv.children()
            self.ui.sfv.layout().replaceWidget(children[1], panel)
            values = panel.set_values(item.values)

    def cancel(self):
        ...

    def info_tab_changed(self, idx):
        """


        :param idx:
        :return:
        """
        # 获取到当前正在设置的组件，根据场景id
        _ = self.ui.detail_tab.tabText(0).split("/ S")
        if len(_) == 1:
            return
        scene_id = int(_[1].replace(']', '').strip())
        # 获取到当前的信息面板名，如 "规格值"、"结果"
        tab_name = self.ui.info_tab.tabText(idx)
        self.display_info(scene_id=scene_id, tab=tab_name, save_data=False)  # 切换面板触发的显示信息不保存数据

    def get_color(self):
        color = QColorDialog.getColor(Qt.white)
        if color.isValid():
            self.content_widget.scene.setBackgroundBrush(QBrush(color))

    def clear_log_console(self):
        log_editor: YkEditor = self.ui.log_editor
        log_editor.setText("")

    def get_calculate_info(self):
        """
        获取计算设置信息
        """
        settings = self.proj.get('settings')
        if settings is None:
            return None, (None, )
        else:
            cal_type = settings.get('cal_type')
            cal_time_interval = settings.get('cal_time_interval')
        return cal_type, (cal_time_interval,)

    def set_calculate(self):
        """
        计算设置，点击后默认10s计算一次
        {"settings":
            {
                "cal_type": "real_time",
                "cal_time_interval": 10
            }
        }
        """
        settings = self.proj.get('settings')
        if settings is None:
            self.proj.update({"settings": {"cal_type": "real_time", "cal_time_interval": 10}})
        else:
            if settings.get("cal_type") == "real_time":
                self.proj.update({"settings": {"cal_type": "normal"}})
            else:
                self.proj.update({"settings": {"cal_type": "real_time", "cal_time_interval": 10}})


if __name__ == "__main__":
    run_app(MainWindow)
