From fc697effc94c4f4e4a63764c1bff4cde6c94cee3 Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 31 May 2023 14:03:57 -0400 Subject: [PATCH 1/4] _SearchInput subclass How this works: - we `bind_dataset` to search input box. - when a value is changed, it updates a stringvar, dataset.search_string - and calls update_selectors(key) - table_values is setup to filter rows by search_string if its set - and finally, any call to @current_index.setter sets search_string to "", which also clears the input box. --- pysimplesql/pysimplesql.py | 81 ++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/pysimplesql/pysimplesql.py b/pysimplesql/pysimplesql.py index 72933957..8c8df237 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): @@ -663,6 +672,9 @@ def current_index(self): @current_index.setter # Keeps the current_index in bounds def current_index(self, val: int): + # reset search string + self.search_string = "" + if val > self.row_count - 1: self._current_index = self.row_count - 1 elif val < 0: @@ -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( @@ -2448,6 +2464,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 +3634,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 +4249,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 = [] @@ -5470,6 +5500,43 @@ 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 + 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 + return + + def _autocomplete_combo(widget, completion_list, delta=0): """Perform autocompletion on a Combobox widget based on the current input.""" if delta: @@ -6370,7 +6437,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 +6454,7 @@ def actions( ] else: layout += [ - _EnhancedInput( + _SearchInput( "", key=keygen.get(f"{key}search_input"), size=search_size ), sg.B( From e965cf34be8cb4e4dbb47401f678d4d6e533e5ff Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 31 May 2023 14:11:34 -0400 Subject: [PATCH 2/4] LazyTable fix --- pysimplesql/pysimplesql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysimplesql/pysimplesql.py b/pysimplesql/pysimplesql.py index 8c8df237..205eacc7 100644 --- a/pysimplesql/pysimplesql.py +++ b/pysimplesql/pysimplesql.py @@ -5270,7 +5270,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] From e6b1f156cf5706f7e3d7abcae3d2eaab2001025f Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 31 May 2023 14:29:07 -0400 Subject: [PATCH 3/4] small fix --- pysimplesql/pysimplesql.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pysimplesql/pysimplesql.py b/pysimplesql/pysimplesql.py index 205eacc7..8fd92ebd 100644 --- a/pysimplesql/pysimplesql.py +++ b/pysimplesql/pysimplesql.py @@ -5511,8 +5511,12 @@ def _add_binds(self): def on_key_release(event): # update selectors after each key-release - self.search_string.set(self.get()) - self.dataset.frm.update_selectors(self.dataset.key) + 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, "+" @@ -5534,7 +5538,6 @@ def _on_search_string_change(self, *args): self.widget.insert(0, self.placeholder_text) self.widget.config(fg=self.placeholder_color, font=self.placeholder_font) self.active_placeholder = True - return def _autocomplete_combo(widget, completion_list, delta=0): From 52236fab38a7c21356a0e6640f7efedfa4f162b0 Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 31 May 2023 15:03:18 -0400 Subject: [PATCH 4/4] Keep search_string persistent Resets when you insert virtual row, save a record, or requery --- pysimplesql/pysimplesql.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pysimplesql/pysimplesql.py b/pysimplesql/pysimplesql.py index 8fd92ebd..3e5952c9 100644 --- a/pysimplesql/pysimplesql.py +++ b/pysimplesql/pysimplesql.py @@ -672,9 +672,6 @@ def current_index(self): @current_index.setter # Keeps the current_index in bounds def current_index(self, val: int): - # reset search string - self.search_string = "" - if val > self.row_count - 1: self._current_index = self.row_count - 1 elif val < 0: @@ -1180,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, @@ -1786,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 @@ -1946,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: