diff --git a/pysimplesql/pysimplesql.py b/pysimplesql/pysimplesql.py index 72933957..3e5952c9 100644 --- a/pysimplesql/pysimplesql.py +++ b/pysimplesql/pysimplesql.py @@ -615,6 +615,7 @@ def __init__( self.column_info: ColumnInfo # ColumnInfo collection self.rows: Union[pd.DataFrame, None] = None self.search_order: List[str] = [] + self._search_string: tk.StringVar = tk.StringVar() self.selector: List[str] = [] self.callbacks: CallbacksDict = {} self.transform: Optional[Callable[[pd.DataFrame, int], None]] = None @@ -655,6 +656,14 @@ def __setitem__(self, column, value: Union[str, int]) -> None: """ self.set_current(column, value) + @property + def search_string(self): + return self._search_string.get() + + @search_string.setter + def search_string(self, val: str): + self._search_string.set(val) + # Make current_index a property so that bounds can be respected @property def current_index(self): @@ -1168,6 +1177,9 @@ def requery( lambda x: x.rstrip() if isinstance(x, str) else x ) + # reset search string + self.search_string = "" + if select_first: self.first( update_elements=update_elements, @@ -1572,7 +1584,7 @@ def set_by_pk( idx = [ i for i, value in enumerate(self.rows[self.pk_column]) if value == pk ] - except IndexError: + except (IndexError, KeyError): idx = None logger.debug("Error finding pk!") @@ -1677,10 +1689,14 @@ def get_current_row(self) -> Union[pd.Series, None]: if not self.rows.empty: # force the current_index to be in bounds! # For child reparenting - self.current_index = self.current_index + idx = ( + self.current_index + if self.current_index < len(self.rows) + else len(self.rows) + ) # make sure to return as python type - return self.rows.astype("O").iloc[self.current_index] + return self.rows.astype("O").iloc[idx] return None def add_selector( @@ -1770,6 +1786,9 @@ def insert_record( # Insert the new values using DataSet.insert_row(), # marking the new row as virtual self.insert_row(new_values) + + # reset search string + self.search_string = "" # and move to the new record # do this in insert_record, because possibly current_index is already 0 @@ -1930,6 +1949,9 @@ def save_record( for col, value in changed_row_dict.items() if self.column_info[col] and not self.column_info[col]["generated"] } + + # reset search string + self.search_string = "" # Save or Insert the record as needed if keyed_queries is not None: @@ -2448,6 +2470,16 @@ def table_values( # we only want transform col once break + if self.search_string not in ["", None]: + # Generate the mask dynamically + masks = [ + rows[col].astype(str).str.contains(self.search_string, case=False) + for col in self.search_order + ] + mask_pd = pd.concat(masks, axis=1).any(axis=1) + # Apply the mask to filter the DataFrame + rows = rows[mask_pd] + # transform bool if themepack.display_boolean_as_checkbox: bool_columns = [ @@ -3608,6 +3640,7 @@ def auto_map_events(self, win: sg.Window) -> None: placeholder=lang.search_placeholder, color=themepack.placeholder_color, ) + self.window[search_box].bind_dataset(self[data_key]) # elif event_type==EVENT_SEARCH_DB: elif event_type == EVENT_QUICK_EDIT: referring_table = table @@ -4222,8 +4255,11 @@ def update_selectors( found = False if len(values): # set to index by pk - index = [[v.pk for v in values].index(pk)] - found = True + try: + index = [[v.pk for v in values].index(pk)] + found = True + except ValueError: + index = [] else: # if empty index = [] @@ -5240,7 +5276,7 @@ def SelectedRows(self): This property assumes that the LazyTable is using a primary key (pk) value to uniquely identify rows in the data list. """ - if self.data: + if self.data and self.widget.selection(): index = [ [v.pk for v in self.data].index( [int(x) for x in self.widget.selection()][0] @@ -5470,6 +5506,46 @@ def on_focusout(event): self.binds[""] = widget.bind("", on_focusout, "+") +class _SearchInput(_EnhancedInput): + def __init__(self, *args, **kwargs): + self.dataset = None + self.search_string = None # Track the StringVar + super().__init__(*args, **kwargs) + + def _add_binds(self): + super()._add_binds() # Call the parent method to maintain existing binds + + def on_key_release(event): + # update selectors after each key-release + non_keys = self._non_keys.copy() + non_keys.remove("BackSpace") + non_keys.remove("Delete") + if event.keysym not in non_keys: + self.search_string.set(self.get()) + self.dataset.frm.update_selectors(self.dataset.key) + + self.binds[""] = self.widget.bind( + "", on_key_release, "+" + ) + + def bind_dataset(self, dataset): + self.dataset = dataset + self.search_string = dataset._search_string + self.search_string.trace_add("write", self._on_search_string_change) + + def _on_search_string_change(self, *args): + if ( + not self.active_placeholder + and self.get() != self.search_string.get() + and self.search_string.get() == "" # noqa PLC1901 + ): + # reinsert placeholder if DataSet.search_string == "" + self.widget.delete(0, "end") + self.widget.insert(0, self.placeholder_text) + self.widget.config(fg=self.placeholder_color, font=self.placeholder_font) + self.active_placeholder = True + + def _autocomplete_combo(widget, completion_list, delta=0): """Perform autocompletion on a Combobox widget based on the current input.""" if delta: @@ -6370,7 +6446,7 @@ def actions( } if type(themepack.search) is bytes: layout += [ - _EnhancedInput( + _SearchInput( "", key=keygen.get(f"{key}search_input"), size=search_size ), sg.B( @@ -6387,7 +6463,7 @@ def actions( ] else: layout += [ - _EnhancedInput( + _SearchInput( "", key=keygen.get(f"{key}search_input"), size=search_size ), sg.B(