RubendeBruin / DAVE

DAVE - Design Analysis Visualization and Engineering
Mozilla Public License 2.0
29 stars 7 forks source link

NodeSelector : User interface #162

Open RubendeBruin opened 2 months ago

RubendeBruin commented 2 months ago

The NodeSelector dataclass

@dataclass
class NodeSelector:
    name: str or None = None
    kind: type or None = None
    kind_not: type or None = None
    tag: str or None = None
    managed: bool or None = None
    family_of: Node or None = None
    on: Node or None = None
    core_connected_to: Node or None = None
    fixed_to: Node or str or None = None

provides a powerfull way to filter or select nodes. But it needs to be exposed to the user as well such that it can be used in the GUI.

Use For the filter above the tree In report sections to select nodes To filter visuals in the viewport To filter nodes in the viewport selection

How Outlook has a similar feature to filter messages, there the From: syntax is used.

type: Point, Frame name: F --> name = "F" F --> name = "F name: "F" --> name = "F"

interpretation:

parsing values

RubendeBruin commented 2 months ago

Default field if no argument is specified (no :)

The default would really depend on the context: selections in gui: type reporting: tag or name

but we can add shorthands:

k: --> type (not t as that is used for tag) n: --> name t: --> tag

For clarity the fuzzy auto-completed version of the user input should be shown in the Gui as feedback (and maybe as auto-complete as well ???)

For auto-completion we need a scene (for the node names and possible parents and such)

RubendeBruin commented 3 weeks ago

WIP in scratch 52

from dataclasses import fields

from DAVE import *
from DAVE.settings import DAVE_ADDITIONAL_RUNTIME_MODULES
from DAVE.tools import MostLikelyMatch

def make_node_selector_query(user_string, s) -> tuple[str,list[str]]:

    # user_string = "type: Poi"
    errors = []

    # cut into blocks
    blocks = user_string.replace(':', ':,').replace(' ',',').split(',')

    # remove empty blocks
    blocks = [b for b in blocks if b]

    if not blocks:
        return '', ['No input']

    # parse values into a dict with keys, where keys are identified by the string ending with :
    # first block has to be a key
    if blocks[0][-1] != ':':
        blocks.insert(0, 'type:')

    raw_tokens = dict()
    values = None
    for block in blocks:
        if block[-1] == ':':

            if values:
                raw_tokens[key[:-1]] = values

            key = block
            values = []
            continue

        values.append(block)  # first block as alwyas a key, so we can append to values

    if key not in raw_tokens.keys():
        raw_tokens[key[:-1]] = values

    print(raw_tokens)

    # clean up the keys
    # subsitutions
    substitutions = {'type': 'kind',
                     'kind': 'type',
                     'k': 'kind',
                     't' : 'tag',
                     'tag' : 'tag',
                     'tags' : 'tag',
                     'name': 'name',
                     'names': 'name',
                     'n': 'name',
                     }

    # get all field names from the NodeSelector data-class
    for f in fields(NodeSelector):
        name = f.name
        substitutions[name] = name

    # apply substitutions
    fields_clean = dict()

    for key, value in raw_tokens.items():
        if key in substitutions.keys():
            fields_clean[substitutions[key]] = value

        else:
            errors.append(f'Unknown key: {key}')

    print(fields_clean)

    node_types = []
    for string, kind in DAVE_ADDITIONAL_RUNTIME_MODULES.items():
        if issubclass(kind, Node):
            node_types.append(string)

    node_names = s.node_names

    # found node-type:
    print(node_types)

    # Now fill in the values using the intended types
    all_clean = dict()

    for key, value in fields_clean.items():
        values = []

        if not value:
            pass

        elif key == 'kind' or key == 'kind_not':
            for v in value:
                if v in node_types:
                    values.append(v)
                else:
                    values.append(MostLikelyMatch(v, node_types))

        elif key == 'name':
            if len(values) > 1:
                errors.append(f'Only one name allowed, ignoring {values[1:]}')
            values.append(value[0])  # can only have one name

        elif key == 'tag':
            values = value

        elif key == 'managed':
            if len(value) > 1:
                errors.append(f'Only one managed allowed, ignoring {values[1:]}')

            if value[0][0].lower() in ['t','y']:
                values = True
            else:
                values = False

        elif key in ['on','family_of','core_connected_to','fixed_to']:

            if len(value) > 1:
                errors.append(f'Only one node allowed for {key}, ignoring {values[1:]}')

            v = value[0]
            if v in node_names:
                values.append(v)
            else:
                errors.append(f'No node with name {v} - using most likely match instead')
                values.append(MostLikelyMatch(v, node_names))

        all_clean[key] = values

    print(all_clean)
    print(errors)

    # make a query
    q = ""
    for key, value in all_clean.items():
        q += key + ': '

        if isinstance(value, (str, bool)):
            q += str(value) + ' '

        for v in value:
            q += v + ' '

    return q, errors

s = Scene("res: cheetah with crane and 4p block.dave")

make_node_selector_query("Cheetah on:",s)

# using pyside6, make a window with a text box and a label
# when the text box is changed, the label should show the result of make_node_selector_query
# hint: use the signal textChanged
# hint: use the slot make_node_selector_query
# hint: use the signal textChanged.connect(make_node_selector_query)
# hint: use a QVBoxLayout with a QLineEdit and a QLabel
# hint: use the QVBoxLayout.addWidget method to add the QLineEdit and QLabel to the layout
# hint: use the QWidget.setLayout method to set the layout of the window
# hint: use the QWidget.show method to show the window
# hint: use the QApplication.exec method to start the event loop

from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QLabel, QCompleter
from PySide6.QtCore import QStringListModel

app = QApplication([])

window = QWidget()
layout = QVBoxLayout()

text = QLineEdit()
label = QLabel()
label2 = QLabel()

# Create a QCompleter
completer = QCompleter()

# Set the model for the completer
model = QStringListModel()
completer.setModel(model)

# Set the completer for the QLineEdit
text.setCompleter(completer)

# Set the completion string
model.setStringList(["Your completion string"])

# completer.setFilterMode(Qt.MatchContains)
completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)

text.setText('Cheetah')

def update():
    t = text.text()
    r, _  = make_node_selector_query(t, s)
    model.setStringList([str(r)])
    label.setText(str(r))
    label2.setText(str(_))

    nodes = s.nodes_where(r)

    label3.setText()

text.textChanged.connect(update)

layout.addWidget(label2)
layout.addWidget(text)
layout.addWidget(label)

window.setLayout(layout)
window.show()

app.exec()