Added IconsHelper and updated Keyboard to support require_inside flag
This commit is contained in:
@@ -165,7 +165,6 @@
|
||||
|
||||
// Add key to current pressed keys
|
||||
KeyboardRegistry.currentKeys.add(key);
|
||||
// console.debug("Received key", key);
|
||||
|
||||
// Create a snapshot of current keyboard state
|
||||
const snapshot = new Set(KeyboardRegistry.currentKeys);
|
||||
@@ -218,14 +217,17 @@
|
||||
anyHasLongerSequence = true;
|
||||
}
|
||||
|
||||
// Collect matches
|
||||
// Collect matches, respecting require_inside flag
|
||||
if (hasMatch) {
|
||||
currentMatches.push({
|
||||
elementId: elementId,
|
||||
config: currentNode.config,
|
||||
combinationStr: currentNode.combinationStr,
|
||||
isInside: isInside
|
||||
});
|
||||
const requireInside = currentNode.config["require_inside"] === true;
|
||||
if (!requireInside || isInside) {
|
||||
currentMatches.push({
|
||||
elementId: elementId,
|
||||
config: currentNode.config,
|
||||
combinationStr: currentNode.combinationStr,
|
||||
isInside: isInside
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,13 @@ from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
|
||||
from myfasthtml.controls.DataGridFormattingEditor import DataGridFormattingEditor
|
||||
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
|
||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Keyboard import Keyboard
|
||||
from myfasthtml.controls.Mouse import Mouse
|
||||
from myfasthtml.controls.Panel import Panel, PanelConf
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||
from myfasthtml.controls.helpers import mk, icons, column_type_defaults
|
||||
from myfasthtml.controls.helpers import mk, column_type_defaults
|
||||
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.dbmanager import DbObject
|
||||
@@ -261,7 +262,7 @@ class DataGrid(MultipleInstance):
|
||||
}
|
||||
|
||||
self._key_support = {
|
||||
"esc": self.commands.on_key_pressed(),
|
||||
"esc": {"command": self.commands.on_key_pressed(), "require_inside": True},
|
||||
}
|
||||
|
||||
logger.debug(f"DataGrid '{self.get_table_name()}' with id='{self._id}' created.")
|
||||
@@ -647,7 +648,7 @@ class DataGrid(MultipleInstance):
|
||||
|
||||
def _mk_header_name(col_def: DataGridColumnState):
|
||||
return Div(
|
||||
mk.label(col_def.title, icon=icons.get(col_def.type, None)),
|
||||
mk.label(col_def.title, icon=IconsHelper.get(col_def.type)),
|
||||
# make room for sort and filter indicators
|
||||
cls="flex truncate cursor-default",
|
||||
data_tooltip=col_def.title,
|
||||
|
||||
@@ -5,9 +5,10 @@ from fasthtml.components import *
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.DataGridFormulaEditor import DataGridFormulaEditor
|
||||
from myfasthtml.controls.DslEditor import DslEditorConf
|
||||
from myfasthtml.controls.IconsHelper import IconsHelper
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState
|
||||
from myfasthtml.controls.helpers import icons, mk
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.dsls import DslsManager
|
||||
@@ -179,7 +180,7 @@ class DataGridColumnsManager(MultipleInstance):
|
||||
),
|
||||
mk.mk(
|
||||
Div(
|
||||
Div(mk.label(col_def.col_id, icon=icons.get(col_def.type, None), cls="ml-2")),
|
||||
Div(mk.label(col_def.col_id, icon=IconsHelper.get(col_def.type), cls="ml-2")),
|
||||
Div(mk.icon(chevron_right20_regular), cls="mr-2"),
|
||||
cls="dt2-column-manager-label"
|
||||
),
|
||||
|
||||
@@ -97,7 +97,7 @@ class Dropdown(MultipleInstance):
|
||||
self._mk_content(),
|
||||
cls="mf-dropdown-wrapper"
|
||||
),
|
||||
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
|
||||
Keyboard(self, _id="-keyboard").add("esc", self.commands.close(), require_inside=True),
|
||||
Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
|
||||
id=self._id
|
||||
)
|
||||
|
||||
71
src/myfasthtml/controls/IconsHelper.py
Normal file
71
src/myfasthtml/controls/IconsHelper.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
||||
number_row20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
checkbox_checked20_filled, math_formula16_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
|
||||
|
||||
default_icons = {
|
||||
None: question20_regular,
|
||||
True: checkbox_checked20_regular,
|
||||
False: checkbox_unchecked20_regular,
|
||||
|
||||
"Brain": brain_circuit20_regular,
|
||||
|
||||
ColumnType.RowIndex: number_symbol20_regular,
|
||||
ColumnType.Text: text_field20_regular,
|
||||
ColumnType.Number: number_row20_regular,
|
||||
ColumnType.Datetime: calendar_ltr20_regular,
|
||||
ColumnType.Bool: checkbox_checked20_filled,
|
||||
ColumnType.Enum: text_bullet_list_square20_regular,
|
||||
ColumnType.Formula: math_formula16_regular,
|
||||
}
|
||||
|
||||
|
||||
class IconsHelper:
|
||||
_icons = default_icons.copy()
|
||||
|
||||
@staticmethod
|
||||
def get(name, package=None):
|
||||
"""
|
||||
Fetches and returns an icon resource based on the provided name and optional package. If the icon is not already
|
||||
cached, it will attempt to dynamically load the icon from the available modules under the `myfasthtml.icons` package.
|
||||
This method uses an internal caching mechanism to store previously fetched icons for future quick lookups.
|
||||
|
||||
:param name: The name of the requested icon.
|
||||
:param package: The optional sub-package to limit the search for the requested icon. If not provided, the method will
|
||||
iterate through all available modules within the `myfasthtml.icons` package.
|
||||
:return: The requested icon resource if found; otherwise, returns None.
|
||||
:rtype: object or None
|
||||
"""
|
||||
if name in IconsHelper._icons:
|
||||
return IconsHelper._icons[name]
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
import myfasthtml.icons as icons_pkg
|
||||
|
||||
_UTILITY_MODULES = {'manage_icons', 'update_icons'}
|
||||
|
||||
if package:
|
||||
module = importlib.import_module(f"myfasthtml.icons.{package}")
|
||||
icon = getattr(module, name, None)
|
||||
if icon is not None:
|
||||
IconsHelper._icons[name] = icon
|
||||
return icon
|
||||
|
||||
for _, modname, _ in pkgutil.iter_modules(icons_pkg.__path__):
|
||||
if modname in _UTILITY_MODULES:
|
||||
continue
|
||||
module = importlib.import_module(f"myfasthtml.icons.{modname}")
|
||||
icon = getattr(module, name, None)
|
||||
if icon is not None:
|
||||
IconsHelper._icons[name] = icon
|
||||
return icon
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
IconsHelper._icons = default_icons.copy()
|
||||
@@ -17,12 +17,16 @@ class Keyboard(MultipleInstance):
|
||||
super().__init__(parent, _id=_id)
|
||||
self.combinations = combinations or {}
|
||||
|
||||
def add(self, sequence: str, command: Command):
|
||||
self.combinations[sequence] = command
|
||||
def add(self, sequence: str, command: Command, require_inside: bool = True):
|
||||
self.combinations[sequence] = {"command": command, "require_inside": require_inside}
|
||||
return self
|
||||
|
||||
|
||||
def render(self):
|
||||
str_combinations = {sequence: command.get_htmx_params() for sequence, command in self.combinations.items()}
|
||||
str_combinations = {}
|
||||
for sequence, value in self.combinations.items():
|
||||
params = value["command"].get_htmx_params()
|
||||
params["require_inside"] = value.get("require_inside", True)
|
||||
str_combinations[sequence] = params
|
||||
return Script(f"add_keyboard_support('{self._parent.get_id()}', '{json.dumps(str_combinations)}')")
|
||||
|
||||
def __ft__(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ TreeView component for hierarchical data visualization with inline editing.
|
||||
This component provides an interactive tree structure with expand/collapse,
|
||||
selection, and inline editing capabilities.
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
@@ -19,6 +20,19 @@ from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.icons.fluent_p1 import chevron_right20_regular, edit20_regular
|
||||
from myfasthtml.icons.fluent_p2 import chevron_down20_regular, add_circle20_regular, delete20_regular
|
||||
|
||||
logger = logging.getLogger("TreeView")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeViewConf:
|
||||
edit_node: bool = True
|
||||
add_node: bool = True
|
||||
delete_node: bool = True
|
||||
edit_leaf: bool = True
|
||||
add_leaf: bool = True
|
||||
delete_leaf: bool = True
|
||||
icons: dict = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeNode:
|
||||
@@ -157,7 +171,7 @@ class TreeView(MultipleInstance):
|
||||
- Node selection
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None):
|
||||
def __init__(self, parent, items: Optional[dict] = None, conf: TreeViewConf = None, _id: Optional[str] = None):
|
||||
"""
|
||||
Initialize TreeView component.
|
||||
|
||||
@@ -168,10 +182,14 @@ class TreeView(MultipleInstance):
|
||||
"""
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = TreeViewState(self)
|
||||
self.conf = conf or TreeViewConf()
|
||||
self.commands = Commands(self)
|
||||
|
||||
|
||||
if items:
|
||||
self._state.items = items
|
||||
|
||||
if self.conf.icons:
|
||||
self._state.icon_config = self.conf.icons
|
||||
|
||||
def set_icon_config(self, config: dict[str, str]):
|
||||
"""
|
||||
@@ -207,7 +225,7 @@ class TreeView(MultipleInstance):
|
||||
else:
|
||||
parent.children.append(node.id)
|
||||
|
||||
def ensure_path(self, path: str):
|
||||
def ensure_path(self, path: str, node_type="folder"):
|
||||
"""Add a node to the tree based on a path string.
|
||||
|
||||
Args:
|
||||
@@ -237,7 +255,7 @@ class TreeView(MultipleInstance):
|
||||
node = [node for node in current_nodes if node.label == part]
|
||||
if len(node) == 0:
|
||||
# create the node
|
||||
node = TreeNode(label=part, type="folder")
|
||||
node = TreeNode(label=part, type=node_type)
|
||||
self.add_node(node, parent_id=parent_id)
|
||||
else:
|
||||
node = node[0]
|
||||
@@ -341,6 +359,7 @@ class TreeView(MultipleInstance):
|
||||
|
||||
def _cancel_rename(self):
|
||||
"""Cancel renaming operation."""
|
||||
logger.debug("_cancel_rename")
|
||||
self._state.editing = None
|
||||
return self
|
||||
|
||||
@@ -380,12 +399,22 @@ class TreeView(MultipleInstance):
|
||||
|
||||
def _render_action_buttons(self, node_id: str):
|
||||
"""Render action buttons for a node (visible on hover)."""
|
||||
return Div(
|
||||
mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)),
|
||||
mk.icon(edit20_regular, command=self.commands.start_rename(node_id)),
|
||||
mk.icon(delete20_regular, command=self.commands.delete_node(node_id)),
|
||||
cls="mf-treenode-actions"
|
||||
)
|
||||
is_leaf = len(self._state.items[node_id].children) == 0
|
||||
conf = self.conf
|
||||
|
||||
add_visible = conf.add_leaf if is_leaf else conf.add_node
|
||||
edit_visible = conf.edit_leaf if is_leaf else conf.edit_node
|
||||
delete_visible = conf.delete_leaf if is_leaf else conf.delete_node
|
||||
|
||||
buttons = []
|
||||
if add_visible:
|
||||
buttons.append(mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)))
|
||||
if edit_visible:
|
||||
buttons.append(mk.icon(edit20_regular, command=self.commands.start_rename(node_id)))
|
||||
if delete_visible:
|
||||
buttons.append(mk.icon(delete20_regular, command=self.commands.delete_node(node_id)))
|
||||
|
||||
return Div(*buttons, cls="mf-treenode-actions")
|
||||
|
||||
def _render_node(self, node_id: str, level: int = 0):
|
||||
"""
|
||||
@@ -404,10 +433,13 @@ class TreeView(MultipleInstance):
|
||||
is_editing = node_id == self._state.editing
|
||||
has_children = len(node.children) > 0
|
||||
|
||||
# Toggle icon
|
||||
toggle = mk.icon(
|
||||
chevron_down20_regular if is_expanded else chevron_right20_regular if has_children else None,
|
||||
command=self.commands.toggle_node(node_id))
|
||||
# Toggle icon (only for nodes with children)
|
||||
if has_children:
|
||||
toggle = mk.icon(
|
||||
chevron_down20_regular if is_expanded else chevron_right20_regular,
|
||||
command=self.commands.toggle_node(node_id))
|
||||
else:
|
||||
toggle = None
|
||||
|
||||
# Label or input for editing
|
||||
if is_editing:
|
||||
@@ -426,7 +458,7 @@ class TreeView(MultipleInstance):
|
||||
node_element = Div(
|
||||
toggle,
|
||||
label_element,
|
||||
self._render_action_buttons(node_id),
|
||||
*([self._render_action_buttons(node_id)] if not is_editing else []),
|
||||
cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}",
|
||||
style=f"padding-left: {level * 20}px"
|
||||
)
|
||||
@@ -461,7 +493,7 @@ class TreeView(MultipleInstance):
|
||||
|
||||
return Div(
|
||||
*[self._render_node(node_id) for node_id in root_nodes],
|
||||
Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard"),
|
||||
Keyboard(self, {"esc": {"command": self.commands.cancel_rename(), "require_inside": False}}, _id="-keyboard"),
|
||||
id=self._id,
|
||||
cls="mf-treeview"
|
||||
)
|
||||
|
||||
@@ -5,12 +5,6 @@ from myfasthtml.core.bindings import Binding
|
||||
from myfasthtml.core.commands import Command, CommandTemplate
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.utils import merge_classes
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_row20_regular, \
|
||||
number_symbol20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
checkbox_checked20_filled, math_formula16_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_bullet_list_square20_regular, text_field20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
|
||||
|
||||
|
||||
class Ids:
|
||||
@@ -148,26 +142,9 @@ class mk:
|
||||
return ft
|
||||
|
||||
|
||||
icons = {
|
||||
None: question20_regular,
|
||||
True: checkbox_checked20_regular,
|
||||
False: checkbox_unchecked20_regular,
|
||||
|
||||
"Brain": brain_circuit20_regular,
|
||||
|
||||
ColumnType.RowIndex: number_symbol20_regular,
|
||||
ColumnType.Text: text_field20_regular,
|
||||
ColumnType.Number: number_row20_regular,
|
||||
ColumnType.Datetime: calendar_ltr20_regular,
|
||||
ColumnType.Bool: checkbox_checked20_filled,
|
||||
ColumnType.Enum: text_bullet_list_square20_regular,
|
||||
ColumnType.Formula: math_formula16_regular,
|
||||
}
|
||||
|
||||
column_type_defaults = {
|
||||
ColumnType.Number: 0,
|
||||
ColumnType.Text: "",
|
||||
ColumnType.Bool: False,
|
||||
ColumnType.Datetime: pd.NaT,
|
||||
}
|
||||
|
||||
@@ -88,10 +88,10 @@ class Command:
|
||||
if auto_register:
|
||||
if self._key is not None:
|
||||
if self._key in CommandsManager.commands_by_key:
|
||||
logger.debug(f"Command {self.name} with key={self._key} will not be registered.")
|
||||
#logger.debug(f"Command {self.name} with key={self._key} will not be registered.")
|
||||
self.id = CommandsManager.commands_by_key[self._key].id
|
||||
else:
|
||||
logger.debug(f"Command {self.name} with key={self._key} will be registered.")
|
||||
#logger.debug(f"Command {self.name} with key={self._key} will be registered.")
|
||||
CommandsManager.register(self)
|
||||
else:
|
||||
logger.warning(f"Command {self.name} has no key, it will not be registered.")
|
||||
|
||||
Reference in New Issue
Block a user