1313from dataclasses import dataclass , field
1414from typing import Any , cast
1515
16- from mhfu_port .behaviour import Param
16+ from mhfu_port .behaviour import ID , NAMES , Param
1717from PySide6 .QtCore import QObject , QSignalBlocker , Signal
1818from PySide6 .QtWidgets import (
1919 QComboBox ,
3030from mhfu_studio .ui import kit
3131
3232Option = tuple [str , object ]
33+ NUMBERS = ("int" , "float" )
34+ #: a multi-select over the options
35+ PICKS = ("mains" , "sides" )
3336NONE = "none"
3437#: where a number field with no bound ends
3538_FAR = 10 ** 9
@@ -44,6 +47,9 @@ class ParamControl(QObject):
4447 def __init__ (self , param : Param | None , options : Sequence [Option ] = ()) -> None :
4548 super ().__init__ ()
4649 self .optional = param is not None and param .optional
50+ self ._names = param is not None and param .type in NAMES
51+ #: a name combo's committed text; what its edit box shows may be ahead of it
52+ self ._held = ""
4753 self ._vals : list [object ] = []
4854 self ._opts : list [Option ] = []
4955 self ._built = False
@@ -59,9 +65,9 @@ def _make(self, param: Param | None) -> QWidget:
5965 line .setMinimumWidth (160 )
6066 line .editingFinished .connect (self ._changed )
6167 return line
62- if param .type in ( "int" , "float" ) :
68+ if param .type in NUMBERS :
6369 return self ._number (param )
64- if param .type == "mains" :
70+ if param .type in PICKS :
6571 button = QToolButton ()
6672 button .setPopupMode (QToolButton .ToolButtonPopupMode .InstantPopup )
6773 button .setMinimumWidth (120 )
@@ -71,7 +77,18 @@ def _make(self, param: Param | None) -> QWidget:
7177 return button
7278 combo = QComboBox ()
7379 combo .setMinimumWidth (120 )
74- combo .currentIndexChanged .connect (self ._changed )
80+ if param .type in NAMES :
81+ combo .setEditable (True )
82+ combo .setInsertPolicy (QComboBox .InsertPolicy .NoInsert )
83+ combo .setCompleter (cast (Any , None )) # no inline completion: a name may start another
84+ combo .currentIndexChanged .connect (self ._commit )
85+ edit = combo .lineEdit ()
86+ if edit is not None :
87+ edit .editingFinished .connect (self ._commit )
88+ elif param .type in ("choice" , "part" ):
89+ combo .currentIndexChanged .connect (self ._changed )
90+ else :
91+ raise ValueError (f"parameter { param .name !r} : unknown type { param .type !r} " )
7592 return combo
7693
7794 def _number (self , param : Param ) -> QWidget :
@@ -105,6 +122,8 @@ def get_value(self) -> object:
105122 return None
106123 return int (ctl .value ()) if isinstance (ctl , QSpinBox ) else float (ctl .value ())
107124 if isinstance (ctl , QComboBox ):
125+ if self ._names :
126+ return ctl .currentText ()
108127 return (
109128 self ._vals [ctl .currentIndex ()]
110129 if 0 <= ctl .currentIndex () < len (self ._vals )
@@ -121,6 +140,12 @@ def set_value(self, value: object) -> None:
121140 ctl .setText ("" if value is None else str (value ))
122141 elif isinstance (ctl , QSpinBox | QDoubleSpinBox ):
123142 ctl .setValue (cast (Any , ctl .minimum () if value is None else value ))
143+ elif isinstance (ctl , QComboBox ) and self ._names :
144+ text = "" if value is None else str (value )
145+ ctl .setCurrentIndex (ctl .findText (text )) # -1 clears the edit box
146+ if ctl .currentText () != text : # a name the options lack still shows
147+ ctl .setEditText (text )
148+ self ._held = text
124149 elif isinstance (ctl , QComboBox ):
125150 if value not in self ._vals : # a value the options lack still shows
126151 self ._vals .append (value )
@@ -149,9 +174,13 @@ def set_options(self, options: Sequence[Option]) -> None:
149174 built = self ._built
150175 if built and list (options ) == self ._opts :
151176 return
152- keep = self .get_value () if built else None
177+ keep = ( self ._held if self . _names else self . get_value () ) if built else None
153178 self ._built , self ._opts = True , list (options )
154- if isinstance (ctl , QComboBox ):
179+ if isinstance (ctl , QComboBox ) and self ._names :
180+ with QSignalBlocker (ctl ):
181+ ctl .clear ()
182+ ctl .addItems ([str (v ) for _ , v in self ._opts ])
183+ elif isinstance (ctl , QComboBox ):
155184 none = self .optional and all (v is not None for _ , v in self ._opts )
156185 self ._vals = ([None ] if none else []) + [v for _ , v in self ._opts ]
157186 with QSignalBlocker (ctl ):
@@ -179,6 +208,17 @@ def _summary(self) -> None:
179208 text = ", " .join (names ) if 0 < len (names ) <= 2 else f"{ len (names )} picked"
180209 cast (QToolButton , self .ctl ).setText (text if names else (NONE if self .optional else "-" ))
181210
211+ def _commit (self , * _ : object ) -> None :
212+ """A name combo's pick or typed text: a name that is not one is put back, unheard."""
213+ text = cast (QComboBox , self .ctl ).currentText ().strip ()
214+ if text == self ._held :
215+ return
216+ if ID .fullmatch (text ) is None :
217+ self .set_value (self ._held )
218+ return
219+ self .set_value (text )
220+ self ._changed ()
221+
182222 def _changed (self , * _ : object ) -> None :
183223 if isinstance (self .ctl , QToolButton ):
184224 self ._summary ()
0 commit comments