I can add a new column and a new row
This commit is contained in:
@@ -151,18 +151,6 @@ The DataGrid automatically detects column types from pandas dtypes:
|
|||||||
| `datetime64` | Datetime | Formatted date |
|
| `datetime64` | Datetime | Formatted date |
|
||||||
| `object`, others | Text | Left-aligned, truncated |
|
| `object`, others | Text | Left-aligned, truncated |
|
||||||
|
|
||||||
### Row Index Column
|
|
||||||
|
|
||||||
By default, the DataGrid displays a row index column on the left. This can be useful for identifying rows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Row index is enabled by default
|
|
||||||
grid._state.row_index = True
|
|
||||||
|
|
||||||
# To disable the row index column
|
|
||||||
grid._state.row_index = False
|
|
||||||
grid.init_from_dataframe(df)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Column Features
|
## Column Features
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
margin: 4px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
@@ -45,6 +44,40 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dt2-last-cell {
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt2-add-column {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dt2-add-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
width: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dt2-row-selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Cell content types */
|
/* Cell content types */
|
||||||
.dt2-cell-content-text {
|
.dt2-cell-content-text {
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEdito
|
|||||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||||
from myfasthtml.controls.Keyboard import Keyboard
|
from myfasthtml.controls.Keyboard import Keyboard
|
||||||
from myfasthtml.controls.Mouse import Mouse
|
|
||||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||||
from myfasthtml.controls.Query import Query, QUERY_FILTER
|
from myfasthtml.controls.Query import Query, QUERY_FILTER
|
||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||||
from myfasthtml.controls.helpers import mk, column_type_defaults
|
from myfasthtml.controls.helpers import mk, column_type_defaults
|
||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID, \
|
||||||
|
ROW_SELECTION_ID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.dsls import DslsManager
|
from myfasthtml.core.dsls import DslsManager
|
||||||
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine
|
from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine
|
||||||
@@ -36,7 +36,7 @@ from myfasthtml.core.optimized_ft import OptimizedDiv
|
|||||||
from myfasthtml.core.utils import make_safe_id, merge_classes, make_unique_safe_id, is_null
|
from myfasthtml.core.utils import make_safe_id, merge_classes, make_unique_safe_id, is_null
|
||||||
from myfasthtml.icons.carbon import row, column, grid
|
from myfasthtml.icons.carbon import row, column, grid
|
||||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
||||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular
|
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular, add12_filled
|
||||||
from myfasthtml.icons.fluent_p3 import text_edit_style20_regular
|
from myfasthtml.icons.fluent_p3 import text_edit_style20_regular
|
||||||
|
|
||||||
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
||||||
@@ -70,7 +70,6 @@ class DatagridState(DbObject):
|
|||||||
super().__init__(owner, name=f"{owner.get_id()}#state", save_state=save_state)
|
super().__init__(owner, name=f"{owner.get_id()}#state", save_state=save_state)
|
||||||
self.sidebar_visible: bool = False
|
self.sidebar_visible: bool = False
|
||||||
self.selected_view: str = None
|
self.selected_view: str = None
|
||||||
self.row_index: bool = True
|
|
||||||
self.columns: list[DataGridColumnState] = []
|
self.columns: list[DataGridColumnState] = []
|
||||||
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
|
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
|
||||||
self.headers: list[DataGridHeaderFooterConf] = []
|
self.headers: list[DataGridHeaderFooterConf] = []
|
||||||
@@ -99,12 +98,14 @@ class DatagridSettings(DbObject):
|
|||||||
self.open_settings_visible: bool = True
|
self.open_settings_visible: bool = True
|
||||||
self.text_size: str = "sm"
|
self.text_size: str = "sm"
|
||||||
self.enable_formatting: bool = True
|
self.enable_formatting: bool = True
|
||||||
|
self.enable_edition: bool = True
|
||||||
|
|
||||||
|
|
||||||
class DatagridStore(DbObject):
|
class DatagridStore(DbObject):
|
||||||
"""
|
"""
|
||||||
Store Dataframes
|
Store Dataframes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, owner, save_state):
|
def __init__(self, owner, save_state):
|
||||||
with self.initializing():
|
with self.initializing():
|
||||||
super().__init__(owner, name=f"{owner.get_id()}#df", save_state=save_state)
|
super().__init__(owner, name=f"{owner.get_id()}#df", save_state=save_state)
|
||||||
@@ -187,16 +188,35 @@ class Commands(BaseCommands):
|
|||||||
return Command("ToggleColumnsManager",
|
return Command("ToggleColumnsManager",
|
||||||
"Hide/Show Columns Manager",
|
"Hide/Show Columns Manager",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.toggle_columns_manager
|
self._owner.handle_toggle_columns_manager
|
||||||
|
).htmx(target=None)
|
||||||
|
|
||||||
|
def toggle_new_column_editor(self):
|
||||||
|
return Command("ToggleNewColumnEditor",
|
||||||
|
"Add New Column",
|
||||||
|
self._owner,
|
||||||
|
self._owner.handle_toggle_new_column_editor,
|
||||||
|
icon=add12_filled,
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
def toggle_formatting_editor(self):
|
def toggle_formatting_editor(self):
|
||||||
return Command("ToggleFormattingEditor",
|
return Command("ToggleFormattingEditor",
|
||||||
"Hide/Show Formatting Editor",
|
"Hide/Show Formatting Editor",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.toggle_formatting_editor
|
self._owner.handle_toggle_formatting_editor
|
||||||
).htmx(target=None)
|
).htmx(target=None)
|
||||||
|
|
||||||
|
def add_row(self):
|
||||||
|
return Command("AddRow",
|
||||||
|
"Add row",
|
||||||
|
self._owner,
|
||||||
|
self._owner.handle_add_row,
|
||||||
|
icon=add12_filled,
|
||||||
|
).htmx(target=f"#tr_{self._id}-last",
|
||||||
|
swap="outerHTML",
|
||||||
|
auto_swap_oob=False
|
||||||
|
)
|
||||||
|
|
||||||
def on_column_changed(self):
|
def on_column_changed(self):
|
||||||
return Command("OnColumnChanged",
|
return Command("OnColumnChanged",
|
||||||
"Column definition changed",
|
"Column definition changed",
|
||||||
@@ -213,6 +233,7 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
self._state = DatagridState(self, save_state=self._settings.save_state)
|
||||||
self._df_store = DatagridStore(self, save_state=self._settings.save_state)
|
self._df_store = DatagridStore(self, save_state=self._settings.save_state)
|
||||||
self._formatting_engine = FormattingEngine()
|
self._formatting_engine = FormattingEngine()
|
||||||
|
self._columns = None
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self.init_from_dataframe(self._df_store.ne_df, init_state=False) # data comes from DatagridStore
|
self.init_from_dataframe(self._df_store.ne_df, init_state=False) # data comes from DatagridStore
|
||||||
|
|
||||||
@@ -220,6 +241,7 @@ class DataGrid(MultipleInstance):
|
|||||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
||||||
self._panel.set_side_visible("right", False) # the right Panel always starts closed
|
self._panel.set_side_visible("right", False) # the right Panel always starts closed
|
||||||
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
|
self.bind_command("ToggleColumnsManager", self._panel.commands.toggle_side("right"))
|
||||||
|
self.bind_command("ToggleNewColumnEditor", self._panel.commands.set_side_visible("right", True))
|
||||||
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
|
self.bind_command("ToggleFormattingEditor", self._panel.commands.toggle_side("right"))
|
||||||
|
|
||||||
# add Query
|
# add Query
|
||||||
@@ -421,6 +443,14 @@ class DataGrid(MultipleInstance):
|
|||||||
# Get global tables formatting from manager
|
# Get global tables formatting from manager
|
||||||
return self._parent.all_tables_formats
|
return self._parent.all_tables_formats
|
||||||
|
|
||||||
|
def _init_columns(self):
|
||||||
|
self._columns = self._state.columns.copy()
|
||||||
|
if self._settings.enable_edition:
|
||||||
|
self._columns.insert(0, DataGridColumnState(ROW_SELECTION_ID,
|
||||||
|
-1,
|
||||||
|
"",
|
||||||
|
ColumnType.RowSelection_))
|
||||||
|
|
||||||
def init_from_dataframe(self, df, init_state=True):
|
def init_from_dataframe(self, df, init_state=True):
|
||||||
|
|
||||||
def _get_column_type(dtype):
|
def _get_column_type(dtype):
|
||||||
@@ -441,8 +471,6 @@ class DataGrid(MultipleInstance):
|
|||||||
col_id,
|
col_id,
|
||||||
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
||||||
for col_index, col_id in enumerate(_df.columns)]
|
for col_index, col_id in enumerate(_df.columns)]
|
||||||
if self._state.row_index:
|
|
||||||
columns.insert(0, DataGridColumnState(make_safe_id(ROW_INDEX_ID), -1, " ", ColumnType.RowIndex))
|
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
@@ -494,6 +522,8 @@ class DataGrid(MultipleInstance):
|
|||||||
self._df_store.save()
|
self._df_store.save()
|
||||||
self._state.rows = [] # sparse: only rows with non-default state are stored
|
self._state.rows = [] # sparse: only rows with non-default state are stored
|
||||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
||||||
|
|
||||||
|
self._init_columns()
|
||||||
self._df_store.ns_fast_access = _init_fast_access(self._df)
|
self._df_store.ns_fast_access = _init_fast_access(self._df)
|
||||||
self._df_store.ns_row_data = _init_row_data(self._df)
|
self._df_store.ns_row_data = _init_row_data(self._df)
|
||||||
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
|
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||||
@@ -532,15 +562,75 @@ class DataGrid(MultipleInstance):
|
|||||||
row_dict[col_def.col_id] = default_value
|
row_dict[col_def.col_id] = default_value
|
||||||
self._df_store.save()
|
self._df_store.save()
|
||||||
|
|
||||||
def handle_set_column_width(self, col_id: str, width: str):
|
def add_new_row(self, row_data: dict = None) -> None:
|
||||||
"""Update column width after resize. Called via Command from JS."""
|
"""Add a new row to the DataGrid with incremental updates.
|
||||||
logger.debug(f"set_column_width: {col_id=} {width=}")
|
|
||||||
for col in self._state.columns:
|
|
||||||
if col.col_id == col_id:
|
|
||||||
col.width = int(width)
|
|
||||||
break
|
|
||||||
|
|
||||||
self._state.save()
|
Creates default values based on column types and handles formula columns.
|
||||||
|
Updates ns_fast_access and ns_row_data incrementally for better performance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row_data: Optional dict with initial values. If None, uses type defaults.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def _get_default_value(col_def: DataGridColumnState):
|
||||||
|
"""Get default value for a column based on its type."""
|
||||||
|
if col_def.type == ColumnType.Formula:
|
||||||
|
return None # Will be calculated by FormulaEngine
|
||||||
|
return column_type_defaults.get(col_def.type, "")
|
||||||
|
|
||||||
|
if row_data is None:
|
||||||
|
# Create default values for all non-formula, non-selection columns
|
||||||
|
row_data = {}
|
||||||
|
for col in self._state.columns:
|
||||||
|
if col.type not in (ColumnType.Formula, ColumnType.RowSelection_):
|
||||||
|
row_data[col.col_id] = _get_default_value(col)
|
||||||
|
|
||||||
|
# 1. Add row to DataFrame (only non-formula columns)
|
||||||
|
new_index = len(self._df)
|
||||||
|
self._df.loc[new_index] = row_data
|
||||||
|
|
||||||
|
# 2. Incremental update of ns_fast_access
|
||||||
|
for col_id, value in row_data.items():
|
||||||
|
if col_id in self._df_store.ns_fast_access:
|
||||||
|
self._df_store.ns_fast_access[col_id] = np.append(
|
||||||
|
self._df_store.ns_fast_access[col_id],
|
||||||
|
value
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# First value for this column (rare case)
|
||||||
|
self._df_store.ns_fast_access[col_id] = np.array([value])
|
||||||
|
|
||||||
|
# Update row index
|
||||||
|
if ROW_INDEX_ID in self._df_store.ns_fast_access:
|
||||||
|
self._df_store.ns_fast_access[ROW_INDEX_ID] = np.append(
|
||||||
|
self._df_store.ns_fast_access[ROW_INDEX_ID],
|
||||||
|
new_index
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._df_store.ns_fast_access[ROW_INDEX_ID] = np.array([new_index])
|
||||||
|
|
||||||
|
# 3. Incremental update of ns_row_data
|
||||||
|
self._df_store.ns_row_data.append(row_data.copy())
|
||||||
|
|
||||||
|
# 4. Handle formula columns
|
||||||
|
engine = self.get_formula_engine()
|
||||||
|
if engine is not None:
|
||||||
|
table_name = self.get_table_name()
|
||||||
|
# Check if there are any formula columns
|
||||||
|
has_formulas = any(col.type == ColumnType.Formula for col in self._state.columns)
|
||||||
|
if has_formulas:
|
||||||
|
try:
|
||||||
|
# Recalculate all formulas (engine handles efficiency)
|
||||||
|
engine.recalculate_if_needed(table_name, self._df_store)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to calculate formulas for new row: {e}")
|
||||||
|
|
||||||
|
# 5. Update total row count
|
||||||
|
self._df_store.ns_total_rows = len(self._df)
|
||||||
|
|
||||||
|
# Save changes
|
||||||
|
self._df_store.save()
|
||||||
|
|
||||||
def move_column(self, source_col_id: str, target_col_id: str):
|
def move_column(self, source_col_id: str, target_col_id: str):
|
||||||
"""Move column to new position. Called via Command from JS."""
|
"""Move column to new position. Called via Command from JS."""
|
||||||
@@ -569,6 +659,7 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.columns.insert(target_idx, col)
|
self._state.columns.insert(target_idx, col)
|
||||||
else:
|
else:
|
||||||
self._state.columns.insert(target_idx, col)
|
self._state.columns.insert(target_idx, col)
|
||||||
|
self._init_columns()
|
||||||
|
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
@@ -688,16 +779,46 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.save()
|
self._state.save()
|
||||||
return self.render_partial()
|
return self.render_partial()
|
||||||
|
|
||||||
def toggle_columns_manager(self):
|
def handle_toggle_columns_manager(self):
|
||||||
logger.debug(f"toggle_columns_manager")
|
logger.debug(f"toggle_columns_manager")
|
||||||
self._panel.set_title(side="right", title="Columns")
|
self._panel.set_title(side="right", title="Columns")
|
||||||
|
self._columns_manager.adding_new_column = False
|
||||||
|
self._columns_manager.set_all_columns(True)
|
||||||
|
self._columns_manager.unbind_command(("ShowAllColumns", "SaveColumnDetails"))
|
||||||
self._panel.set_right(self._columns_manager)
|
self._panel.set_right(self._columns_manager)
|
||||||
|
|
||||||
def toggle_formatting_editor(self):
|
def handle_toggle_new_column_editor(self):
|
||||||
|
logger.debug(f"handle_toggle_new_column_editor")
|
||||||
|
self._panel.set_title(side="right", title="Columns")
|
||||||
|
self._columns_manager.adding_new_column = True
|
||||||
|
self._columns_manager.set_all_columns(False)
|
||||||
|
self._columns_manager.bind_command(("ShowAllColumns", "SaveColumnDetails"),
|
||||||
|
self._panel.commands.set_side_visible("right", False))
|
||||||
|
self._panel.set_right(self._columns_manager)
|
||||||
|
|
||||||
|
def handle_toggle_formatting_editor(self):
|
||||||
logger.debug(f"toggle_formatting_editor")
|
logger.debug(f"toggle_formatting_editor")
|
||||||
self._panel.set_title(side="right", title="Formatting")
|
self._panel.set_title(side="right", title="Formatting")
|
||||||
self._panel.set_right(self._formatting_editor)
|
self._panel.set_right(self._formatting_editor)
|
||||||
|
|
||||||
|
def handle_set_column_width(self, col_id: str, width: str):
|
||||||
|
"""Update column width after resize. Called via Command from JS."""
|
||||||
|
logger.debug(f"set_column_width: {col_id=} {width=}")
|
||||||
|
for col in self._state.columns:
|
||||||
|
if col.col_id == col_id:
|
||||||
|
col.width = int(width)
|
||||||
|
break
|
||||||
|
|
||||||
|
self._state.save()
|
||||||
|
|
||||||
|
def handle_add_row(self):
|
||||||
|
self.add_new_row() # Add row to data
|
||||||
|
row_index = len(self._df) - 1 # Index of the newly added row
|
||||||
|
len_columns_1 = len(self._columns) - 1
|
||||||
|
rows = [self.mk_row(row_index, None, len_columns_1)]
|
||||||
|
self._mk_append_add_row_if_needed(rows)
|
||||||
|
return rows
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
self._state.save()
|
self._state.save()
|
||||||
|
|
||||||
@@ -734,6 +855,9 @@ class DataGrid(MultipleInstance):
|
|||||||
if not col_def.visible:
|
if not col_def.visible:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if col_def.type == ColumnType.RowSelection_:
|
||||||
|
return Div(cls="dt2-row-selection")
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
_mk_header_name(col_def),
|
_mk_header_name(col_def),
|
||||||
Div(cls="dt2-resize-handle", data_command_id=resize_cmd.id, data_reset_command_id=reset_cmd.id),
|
Div(cls="dt2-resize-handle", data_command_id=resize_cmd.id, data_reset_command_id=reset_cmd.id),
|
||||||
@@ -744,8 +868,13 @@ class DataGrid(MultipleInstance):
|
|||||||
)
|
)
|
||||||
|
|
||||||
header_class = "dt2-header"
|
header_class = "dt2-header"
|
||||||
|
header_columns = [_mk_header(col_def) for col_def in self._columns]
|
||||||
|
if self._settings.enable_edition:
|
||||||
|
header_columns.append(Div(
|
||||||
|
mk.icon(command=self.commands.toggle_new_column_editor(), size=16),
|
||||||
|
cls="dt2-add-column"))
|
||||||
return Div(
|
return Div(
|
||||||
*[_mk_header(col_def) for col_def in self._state.columns],
|
*header_columns,
|
||||||
cls=header_class,
|
cls=header_class,
|
||||||
id=f"th_{self._id}",
|
id=f"th_{self._id}",
|
||||||
data_move_command_id=move_cmd.id
|
data_move_command_id=move_cmd.id
|
||||||
@@ -841,7 +970,7 @@ class DataGrid(MultipleInstance):
|
|||||||
else:
|
else:
|
||||||
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-text", cls), css_string)
|
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-text", cls), css_string)
|
||||||
|
|
||||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower, is_last):
|
||||||
"""
|
"""
|
||||||
OPTIMIZED: Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups.
|
OPTIMIZED: Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups.
|
||||||
OPTIMIZED: Uses OptimizedDiv instead of Div for faster rendering.
|
OPTIMIZED: Uses OptimizedDiv instead of Div for faster rendering.
|
||||||
@@ -849,6 +978,9 @@ class DataGrid(MultipleInstance):
|
|||||||
if not col_def.visible:
|
if not col_def.visible:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if col_def.type == ColumnType.RowSelection_:
|
||||||
|
return OptimizedDiv(cls="dt2-row-selection")
|
||||||
|
|
||||||
col_array = self._df_store.ns_fast_access.get(col_def.col_id)
|
col_array = self._df_store.ns_fast_access.get(col_def.col_id)
|
||||||
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
|
value = col_array[row_index] if col_array is not None and row_index < len(col_array) else None
|
||||||
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
||||||
@@ -858,7 +990,23 @@ class DataGrid(MultipleInstance):
|
|||||||
data_tooltip=str(value),
|
data_tooltip=str(value),
|
||||||
style=f"width:{col_def.width}px;",
|
style=f"width:{col_def.width}px;",
|
||||||
id=self._get_element_id_from_pos("cell", (col_pos, row_index)),
|
id=self._get_element_id_from_pos("cell", (col_pos, row_index)),
|
||||||
cls="dt2-cell")
|
cls=merge_classes("dt2-cell", "dt2-last-cell" if is_last else None))
|
||||||
|
|
||||||
|
def mk_row(self, row_index, filter_keyword_lower, len_columns_1):
|
||||||
|
return OptimizedDiv(
|
||||||
|
*[self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower, col_pos == len_columns_1)
|
||||||
|
for col_pos, col_def in enumerate(self._columns)],
|
||||||
|
cls="dt2-row",
|
||||||
|
data_row=f"{row_index}",
|
||||||
|
id=f"tr_{self._id}-{row_index}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mk_append_add_row_if_needed(self, rows):
|
||||||
|
if self._settings.enable_edition:
|
||||||
|
rows.append(Div(
|
||||||
|
mk.icon(command=self.commands.add_row(), size=16),
|
||||||
|
cls="dt2-add-row",
|
||||||
|
id=f"tr_{self._id}-last"))
|
||||||
|
|
||||||
def mk_body_content_page(self, page_index: int):
|
def mk_body_content_page(self, page_index: int):
|
||||||
"""
|
"""
|
||||||
@@ -872,22 +1020,16 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
start = page_index * DATAGRID_PAGE_SIZE
|
start = page_index * DATAGRID_PAGE_SIZE
|
||||||
end = start + DATAGRID_PAGE_SIZE
|
end = start + DATAGRID_PAGE_SIZE
|
||||||
if self._df_store.ns_total_rows > end:
|
|
||||||
last_row = df.index[end - 1]
|
|
||||||
else:
|
|
||||||
last_row = None
|
|
||||||
|
|
||||||
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
|
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
|
||||||
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
|
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
|
||||||
|
len_columns_1 = len(self._columns) - 1
|
||||||
|
|
||||||
rows = [OptimizedDiv(
|
rows = [self.mk_row(row_index, filter_keyword_lower, len_columns_1) for row_index in df.index[start:end]]
|
||||||
*[self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower)
|
if self._df_store.ns_total_rows > end:
|
||||||
for col_pos, col_def in enumerate(self._state.columns)],
|
rows[-1].attrs.extend(self.commands.get_page(page_index + 1).get_htmx_params(escaped=True))
|
||||||
cls="dt2-row",
|
|
||||||
data_row=f"{row_index}",
|
self._mk_append_add_row_if_needed(rows)
|
||||||
id=f"tr_{self._id}-{row_index}",
|
|
||||||
**self.commands.get_page(page_index + 1).get_htmx_params(escaped=True) if row_index == last_row else {}
|
|
||||||
) for row_index in df.index[start:end]]
|
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -907,7 +1049,7 @@ class DataGrid(MultipleInstance):
|
|||||||
def mk_footers(self):
|
def mk_footers(self):
|
||||||
return Div(
|
return Div(
|
||||||
*[Div(
|
*[Div(
|
||||||
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns],
|
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._columns],
|
||||||
id=f"tf_{self._id}",
|
id=f"tf_{self._id}",
|
||||||
data_row=f"{row_index}",
|
data_row=f"{row_index}",
|
||||||
cls="dt2-row dt2-row-footer",
|
cls="dt2-row dt2-row-footer",
|
||||||
@@ -1059,7 +1201,7 @@ class DataGrid(MultipleInstance):
|
|||||||
cls="flex items-center justify-between mb-2"),
|
cls="flex items-center justify-between mb-2"),
|
||||||
self._panel.set_main(self.mk_table_wrapper()),
|
self._panel.set_main(self.mk_table_wrapper()),
|
||||||
Script(f"initDataGrid('{self._id}');"),
|
Script(f"initDataGrid('{self._id}');"),
|
||||||
Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
# Mouse(self, combinations=self._mouse_support, _id="-mouse"),
|
||||||
Keyboard(self, combinations=self._key_support, _id="-keyboard"),
|
Keyboard(self, combinations=self._key_support, _id="-keyboard"),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
cls="grid",
|
cls="grid",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from myfasthtml.controls.Search import Search
|
|||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
||||||
from myfasthtml.controls.helpers import mk
|
from myfasthtml.controls.helpers import mk
|
||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType
|
from myfasthtml.core.constants import ColumnType, get_columns_types
|
||||||
from myfasthtml.core.dsls import DslsManager
|
from myfasthtml.core.dsls import DslsManager
|
||||||
from myfasthtml.core.formula.dsl.completion.FormulaCompletionEngine import FormulaCompletionEngine
|
from myfasthtml.core.formula.dsl.completion.FormulaCompletionEngine import FormulaCompletionEngine
|
||||||
from myfasthtml.core.formula.dsl.parser import FormulaParser
|
from myfasthtml.core.formula.dsl.parser import FormulaParser
|
||||||
@@ -39,21 +39,21 @@ class Commands(BaseCommands):
|
|||||||
return Command(f"ShowAllColumns",
|
return Command(f"ShowAllColumns",
|
||||||
f"Show all columns",
|
f"Show all columns",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML")
|
self._owner.handle_show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML")
|
||||||
|
|
||||||
def save_column_details(self, col_id):
|
def save_column_details(self, col_id):
|
||||||
return Command(f"SaveColumnDetails",
|
return Command(f"SaveColumnDetails",
|
||||||
f"Save column {col_id}",
|
f"Save column {col_id}",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.save_column_details,
|
self._owner.handle_save_column_details,
|
||||||
kwargs={"col_id": col_id}
|
kwargs={"col_id": col_id}
|
||||||
).htmx(target=f"#{self._id}", swap="innerHTML")
|
).htmx(target=f"#{self._id}", swap="innerHTML")
|
||||||
|
|
||||||
def on_new_column(self):
|
def add_new_column(self):
|
||||||
return Command(f"OnNewColumn",
|
return Command(f"AddNewColumn",
|
||||||
f"New column",
|
f"Add a new column",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_new_column).htmx(target=f"#{self._id}", swap="innerHTML")
|
self._owner.handle_add_new_column).htmx(target=f"#{self._id}", swap="innerHTML")
|
||||||
|
|
||||||
def on_column_type_changed(self):
|
def on_column_type_changed(self):
|
||||||
return Command(f"OnColumnTypeChanged",
|
return Command(f"OnColumnTypeChanged",
|
||||||
@@ -66,7 +66,8 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
def __init__(self, parent, _id=None):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self._new_column = False
|
self.adding_new_column = False
|
||||||
|
self._all_columns = True # Do not return to all_columns after column details
|
||||||
|
|
||||||
completion_engine = FormulaCompletionEngine(
|
completion_engine = FormulaCompletionEngine(
|
||||||
self._parent._parent,
|
self._parent._parent,
|
||||||
@@ -109,6 +110,9 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
|
|
||||||
return col_def
|
return col_def
|
||||||
|
|
||||||
|
def set_all_columns(self, all_columns):
|
||||||
|
self._all_columns = all_columns
|
||||||
|
|
||||||
def toggle_column(self, col_id):
|
def toggle_column(self, col_id):
|
||||||
logger.debug(f"toggle_column {col_id=}")
|
logger.debug(f"toggle_column {col_id=}")
|
||||||
col_def = self._get_col_def_from_col_id(col_id, copy=False)
|
col_def = self._get_col_def_from_col_id(col_id, copy=False)
|
||||||
@@ -129,22 +133,24 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
|
|
||||||
return self.mk_column_details(col_def)
|
return self.mk_column_details(col_def)
|
||||||
|
|
||||||
def show_all_columns(self):
|
def handle_show_all_columns(self):
|
||||||
return self._mk_inner_content()
|
self.adding_new_column = False
|
||||||
|
return self._mk_inner_content() if self._all_columns else None
|
||||||
|
|
||||||
def save_column_details(self, col_id, client_response):
|
def handle_save_column_details(self, col_id, client_response):
|
||||||
logger.debug(f"save_column_details {col_id=}, {client_response=}")
|
logger.debug(f"save_column_details {col_id=}, {client_response=}")
|
||||||
col_def = self._get_updated_col_def_from_col_id(col_id, client_response, copy=False)
|
col_def = self._get_updated_col_def_from_col_id(col_id, client_response, copy=False)
|
||||||
if col_def.col_id == "__new__":
|
if col_def.col_id == "__new__":
|
||||||
self._parent.add_new_column(col_def) # sets the correct col_id before _register_formula
|
self._parent.add_new_column(col_def) # sets the correct col_id before _register_formula
|
||||||
|
self.adding_new_column = False
|
||||||
self._register_formula(col_def)
|
self._register_formula(col_def)
|
||||||
self._parent.save_state()
|
self._parent.save_state()
|
||||||
|
|
||||||
return self._mk_inner_content()
|
return self._mk_inner_content() if self._all_columns else None
|
||||||
|
|
||||||
def on_new_column(self):
|
def handle_add_new_column(self):
|
||||||
self._new_column = True
|
self.adding_new_column = True
|
||||||
col_def = self._get_updated_col_def_from_col_id("__new__")
|
col_def = DataGridColumnState("__new__", -1)
|
||||||
return self.mk_column_details(col_def)
|
return self.mk_column_details(col_def)
|
||||||
|
|
||||||
def on_column_type_changed(self, col_id, client_response):
|
def on_column_type_changed(self, col_id, client_response):
|
||||||
@@ -192,6 +198,7 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
|
|
||||||
def mk_column_details(self, col_def: DataGridColumnState):
|
def mk_column_details(self, col_def: DataGridColumnState):
|
||||||
size = "sm"
|
size = "sm"
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
mk.label("Back", icon=chevron_left20_regular, command=self.commands.show_all_columns()),
|
mk.label("Back", icon=chevron_left20_regular, command=self.commands.show_all_columns()),
|
||||||
Form(
|
Form(
|
||||||
@@ -210,7 +217,9 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
Label("type"),
|
Label("type"),
|
||||||
mk.mk(
|
mk.mk(
|
||||||
Select(
|
Select(
|
||||||
*[Option(option.value, value=option.value, selected=option == col_def.type) for option in ColumnType],
|
*[Option(option.value,
|
||||||
|
value=option.value,
|
||||||
|
selected=option == col_def.type) for option in get_columns_types()],
|
||||||
name="type",
|
name="type",
|
||||||
cls=f"select select-{size}",
|
cls=f"select select-{size}",
|
||||||
value=col_def.title,
|
value=col_def.title,
|
||||||
@@ -260,11 +269,15 @@ class DataGridColumnsManager(MultipleInstance):
|
|||||||
|
|
||||||
def mk_new_column(self):
|
def mk_new_column(self):
|
||||||
return Div(
|
return Div(
|
||||||
mk.button("New Column", command=self.commands.on_new_column()),
|
mk.button("New Column", command=self.commands.add_new_column()),
|
||||||
cls="mb-1",
|
cls="mb-1",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mk_inner_content(self):
|
def _mk_inner_content(self):
|
||||||
|
if self.adding_new_column:
|
||||||
|
col_def = DataGridColumnState("__new__", -1)
|
||||||
|
return self.mk_column_details(col_def)
|
||||||
|
|
||||||
return (self.mk_all_columns(),
|
return (self.mk_all_columns(),
|
||||||
self.mk_new_column())
|
self.mk_new_column())
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from fastcore.basics import NotStr
|
||||||
|
|
||||||
from myfasthtml.core.constants import ColumnType
|
from myfasthtml.core.constants import ColumnType
|
||||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
||||||
number_row20_regular
|
number_row20_regular
|
||||||
@@ -13,7 +15,7 @@ default_icons = {
|
|||||||
False: checkbox_unchecked20_regular,
|
False: checkbox_unchecked20_regular,
|
||||||
|
|
||||||
"Brain": brain_circuit20_regular,
|
"Brain": brain_circuit20_regular,
|
||||||
"QuestionMark" : question20_regular,
|
"QuestionMark": question20_regular,
|
||||||
|
|
||||||
# TreeView icons
|
# TreeView icons
|
||||||
"TreeViewFolder": folder20_regular,
|
"TreeViewFolder": folder20_regular,
|
||||||
@@ -46,6 +48,9 @@ class IconsHelper:
|
|||||||
:return: The requested icon resource if found; otherwise, returns None.
|
:return: The requested icon resource if found; otherwise, returns None.
|
||||||
:rtype: object or None
|
:rtype: object or None
|
||||||
"""
|
"""
|
||||||
|
if isinstance(name, NotStr):
|
||||||
|
return name
|
||||||
|
|
||||||
if name in IconsHelper._icons:
|
if name in IconsHelper._icons:
|
||||||
return IconsHelper._icons[name]
|
return IconsHelper._icons[name]
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ class Menu(MultipleInstance):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _mk_menu(self, command_name):
|
def _mk_menu(self, command_name):
|
||||||
|
if not isinstance(command_name, str):
|
||||||
|
return command_name
|
||||||
|
|
||||||
command = self.usable_commands.get(command_name)
|
command = self.usable_commands.get(command_name)
|
||||||
return mk.icon(command.icon or IconsHelper.get("QuestionMark"),
|
return mk.icon(command.icon or IconsHelper.get("QuestionMark"),
|
||||||
command=command,
|
command=command,
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ class PanelState(DbObject):
|
|||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
def set_side_visible(self, side: Literal["left", "right"], visible: bool = None):
|
def set_side_visible(self, side: Literal["left", "right"], visible: bool = None):
|
||||||
return Command("TogglePanelSide",
|
return Command("SetVisiblePanelSide",
|
||||||
f"Toggle {side} side panel",
|
f"Open / Close {side} side panel",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.set_side_visible,
|
self._owner.set_side_visible,
|
||||||
args=[side, visible]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
|
args=[side, visible]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class Command:
|
|||||||
before_commands = [bc for bc in self.owner.get_bound_commands(self.name) if bc.when == "before"]
|
before_commands = [bc for bc in self.owner.get_bound_commands(self.name) if bc.when == "before"]
|
||||||
for bound_cmd in before_commands:
|
for bound_cmd in before_commands:
|
||||||
logger.debug(f" will execute bound command {bound_cmd.command.name} BEFORE...")
|
logger.debug(f" will execute bound command {bound_cmd.command.name} BEFORE...")
|
||||||
r = bound_cmd.command.execute() # client_response should not be forwarded as it's not the same command
|
r = bound_cmd.command.execute(client_response)
|
||||||
ret_from_before_commands.append(r)
|
ret_from_before_commands.append(r)
|
||||||
|
|
||||||
# Execute main callback
|
# Execute main callback
|
||||||
@@ -166,7 +166,7 @@ class Command:
|
|||||||
after_commands = [bc for bc in self.owner.get_bound_commands(self.name) if bc.when == "after"]
|
after_commands = [bc for bc in self.owner.get_bound_commands(self.name) if bc.when == "after"]
|
||||||
for bound_cmd in after_commands:
|
for bound_cmd in after_commands:
|
||||||
logger.debug(f" will execute bound command {bound_cmd.command.name} AFTER...")
|
logger.debug(f" will execute bound command {bound_cmd.command.name} AFTER...")
|
||||||
r = bound_cmd.command.execute() # client_response should not be forwarded as it's not the same command
|
r = bound_cmd.command.execute(client_response)
|
||||||
ret_from_after_commands.append(r)
|
ret_from_after_commands.append(r)
|
||||||
|
|
||||||
all_ret = flatten(ret, ret_from_before_commands, ret_from_after_commands, collector.results)
|
all_ret = flatten(ret, ret_from_before_commands, ret_from_after_commands, collector.results)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ ROUTE_ROOT = "/myfasthtml"
|
|||||||
|
|
||||||
# Datagrid
|
# Datagrid
|
||||||
ROW_INDEX_ID = "__row_index__"
|
ROW_INDEX_ID = "__row_index__"
|
||||||
|
ROW_SELECTION_ID = "__row_selection__"
|
||||||
DATAGRID_DEFAULT_COLUMN_WIDTH = 100
|
DATAGRID_DEFAULT_COLUMN_WIDTH = 100
|
||||||
DATAGRID_PAGE_SIZE = 1000
|
DATAGRID_PAGE_SIZE = 1000
|
||||||
FILTER_INPUT_CID = "__filter_input__"
|
FILTER_INPUT_CID = "__filter_input__"
|
||||||
@@ -19,6 +20,7 @@ class Routes:
|
|||||||
|
|
||||||
|
|
||||||
class ColumnType(Enum):
|
class ColumnType(Enum):
|
||||||
|
RowSelection_ = "RowSelection_"
|
||||||
RowIndex = "RowIndex"
|
RowIndex = "RowIndex"
|
||||||
Text = "Text"
|
Text = "Text"
|
||||||
Number = "Number"
|
Number = "Number"
|
||||||
@@ -29,6 +31,10 @@ class ColumnType(Enum):
|
|||||||
Formula = "Formula"
|
Formula = "Formula"
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns_types() -> list[ColumnType]:
|
||||||
|
return [c for c in ColumnType if not c.value.endswith("_")]
|
||||||
|
|
||||||
|
|
||||||
class ViewType(Enum):
|
class ViewType(Enum):
|
||||||
Table = "Table"
|
Table = "Table"
|
||||||
Chart = "Chart"
|
Chart = "Chart"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional, Literal
|
||||||
|
|
||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids
|
||||||
from myfasthtml.core.commands import BoundCommand
|
from myfasthtml.core.commands import BoundCommand, Command
|
||||||
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||||
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal
|
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal
|
||||||
|
|
||||||
@@ -113,7 +113,10 @@ class BaseInstance:
|
|||||||
parent = self.get_parent()
|
parent = self.get_parent()
|
||||||
return parent.get_full_id() if parent else None
|
return parent.get_full_id() if parent else None
|
||||||
|
|
||||||
def bind_command(self, command, command_to_bind, when="after"):
|
def bind_command(self,
|
||||||
|
command: Command | str | tuple | list,
|
||||||
|
command_to_bind,
|
||||||
|
when: Literal["before", "after"] = "after"):
|
||||||
"""
|
"""
|
||||||
Bind a command to another command.
|
Bind a command to another command.
|
||||||
|
|
||||||
@@ -126,18 +129,28 @@ class BaseInstance:
|
|||||||
Duplicate bindings are automatically prevented using two mechanisms:
|
Duplicate bindings are automatically prevented using two mechanisms:
|
||||||
1. Check if the same binding already exists
|
1. Check if the same binding already exists
|
||||||
"""
|
"""
|
||||||
command_name = command.name if hasattr(command, "name") else command
|
|
||||||
|
|
||||||
# Protection 1: Check if this binding already exists to prevent duplicates
|
def _bind(_command_name, _command_to_bind, _when):
|
||||||
existing_bindings = self._bound_commands.get(command_name, [])
|
# Check if this binding already exists to prevent duplicates
|
||||||
for existing in existing_bindings:
|
existing_bindings = self._bound_commands.get(_command_name, [])
|
||||||
if existing.command.name == command_to_bind.name and existing.when == when:
|
for existing in existing_bindings:
|
||||||
# Binding already exists, don't add it again
|
if existing.command.name == _command_to_bind.name and existing.when == _when:
|
||||||
return
|
return # Binding already exists, don't add it again
|
||||||
|
|
||||||
# Add new binding
|
# Add new binding
|
||||||
bound = BoundCommand(command=command_to_bind, when=when)
|
bound = BoundCommand(command=_command_to_bind, when=_when)
|
||||||
self._bound_commands.setdefault(command_name, []).append(bound)
|
self._bound_commands.setdefault(command_name, []).append(bound)
|
||||||
|
|
||||||
|
commands = [command] if isinstance(command, (str, Command)) else command
|
||||||
|
for c in commands:
|
||||||
|
command_name = c.name if hasattr(c, "name") else c
|
||||||
|
_bind(c, command_to_bind, when)
|
||||||
|
|
||||||
|
def unbind_command(self, command: Command | str | tuple | list):
|
||||||
|
commands = [command] if isinstance(command, (str, Command)) else command
|
||||||
|
for c in commands:
|
||||||
|
command_name = c.name if hasattr(c, "name") else c
|
||||||
|
self._bound_commands.pop(command_name, None)
|
||||||
|
|
||||||
def get_bound_commands(self, command_name):
|
def get_bound_commands(self, command_name):
|
||||||
return self._bound_commands.get(command_name, [])
|
return self._bound_commands.get(command_name, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user