From bc12036056c3411c78de58d4a76997b0fa084ed1 Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Thu, 13 Nov 2025 11:50:55 +0000 Subject: [PATCH 01/12] combo box work initail working - added initial combobox bundle to test case --- pyrevitlib/pyrevit/coreutils/ribbon.py | 49 ++++++++++++++ pyrevitlib/pyrevit/extensions/__init__.py | 1 + pyrevitlib/pyrevit/extensions/components.py | 25 +++++++ pyrevitlib/pyrevit/loader/uimaker.py | 72 ++++++++++++++++++++- 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index cc51a860f..50071e9b4 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -885,6 +885,23 @@ def availability_class_name(self): return self._rvtapi_object.AvailabilityClassName +class _PyRevitRibbonComboBox(GenericPyRevitUIContainer): + """Wrapper for Revit API ComboBox - bare minimum.""" + + def __init__(self, ribbon_combobox): + GenericPyRevitUIContainer.__init__(self) + + self.name = ribbon_combobox.Name + self._rvtapi_object = ribbon_combobox + + # Check if it's ComboBoxData (itemdata_mode) or actual ComboBox + self.itemdata_mode = isinstance(self._rvtapi_object, UI.ComboBoxData) + + self.ui_title = self.name + if not self.itemdata_mode: + self.ui_title = self._rvtapi_object.ItemText if hasattr(self._rvtapi_object, 'ItemText') else self.name + + class _PyRevitRibbonGroupItem(GenericPyRevitUIContainer): button = GenericPyRevitUIContainer._get_component @@ -1466,6 +1483,38 @@ def create_splitpush_button(self, item_name, icon_path, update_if_exists) self.ribbon_item(item_name).sync_with_current_item(False) + def create_combobox(self, item_name, update_if_exists=False): + """Create a ComboBox in the ribbon panel - bare minimum. + + Args: + item_name (str): Name of the ComboBox + update_if_exists (bool, optional): Update if exists. Defaults to False. + """ + if self.contains(item_name): + if update_if_exists: + existing_item = self._get_component(item_name) + existing_item.activate() + return + else: + raise PyRevitUIError('ComboBox already exists and ' + 'update is not allowed: {}' + .format(item_name)) + + # Create ComboBoxData + combobox_data = UI.ComboBoxData(item_name) + + if not self.itemdata_mode: + # Add to panel + new_combobox = self.get_rvtapi_object().AddItem(combobox_data) + pyrvt_combobox = _PyRevitRibbonComboBox(new_combobox) + else: + # Create under stack + pyrvt_combobox = _PyRevitRibbonComboBox(combobox_data) + + pyrvt_combobox.set_dirty_flag() + self._add_component(pyrvt_combobox) + pyrvt_combobox.activate() + def create_panel_push_button(self, button_name, asm_location, class_name, tooltip='', tooltip_ext='', tooltip_media='', ctxhelpurl=None, diff --git a/pyrevitlib/pyrevit/extensions/__init__.py b/pyrevitlib/pyrevit/extensions/__init__.py index 0a179cc55..783f07b33 100644 --- a/pyrevitlib/pyrevit/extensions/__init__.py +++ b/pyrevitlib/pyrevit/extensions/__init__.py @@ -161,6 +161,7 @@ def get_ext_types(cls): NOGUI_COMMAND_POSTFIX = '.nobutton' CONTENT_BUTTON_POSTFIX = '.content' URL_BUTTON_POSTFIX = '.urlbutton' +COMBOBOX_POSTFIX = '.combobox' # known bundle sub-directories COMP_LIBRARY_DIR_NAME = 'lib' diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index 97e9088ce..b78cec776 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -298,6 +298,31 @@ class PullDownButtonGroup(GenericUICommandGroup): type_id = exts.PULLDOWN_BUTTON_POSTFIX +class ComboBoxGroup(GenericUICommandGroup): + """ComboBox group.""" + type_id = exts.COMBOBOX_POSTFIX + + def __init__(self, cmp_path=None): + GenericUICommandGroup.__init__(self, cmp_path=cmp_path) + self.members = [] + mlogger.warning('=== ComboBoxGroup created: %s (path: %s) ===', self.name, cmp_path) + + # Read members from metadata + if self.meta: + raw_members = self.meta.get('members', []) + mlogger.warning('ComboBoxGroup %s metadata members: %s', self.name, raw_members) + if isinstance(raw_members, list): + # Simple list format: ['Option 1', 'Option 2'] + self.members = [(m, m) if isinstance(m, str) else m for m in raw_members] + elif isinstance(raw_members, dict): + # Dict format: {'A': 'Option A'} + self.members = [(k, v) for k, v in raw_members.items()] + else: + mlogger.warning('ComboBoxGroup %s has no metadata', self.name) + + mlogger.warning('ComboBoxGroup %s final members: %s', self.name, self.members) + + class SplitPushButtonGroup(GenericUICommandGroup): """Split push button group.""" type_id = exts.SPLITPUSH_BUTTON_POSTFIX diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index d93f01259..f0f3d8dc6 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -6,6 +6,7 @@ from pyrevit.coreutils import assmutils from pyrevit.coreutils.logger import get_logger from pyrevit.coreutils import applocales +from pyrevit.api import UI from pyrevit.coreutils import ribbon @@ -413,6 +414,68 @@ def _produce_ui_pulldown(ui_maker_params): return None +def _produce_ui_combobox(ui_maker_params): + """Create a ComboBox - bare minimum implementation. + + Args: + ui_maker_params (UIMakerParams): Standard parameters for making ui item. + """ + parent_ribbon_panel = ui_maker_params.parent_ui + combobox = ui_maker_params.component + + mlogger.warning('Creating ComboBox: %s', combobox.name) + try: + # Create ComboBox using panel's create_combobox method + parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) + combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) + + if not combobox_ui: + mlogger.error('Failed to get ComboBox UI item: %s', combobox.name) + return None + + # Get the Revit API ComboBox object + combobox_obj = combobox_ui.get_rvtapi_object() + + # Set ItemText (required for ComboBox to display) + combobox_obj.ItemText = combobox.ui_title or combobox.name + + # Add members + if combobox.members: + for member in combobox.members: + if isinstance(member, (list, tuple)) and len(member) >= 2: + member_id, member_text = member[0], member[1] + elif isinstance(member, str): + member_id = member_text = member + else: + continue + + # Create ComboBoxMemberData and add to ComboBox + member_data = UI.ComboBoxMemberData(member_id, member_text) + combobox_obj.AddItem(member_data) + mlogger.warning('Added ComboBox member: %s (%s)', member_text, member_id) + + # Set Current to first item if members exist + items = combobox_obj.GetItems() + if items and len(items) > 0: + combobox_obj.Current = items[0] + combobox_obj.ItemText = items[0].ItemText + mlogger.warning('Set ComboBox current item: %s', items[0].ItemText) + + _set_highlights(combobox, combobox_ui) + combobox_ui.activate() + + mlogger.warning('ComboBox created successfully: %s', combobox.name) + return combobox_ui + except PyRevitException as err: + mlogger.error('UI error creating ComboBox: %s', err.msg) + return None + except Exception as err: + mlogger.error('Error creating ComboBox: %s', err) + import traceback + mlogger.error(traceback.format_exc()) + return None + + def _produce_ui_split(ui_maker_params): """Produce a split button. @@ -627,6 +690,7 @@ def _produce_ui_tab(ui_maker_params): exts.PANEL_POSTFIX: _produce_ui_panels, exts.STACK_BUTTON_POSTFIX: _produce_ui_stacks, exts.PULLDOWN_BUTTON_POSTFIX: _produce_ui_pulldown, + exts.COMBOBOX_POSTFIX: _produce_ui_combobox, exts.SPLIT_BUTTON_POSTFIX: _produce_ui_split, exts.SPLITPUSH_BUTTON_POSTFIX: _produce_ui_splitpush, exts.PUSH_BUTTON_POSTFIX: _produce_ui_pushbutton, @@ -646,6 +710,11 @@ def _recursively_produce_ui_items(ui_maker_params): for sub_cmp in ui_maker_params.component: ui_item = None try: + # Log ComboBox specifically + if sub_cmp.type_id == exts.COMBOBOX_POSTFIX: + mlogger.warning('=== FOUND COMBOBOX: %s (type_id: %s) ===', + sub_cmp, sub_cmp.type_id) + mlogger.debug('Calling create func %s for: %s', _component_creation_dict[sub_cmp.type_id], sub_cmp) @@ -658,7 +727,8 @@ def _recursively_produce_ui_items(ui_maker_params): if ui_item: cmp_count += 1 except KeyError: - mlogger.debug('Can not find create function for: %s', sub_cmp) + mlogger.warning('Can not find create function for type_id: %s (component: %s)', + sub_cmp.type_id, sub_cmp) except Exception as create_err: mlogger.critical( 'Error creating item: %s | %s', sub_cmp, create_err From 53f96225f83de08b9433a80a390149f1edbdb305 Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Thu, 13 Nov 2025 12:20:17 +0000 Subject: [PATCH 02/12] Fix ComboBox: Handle OrderedDict members from metadata and convert to tuples --- pyrevitlib/pyrevit/extensions/components.py | 17 +++++++++++++++-- pyrevitlib/pyrevit/loader/uimaker.py | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index b78cec776..6e5f7eb43 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -312,8 +312,21 @@ def __init__(self, cmp_path=None): raw_members = self.meta.get('members', []) mlogger.warning('ComboBoxGroup %s metadata members: %s', self.name, raw_members) if isinstance(raw_members, list): - # Simple list format: ['Option 1', 'Option 2'] - self.members = [(m, m) if isinstance(m, str) else m for m in raw_members] + # Process list of members - handle OrderedDict, dict, tuple, list, or string + processed_members = [] + for m in raw_members: + if isinstance(m, dict) or (hasattr(m, 'get') and hasattr(m, 'keys')): + # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings'} + member_id = m.get('id', m.get('name', '')) + member_text = m.get('text', m.get('title', member_id)) + processed_members.append((member_id, member_text)) + elif isinstance(m, (list, tuple)) and len(m) >= 2: + # Tuple/list format: ('id', 'text') + processed_members.append((m[0], m[1])) + elif isinstance(m, str): + # String format: 'Option 1' + processed_members.append((m, m)) + self.members = processed_members elif isinstance(raw_members, dict): # Dict format: {'A': 'Option A'} self.members = [(k, v) for k, v in raw_members.items()] diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index f0f3d8dc6..95d2a084c 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -442,11 +442,17 @@ def _produce_ui_combobox(ui_maker_params): # Add members if combobox.members: for member in combobox.members: + # Handle different member formats if isinstance(member, (list, tuple)) and len(member) >= 2: member_id, member_text = member[0], member[1] + elif isinstance(member, dict) or (hasattr(member, 'get') and hasattr(member, 'keys')): + # OrderedDict or dict format (defensive - should be handled in components.py) + member_id = member.get('id', member.get('name', '')) + member_text = member.get('text', member.get('title', member_id)) elif isinstance(member, str): member_id = member_text = member else: + mlogger.warning('Skipping invalid ComboBox member format: %s (type: %s)', member, type(member)) continue # Create ComboBoxMemberData and add to ComboBox From bab0667ef39549eb6d198c1f01844d023fd1bfce Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Thu, 13 Nov 2025 13:34:02 +0000 Subject: [PATCH 03/12] Add ComboBox support: fix cache invalidation for .combobox bundles - Add COMBOBOX_POSTFIX to extension directory hash calculation - Implement ComboBox creation and member handling in uimaker.py - Add _PyRevitRibbonComboBox wrapper class in ribbon.py - Add ComboBoxGroup component class with members parsing --- pyrevitlib/pyrevit/coreutils/ribbon.py | 5 +-- pyrevitlib/pyrevit/extensions/components.py | 1 + pyrevitlib/pyrevit/loader/uimaker.py | 35 ++++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index 50071e9b4..35cdaf3b9 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -1484,7 +1484,7 @@ def create_splitpush_button(self, item_name, icon_path, self.ribbon_item(item_name).sync_with_current_item(False) def create_combobox(self, item_name, update_if_exists=False): - """Create a ComboBox in the ribbon panel - bare minimum. + """Create a ComboBox in the ribbon panel. Args: item_name (str): Name of the ComboBox @@ -1494,7 +1494,8 @@ def create_combobox(self, item_name, update_if_exists=False): if update_if_exists: existing_item = self._get_component(item_name) existing_item.activate() - return + # Return existing item so caller can update members + return existing_item else: raise PyRevitUIError('ComboBox already exists and ' 'update is not allowed: {}' diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index 6e5f7eb43..eb71b3212 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -476,6 +476,7 @@ def _calculate_extension_dir_hash(self): pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' pat += '|(\\' + exts.CONTENT_BUTTON_POSTFIX + ')' + pat += '|(\\' + exts.COMBOBOX_POSTFIX + ')' # tnteresting directories pat += '|(\\' + exts.COMP_LIBRARY_DIR_NAME + ')' pat += '|(\\' + exts.COMP_HOOKS_DIR_NAME + ')' diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 95d2a084c..941401149 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -425,9 +425,12 @@ def _produce_ui_combobox(ui_maker_params): mlogger.warning('Creating ComboBox: %s', combobox.name) try: - # Create ComboBox using panel's create_combobox method - parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) - combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) + # Create or get existing ComboBox using panel's create_combobox method + combobox_ui = parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) + + # If create_combobox returned None (shouldn't happen), try to get it + if not combobox_ui: + combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) if not combobox_ui: mlogger.error('Failed to get ComboBox UI item: %s', combobox.name) @@ -439,7 +442,15 @@ def _produce_ui_combobox(ui_maker_params): # Set ItemText (required for ComboBox to display) combobox_obj.ItemText = combobox.ui_title or combobox.name - # Add members + # Clear existing members and add fresh ones (cache fix ensures fresh data) + # Note: Revit API doesn't support removing individual members, so we add all + # The cache fix ensures we get fresh members from metadata + existing_items = combobox_obj.GetItems() + if existing_items and len(existing_items) > 0: + mlogger.warning('ComboBox %s already has %d members (will add new ones)', + combobox.name, len(existing_items)) + + # Add members from metadata if combobox.members: for member in combobox.members: # Handle different member formats @@ -684,7 +695,11 @@ def _produce_ui_tab(ui_maker_params): return tab_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + # If tab is native, log as warning instead of error + if 'native item' in err.msg.lower(): + mlogger.warning('UI warning (tab may be native): %s', err.msg) + else: + mlogger.error('UI error: %s', err.msg) return None else: mlogger.debug('Tab does not have any commands. Skipping: %s', tab.name) @@ -716,11 +731,6 @@ def _recursively_produce_ui_items(ui_maker_params): for sub_cmp in ui_maker_params.component: ui_item = None try: - # Log ComboBox specifically - if sub_cmp.type_id == exts.COMBOBOX_POSTFIX: - mlogger.warning('=== FOUND COMBOBOX: %s (type_id: %s) ===', - sub_cmp, sub_cmp.type_id) - mlogger.debug('Calling create func %s for: %s', _component_creation_dict[sub_cmp.type_id], sub_cmp) @@ -741,7 +751,7 @@ def _recursively_produce_ui_items(ui_maker_params): ) mlogger.debug('UI item created by create func is: %s', ui_item) - + # if component does not have any sub components hide it if ui_item \ and not isinstance(ui_item, components.GenericStack) \ and sub_cmp.is_container: @@ -811,7 +821,8 @@ def cleanup_pyrevit_ui(): mlogger.debug('Deactivating: %s', item) item.deactivate() except Exception as deact_err: - mlogger.debug(deact_err) + # Log as debug to avoid cluttering output with expected errors + mlogger.debug('Could not deactivate item (may be native): %s | %s', item, deact_err) def reflow_pyrevit_ui(direction=applocales.DEFAULT_LANG_DIR): From c2c194e4856c7140872fda35e6a88f56d5908a0a Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Thu, 13 Nov 2025 14:46:58 +0000 Subject: [PATCH 04/12] working state - the combobox loads and tirggers update --- pyrevitlib/pyrevit/coreutils/ribbon.py | 36 ++++ pyrevitlib/pyrevit/loader/uimaker.py | 227 ++++++++++++++++++++++--- 2 files changed, 242 insertions(+), 21 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index 35cdaf3b9..864a0442b 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -900,6 +900,42 @@ def __init__(self, ribbon_combobox): self.ui_title = self.name if not self.itemdata_mode: self.ui_title = self._rvtapi_object.ItemText if hasattr(self._rvtapi_object, 'ItemText') else self.name + + # Store event handler reference to prevent garbage collection + self._current_changed_handler = None + + def reset_highlights(self): + """Reset highlight state.""" + try: + if hasattr(AdInternal.Windows, 'HighlightMode'): + adwindows_obj = self.get_adwindows_object() + if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): + adwindows_obj.Highlight = \ + coreutils.get_enum_none(AdInternal.Windows.HighlightMode) + except Exception: + pass # Highlights are optional, fail silently + + def highlight_as_new(self): + """Highlight as new item.""" + try: + if hasattr(AdInternal.Windows, 'HighlightMode'): + adwindows_obj = self.get_adwindows_object() + if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): + adwindows_obj.Highlight = \ + AdInternal.Windows.HighlightMode.New + except Exception: + pass # Highlights are optional, fail silently + + def highlight_as_updated(self): + """Highlight as updated item.""" + try: + if hasattr(AdInternal.Windows, 'HighlightMode'): + adwindows_obj = self.get_adwindows_object() + if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): + adwindows_obj.Highlight = \ + AdInternal.Windows.HighlightMode.Updated + except Exception: + pass # Highlights are optional, fail silently class _PyRevitRibbonGroupItem(GenericPyRevitUIContainer): diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 941401149..a2065dc2e 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -10,6 +10,10 @@ from pyrevit.coreutils import ribbon +# For event handlers in IronPython +from System import EventHandler +from Autodesk.Revit.UI.Events import ComboBoxCurrentChangedEventArgs + #pylint: disable=W0703,C0302,C0103,C0413 import pyrevit.extensions as exts from pyrevit.extensions import components @@ -18,6 +22,10 @@ mlogger = get_logger(__name__) +# Enable verbose logging to see WARNING messages (can be removed later) +# Uncomment the line below to see all log messages: +# mlogger.set_verbose_mode() + CONFIG_SCRIPT_TITLE_POSTFIX = u'\u25CF' @@ -164,9 +172,21 @@ def _produce_ui_slideout(ui_maker_params): ext_asm_info = ui_maker_params.asm_info if not ext_asm_info.reloading: - mlogger.debug('Adding slide out to: %s', parent_ui_item) + # Log panel items before adding slideout (for debugging order issues) + try: + if hasattr(parent_ui_item, 'get_rvtapi_object'): + existing_items = parent_ui_item.get_rvtapi_object().GetItems() + mlogger.warning('SLIDEOUT: Panel has %d items before adding slideout', len(existing_items)) + for idx, item in enumerate(existing_items): + mlogger.warning('SLIDEOUT: Existing item %d: %s (type: %s)', + idx, getattr(item, 'Name', 'unknown'), type(item).__name__) + except Exception as log_err: + mlogger.debug('SLIDEOUT: Could not log existing items: %s', log_err) + + mlogger.warning('SLIDEOUT: Adding slide out to: %s', parent_ui_item) try: parent_ui_item.add_slideout() + mlogger.warning('SLIDEOUT: Slideout added successfully') except PyRevitException as err: mlogger.error('UI error: %s', err.msg) @@ -416,28 +436,105 @@ def _produce_ui_pulldown(ui_maker_params): def _produce_ui_combobox(ui_maker_params): """Create a ComboBox - bare minimum implementation. - + Args: ui_maker_params (UIMakerParams): Standard parameters for making ui item. """ parent_ribbon_panel = ui_maker_params.parent_ui combobox = ui_maker_params.component + + mlogger.warning('COMBOBOX: Creating ComboBox: %s', combobox.name) + print("=== COMBOBOX: Creating ComboBox: {} ===".format(combobox.name)) + + # Validate parent panel + if not parent_ribbon_panel: + mlogger.error('COMBOBOX: Parent ribbon panel is None for: %s', combobox.name) + print("COMBOBOX ERROR: Parent ribbon panel is None") + return None + + # Log panel info for debugging + try: + panel_name = getattr(parent_ribbon_panel, 'name', 'unknown') + panel_type = str(type(parent_ribbon_panel)) + mlogger.warning('COMBOBOX: Parent panel: %s (type: %s)', panel_name, panel_type) + # Warn if panel name suggests it might be native (common native panel names) + native_panel_names = ['Utility', 'Modify', 'Annotate', 'Architecture', 'Structure', 'Systems'] + if panel_name in native_panel_names: + mlogger.error('COMBOBOX: WARNING - Panel "%s" appears to be a native Revit panel! ' + 'Native panels do not support ComboBoxes. This will fail.', panel_name) + except Exception: + pass - mlogger.warning('Creating ComboBox: %s', combobox.name) try: + # Log current panel items before adding ComboBox (for debugging order issues) + # NOTE: Revit's AddItem() always appends to the end, so if the panel already + # has items, the ComboBox will appear at the end regardless of layout order. + # This is a limitation of Revit's ribbon API - there's no way to insert at + # a specific position. + try: + existing_items = parent_ribbon_panel.get_rvtapi_object().GetItems() + item_count = len(existing_items) + mlogger.warning('COMBOBOX: Panel has %d items before adding ComboBox', item_count) + if item_count > 0: + mlogger.warning('COMBOBOX: WARNING - Panel already has items. ComboBox will be added at the end (position %d), ' + 'not at the position specified in layout. This is a Revit API limitation.', item_count) + for idx, item in enumerate(existing_items): + mlogger.warning('COMBOBOX: Existing item %d: %s (type: %s)', + idx, getattr(item, 'Name', 'unknown'), type(item).__name__) + except Exception as log_err: + mlogger.debug('COMBOBOX: Could not log existing items: %s', log_err) + # Create or get existing ComboBox using panel's create_combobox method - combobox_ui = parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) + # Note: create_combobox doesn't return the wrapper, so we get it separately + mlogger.warning('COMBOBOX: About to create ComboBox: %s', combobox.name) + parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) + combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) - # If create_combobox returned None (shouldn't happen), try to get it - if not combobox_ui: - combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) + # Log panel items after adding ComboBox + try: + items_after = parent_ribbon_panel.get_rvtapi_object().GetItems() + mlogger.warning('COMBOBOX: Panel has %d items after adding ComboBox', len(items_after)) + for idx, item in enumerate(items_after): + mlogger.warning('COMBOBOX: Item %d: %s (type: %s)', + idx, getattr(item, 'Name', 'unknown'), type(item).__name__) + except Exception as log_err: + mlogger.debug('COMBOBOX: Could not log items after creation: %s', log_err) if not combobox_ui: - mlogger.error('Failed to get ComboBox UI item: %s', combobox.name) + mlogger.error('COMBOBOX: Failed to get ComboBox UI item: %s', combobox.name) + print("COMBOBOX ERROR: Failed to get ComboBox UI item: {}".format(combobox.name)) return None # Get the Revit API ComboBox object - combobox_obj = combobox_ui.get_rvtapi_object() + # This may fail if the panel is native (e.g., "Utility") which doesn't support ComboBoxes + try: + combobox_obj = combobox_ui.get_rvtapi_object() + except Exception as rvtapi_err: + mlogger.error('COMBOBOX: get_rvtapi_object() failed for %s (panel may be native): %s', + combobox.name, rvtapi_err) + print("COMBOBOX ERROR: get_rvtapi_object() failed: {}".format(rvtapi_err)) + return None + + if not combobox_obj: + mlogger.error('COMBOBOX: get_rvtapi_object() returned None for: %s', combobox.name) + print("COMBOBOX ERROR: get_rvtapi_object() returned None") + return None + + # Log type for debugging (try GetType() first, fallback to Python type) + try: + obj_type = combobox_obj.GetType() + mlogger.warning('COMBOBOX: rvtapi object type: %s', obj_type) + print("COMBOBOX: rvtapi object type: {}".format(obj_type)) + except Exception: + mlogger.warning('COMBOBOX: rvtapi object type (python): %s', type(combobox_obj)) + print("COMBOBOX: rvtapi object type (python): {}".format(type(combobox_obj))) + + # Guard: If we're still in data mode (stack), we cannot attach events yet + if isinstance(combobox_obj, UI.ComboBoxData): + mlogger.warning('COMBOBOX: %s is still UI.ComboBoxData (itemdata_mode). ' + 'Skipping event wiring for now.', combobox.name) + print("COMBOBOX WARNING: ComboBox is still ComboBoxData (itemdata_mode) - skipping event wiring") + return combobox_ui # Set ItemText (required for ComboBox to display) combobox_obj.ItemText = combobox.ui_title or combobox.name @@ -447,7 +544,7 @@ def _produce_ui_combobox(ui_maker_params): # The cache fix ensures we get fresh members from metadata existing_items = combobox_obj.GetItems() if existing_items and len(existing_items) > 0: - mlogger.warning('ComboBox %s already has %d members (will add new ones)', + mlogger.warning('COMBOBOX: %s already has %d members (will add new ones)', combobox.name, len(existing_items)) # Add members from metadata @@ -463,33 +560,109 @@ def _produce_ui_combobox(ui_maker_params): elif isinstance(member, str): member_id = member_text = member else: - mlogger.warning('Skipping invalid ComboBox member format: %s (type: %s)', member, type(member)) + mlogger.warning('COMBOBOX: Skipping invalid member format: %s (type: %s)', member, type(member)) continue # Create ComboBoxMemberData and add to ComboBox member_data = UI.ComboBoxMemberData(member_id, member_text) combobox_obj.AddItem(member_data) - mlogger.warning('Added ComboBox member: %s (%s)', member_text, member_id) + mlogger.warning('COMBOBOX: Added member: %s (%s)', member_text, member_id) # Set Current to first item if members exist items = combobox_obj.GetItems() if items and len(items) > 0: combobox_obj.Current = items[0] combobox_obj.ItemText = items[0].ItemText - mlogger.warning('Set ComboBox current item: %s', items[0].ItemText) + mlogger.warning('COMBOBOX: Set current item: %s', items[0].ItemText) + + # Subscribe to CurrentChanged event to handle selection changes + # Remove existing handler if updating to avoid duplicate subscriptions + prev_handler = getattr(combobox_ui, '_current_changed_handler', None) + if prev_handler: + try: + combobox_obj.CurrentChanged -= prev_handler + mlogger.warning('COMBOBOX: Removed previous CurrentChanged handler: %s', combobox.name) + except Exception as ex: + mlogger.debug('COMBOBOX: Could not remove previous handler: %s', ex) - _set_highlights(combobox, combobox_ui) - combobox_ui.activate() + # Create event handler function (use sender, not captured combobox_obj) + # Store handler reference on combobox_ui to prevent garbage collection + def on_combobox_changed(sender, args): + """Handle ComboBox selection change.""" + try: + # Use sender instead of captured combobox_obj + current_item = sender.Current + if current_item: + selected_text = current_item.ItemText + selected_id = current_item.Name + mlogger.warning('COMBOBOX: Selection changed: %s (id: %s)', selected_text, selected_id) + # TODO: Execute the corresponding script function based on selected_id + # This would require loading and executing the script module + else: + mlogger.warning('COMBOBOX: Selection changed, but Current is None') + except Exception as event_err: + mlogger.error('COMBOBOX: Error in event handler: %s', event_err) - mlogger.warning('ComboBox created successfully: %s', combobox.name) + # Try simple direct assignment first (like WPF events in ribbon.py) + # Store handler reference to prevent garbage collection + combobox_ui._current_changed_handler = on_combobox_changed + try: + combobox_obj.CurrentChanged += combobox_ui._current_changed_handler + mlogger.warning('COMBOBOX: Attached CurrentChanged handler (direct): %s', combobox.name) + except (TypeError, AttributeError) as direct_err: + # If direct assignment fails, try with explicit EventHandler wrapper + mlogger.warning('COMBOBOX: Direct assignment failed, trying EventHandler wrapper: %s', direct_err) + try: + handler = EventHandler[ComboBoxCurrentChangedEventArgs](on_combobox_changed) + combobox_ui._current_changed_handler = handler + combobox_obj.CurrentChanged += handler + mlogger.warning('COMBOBOX: Attached CurrentChanged handler (wrapped): %s', combobox.name) + except Exception as wrapped_err: + mlogger.error('COMBOBOX: Both direct and wrapped event handler failed: %s', wrapped_err) + + # Set highlights (may fail if AdWindows object not available, but that's OK) + try: + _set_highlights(combobox, combobox_ui) + except Exception as highlight_err: + mlogger.debug('COMBOBOX: Could not set highlights: %s', highlight_err) + + # Ensure ComboBox is visible and enabled + try: + if hasattr(combobox_obj, 'Visible'): + combobox_obj.Visible = True + if hasattr(combobox_obj, 'Enabled'): + combobox_obj.Enabled = True + mlogger.warning('COMBOBOX: Set Visible=True, Enabled=True') + except Exception as vis_err: + mlogger.debug('COMBOBOX: Could not set visibility: %s', vis_err) + + # Activate the ComboBox UI item + try: + combobox_ui.activate() + mlogger.warning('COMBOBOX: Activated ComboBox UI item') + except Exception as activate_err: + mlogger.warning('COMBOBOX: Could not activate: %s', activate_err) + + # Final verification - check if ComboBox is in the panel + try: + panel_items = parent_ribbon_panel.ribbon_item(combobox.name) + if panel_items: + mlogger.warning('COMBOBOX: Verified ComboBox exists in panel') + else: + mlogger.error('COMBOBOX: ComboBox not found in panel after creation!') + except Exception as verify_err: + mlogger.debug('COMBOBOX: Could not verify panel item: %s', verify_err) + + mlogger.warning('COMBOBOX: Created successfully: %s', combobox.name) + print("=== COMBOBOX: Created successfully: {} ===".format(combobox.name)) return combobox_ui except PyRevitException as err: - mlogger.error('UI error creating ComboBox: %s', err.msg) + mlogger.error('COMBOBOX: UI error creating: %s', err.msg) return None except Exception as err: - mlogger.error('Error creating ComboBox: %s', err) + mlogger.error('COMBOBOX: Error creating: %s', err) import traceback - mlogger.error(traceback.format_exc()) + mlogger.error('COMBOBOX: %s', traceback.format_exc()) return None @@ -622,7 +795,7 @@ def _produce_ui_panelpushbutton(ui_maker_params): avail_class_name=panelpushbutton.avail_class_name, update_if_exists=True, ui_title=_make_ui_title(panelpushbutton)) - + panelpushbutton_ui = parent_ui_item.button(panelpushbutton.name) _set_highlights(panelpushbutton, panelpushbutton_ui) @@ -700,7 +873,7 @@ def _produce_ui_tab(ui_maker_params): mlogger.warning('UI warning (tab may be native): %s', err.msg) else: mlogger.error('UI error: %s', err.msg) - return None + return None else: mlogger.debug('Tab does not have any commands. Skipping: %s', tab.name) return None @@ -731,6 +904,15 @@ def _recursively_produce_ui_items(ui_maker_params): for sub_cmp in ui_maker_params.component: ui_item = None try: + # Diagnostic logging to track panel placement issues + parent_name = getattr(ui_maker_params.parent_ui, 'name', None) + if not parent_name: + try: + parent_name = str(type(ui_maker_params.parent_ui)) + except: + parent_name = 'unknown' + mlogger.warning('BUILDING COMPONENT: %s parent: %s', sub_cmp.name, parent_name) + mlogger.debug('Calling create func %s for: %s', _component_creation_dict[sub_cmp.type_id], sub_cmp) @@ -752,8 +934,11 @@ def _recursively_produce_ui_items(ui_maker_params): mlogger.debug('UI item created by create func is: %s', ui_item) # if component does not have any sub components hide it + # Exclude GenericStack and ComboBoxGroup from deactivation check + # (GenericStack is a special container, ComboBoxGroup has members not child components) if ui_item \ and not isinstance(ui_item, components.GenericStack) \ + and not isinstance(sub_cmp, components.ComboBoxGroup) \ and sub_cmp.is_container: subcmp_count = _recursively_produce_ui_items( UIMakerParams(ui_item, From 1b398a05bb36f1c5cb2f2d8acb7a9b188311592e Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Thu, 13 Nov 2025 17:35:17 +0000 Subject: [PATCH 05/12] mathcing smartbutton design pattern - because of event handling and initialization, we had to match the design pattern used with the smartbutton - using delayed script loading --- pyrevitlib/pyrevit/loader/uimaker.py | 298 ++++++++++++--------------- 1 file changed, 130 insertions(+), 168 deletions(-) diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index a2065dc2e..16e0b4834 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -1,6 +1,7 @@ """UI maker.""" import sys import imp +import os.path as op from pyrevit import HOST_APP, EXEC_PARAMS, PyRevitException from pyrevit.coreutils import assmutils @@ -10,10 +11,6 @@ from pyrevit.coreutils import ribbon -# For event handlers in IronPython -from System import EventHandler -from Autodesk.Revit.UI.Events import ComboBoxCurrentChangedEventArgs - #pylint: disable=W0703,C0302,C0103,C0413 import pyrevit.extensions as exts from pyrevit.extensions import components @@ -433,200 +430,173 @@ def _produce_ui_pulldown(ui_maker_params): mlogger.error('UI error: %s', err.msg) return None - def _produce_ui_combobox(ui_maker_params): """Create a ComboBox - bare minimum implementation. Args: ui_maker_params (UIMakerParams): Standard parameters for making ui item. """ - parent_ribbon_panel = ui_maker_params.parent_ui - combobox = ui_maker_params.component - - mlogger.warning('COMBOBOX: Creating ComboBox: %s', combobox.name) - print("=== COMBOBOX: Creating ComboBox: {} ===".format(combobox.name)) - - # Validate parent panel - if not parent_ribbon_panel: - mlogger.error('COMBOBOX: Parent ribbon panel is None for: %s', combobox.name) - print("COMBOBOX ERROR: Parent ribbon panel is None") - return None - - # Log panel info for debugging + mlogger.warning('=== COMBOBOX: _produce_ui_combobox CALLED ===') try: - panel_name = getattr(parent_ribbon_panel, 'name', 'unknown') - panel_type = str(type(parent_ribbon_panel)) - mlogger.warning('COMBOBOX: Parent panel: %s (type: %s)', panel_name, panel_type) - # Warn if panel name suggests it might be native (common native panel names) - native_panel_names = ['Utility', 'Modify', 'Annotate', 'Architecture', 'Structure', 'Systems'] - if panel_name in native_panel_names: - mlogger.error('COMBOBOX: WARNING - Panel "%s" appears to be a native Revit panel! ' - 'Native panels do not support ComboBoxes. This will fail.', panel_name) - except Exception: - pass - - try: - # Log current panel items before adding ComboBox (for debugging order issues) - # NOTE: Revit's AddItem() always appends to the end, so if the panel already - # has items, the ComboBox will appear at the end regardless of layout order. - # This is a limitation of Revit's ribbon API - there's no way to insert at - # a specific position. + parent_ribbon_panel = ui_maker_params.parent_ui + combobox = ui_maker_params.component + combobox_name = getattr(combobox, 'name', 'unknown') + + mlogger.warning('COMBOBOX: Function called for: %s', combobox_name) + + # Validate inputs first + if not combobox: + mlogger.error('COMBOBOX: Component is None') + return None + if not parent_ribbon_panel: + mlogger.error('COMBOBOX: Parent UI is None for: %s', combobox_name) + return None + + # Panel info + try: + panel_name = getattr(parent_ribbon_panel, 'name', 'unknown') + panel_type = str(type(parent_ribbon_panel)) + mlogger.warning('COMBOBOX: Parent panel: %s (type: %s)', panel_name, panel_type) + except Exception: + pass + + # Get panel API object + try: + panel_rvtapi = parent_ribbon_panel.get_rvtapi_object() + if not panel_rvtapi: + mlogger.error('COMBOBOX: Panel Revit API object is None for: %s', combobox_name) + return None + except Exception as panel_err: + mlogger.error('COMBOBOX: Could not get panel Revit API object: %s', panel_err) + return None + + # Log items before try: - existing_items = parent_ribbon_panel.get_rvtapi_object().GetItems() - item_count = len(existing_items) - mlogger.warning('COMBOBOX: Panel has %d items before adding ComboBox', item_count) - if item_count > 0: - mlogger.warning('COMBOBOX: WARNING - Panel already has items. ComboBox will be added at the end (position %d), ' - 'not at the position specified in layout. This is a Revit API limitation.', item_count) - for idx, item in enumerate(existing_items): - mlogger.warning('COMBOBOX: Existing item %d: %s (type: %s)', - idx, getattr(item, 'Name', 'unknown'), type(item).__name__) + existing_items = panel_rvtapi.GetItems() + if existing_items: + item_count = len(existing_items) + mlogger.warning('COMBOBOX: Panel has %d items before adding ComboBox', item_count) + for idx, item in enumerate(existing_items): + mlogger.warning( + 'COMBOBOX: Existing item %d: %s (type: %s)', + idx, getattr(item, 'Name', 'unknown'), type(item).__name__ + ) except Exception as log_err: mlogger.debug('COMBOBOX: Could not log existing items: %s', log_err) - - # Create or get existing ComboBox using panel's create_combobox method - # Note: create_combobox doesn't return the wrapper, so we get it separately - mlogger.warning('COMBOBOX: About to create ComboBox: %s', combobox.name) - parent_ribbon_panel.create_combobox(combobox.name, update_if_exists=True) - combobox_ui = parent_ribbon_panel.ribbon_item(combobox.name) - - # Log panel items after adding ComboBox + + # Create combobox + mlogger.warning('COMBOBOX: About to create ComboBox: %s', combobox_name) try: - items_after = parent_ribbon_panel.get_rvtapi_object().GetItems() - mlogger.warning('COMBOBOX: Panel has %d items after adding ComboBox', len(items_after)) - for idx, item in enumerate(items_after): - mlogger.warning('COMBOBOX: Item %d: %s (type: %s)', - idx, getattr(item, 'Name', 'unknown'), type(item).__name__) - except Exception as log_err: - mlogger.debug('COMBOBOX: Could not log items after creation: %s', log_err) - + parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) + except Exception as create_err: + mlogger.error('COMBOBOX: Error calling create_combobox: %s', create_err) + import traceback + mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) + return None + + combobox_ui = parent_ribbon_panel.ribbon_item(combobox_name) if not combobox_ui: - mlogger.error('COMBOBOX: Failed to get ComboBox UI item: %s', combobox.name) - print("COMBOBOX ERROR: Failed to get ComboBox UI item: {}".format(combobox.name)) + mlogger.error('COMBOBOX: Failed to get ComboBox UI item: %s', combobox_name) return None - + # Get the Revit API ComboBox object - # This may fail if the panel is native (e.g., "Utility") which doesn't support ComboBoxes try: combobox_obj = combobox_ui.get_rvtapi_object() except Exception as rvtapi_err: - mlogger.error('COMBOBOX: get_rvtapi_object() failed for %s (panel may be native): %s', - combobox.name, rvtapi_err) - print("COMBOBOX ERROR: get_rvtapi_object() failed: {}".format(rvtapi_err)) + mlogger.error( + 'COMBOBOX: get_rvtapi_object() failed for %s: %s', + combobox_name, rvtapi_err + ) return None - + if not combobox_obj: - mlogger.error('COMBOBOX: get_rvtapi_object() returned None for: %s', combobox.name) - print("COMBOBOX ERROR: get_rvtapi_object() returned None") + mlogger.error('COMBOBOX: get_rvtapi_object() returned None for: %s', combobox_name) return None - - # Log type for debugging (try GetType() first, fallback to Python type) + + # Log type try: obj_type = combobox_obj.GetType() mlogger.warning('COMBOBOX: rvtapi object type: %s', obj_type) - print("COMBOBOX: rvtapi object type: {}".format(obj_type)) except Exception: mlogger.warning('COMBOBOX: rvtapi object type (python): %s', type(combobox_obj)) - print("COMBOBOX: rvtapi object type (python): {}".format(type(combobox_obj))) - - # Guard: If we're still in data mode (stack), we cannot attach events yet + + # IMPORTANT: only bail here if it is still ComboBoxData if isinstance(combobox_obj, UI.ComboBoxData): - mlogger.warning('COMBOBOX: %s is still UI.ComboBoxData (itemdata_mode). ' - 'Skipping event wiring for now.', combobox.name) - print("COMBOBOX WARNING: ComboBox is still ComboBoxData (itemdata_mode) - skipping event wiring") - return combobox_ui - - # Set ItemText (required for ComboBox to display) - combobox_obj.ItemText = combobox.ui_title or combobox.name - - # Clear existing members and add fresh ones (cache fix ensures fresh data) - # Note: Revit API doesn't support removing individual members, so we add all - # The cache fix ensures we get fresh members from metadata - existing_items = combobox_obj.GetItems() - if existing_items and len(existing_items) > 0: - mlogger.warning('COMBOBOX: %s already has %d members (will add new ones)', - combobox.name, len(existing_items)) - + mlogger.warning( + 'COMBOBOX: %s is still UI.ComboBoxData (itemdata_mode). ' + 'Skipping event wiring for now.', combobox_name + ) + return combobox_ui # <- ONLY in this branch + + # From here on we have a real Autodesk.Revit.UI.ComboBox + + # Set ItemText + combobox_obj.ItemText = getattr(combobox, 'ui_title', None) or combobox_name + # Add members from metadata - if combobox.members: + if hasattr(combobox, 'members') and combobox.members: for member in combobox.members: - # Handle different member formats if isinstance(member, (list, tuple)) and len(member) >= 2: member_id, member_text = member[0], member[1] elif isinstance(member, dict) or (hasattr(member, 'get') and hasattr(member, 'keys')): - # OrderedDict or dict format (defensive - should be handled in components.py) member_id = member.get('id', member.get('name', '')) member_text = member.get('text', member.get('title', member_id)) elif isinstance(member, str): member_id = member_text = member else: - mlogger.warning('COMBOBOX: Skipping invalid member format: %s (type: %s)', member, type(member)) + mlogger.warning( + 'COMBOBOX: Skipping invalid member format: %s (type: %s)', + member, type(member) + ) continue - - # Create ComboBoxMemberData and add to ComboBox + member_data = UI.ComboBoxMemberData(member_id, member_text) combobox_obj.AddItem(member_data) mlogger.warning('COMBOBOX: Added member: %s (%s)', member_text, member_id) - - # Set Current to first item if members exist + + # Set Current to first item items = combobox_obj.GetItems() if items and len(items) > 0: combobox_obj.Current = items[0] combobox_obj.ItemText = items[0].ItemText mlogger.warning('COMBOBOX: Set current item: %s', items[0].ItemText) - - # Subscribe to CurrentChanged event to handle selection changes - # Remove existing handler if updating to avoid duplicate subscriptions - prev_handler = getattr(combobox_ui, '_current_changed_handler', None) - if prev_handler: - try: - combobox_obj.CurrentChanged -= prev_handler - mlogger.warning('COMBOBOX: Removed previous CurrentChanged handler: %s', combobox.name) - except Exception as ex: - mlogger.debug('COMBOBOX: Could not remove previous handler: %s', ex) - - # Create event handler function (use sender, not captured combobox_obj) - # Store handler reference on combobox_ui to prevent garbage collection - def on_combobox_changed(sender, args): - """Handle ComboBox selection change.""" - try: - # Use sender instead of captured combobox_obj - current_item = sender.Current - if current_item: - selected_text = current_item.ItemText - selected_id = current_item.Name - mlogger.warning('COMBOBOX: Selection changed: %s (id: %s)', selected_text, selected_id) - # TODO: Execute the corresponding script function based on selected_id - # This would require loading and executing the script module - else: - mlogger.warning('COMBOBOX: Selection changed, but Current is None') - except Exception as event_err: - mlogger.error('COMBOBOX: Error in event handler: %s', event_err) - - # Try simple direct assignment first (like WPF events in ribbon.py) - # Store handler reference to prevent garbage collection - combobox_ui._current_changed_handler = on_combobox_changed - try: - combobox_obj.CurrentChanged += combobox_ui._current_changed_handler - mlogger.warning('COMBOBOX: Attached CurrentChanged handler (direct): %s', combobox.name) - except (TypeError, AttributeError) as direct_err: - # If direct assignment fails, try with explicit EventHandler wrapper - mlogger.warning('COMBOBOX: Direct assignment failed, trying EventHandler wrapper: %s', direct_err) - try: - handler = EventHandler[ComboBoxCurrentChangedEventArgs](on_combobox_changed) - combobox_ui._current_changed_handler = handler - combobox_obj.CurrentChanged += handler - mlogger.warning('COMBOBOX: Attached CurrentChanged handler (wrapped): %s', combobox.name) - except Exception as wrapped_err: - mlogger.error('COMBOBOX: Both direct and wrapped event handler failed: %s', wrapped_err) - - # Set highlights (may fail if AdWindows object not available, but that's OK) + + # Call __selfinit__ on script (SmartButton pattern) try: - _set_highlights(combobox, combobox_ui) - except Exception as highlight_err: - mlogger.debug('COMBOBOX: Could not set highlights: %s', highlight_err) - - # Ensure ComboBox is visible and enabled + combobox_script_file = getattr(combobox, 'script_file', None) + combobox_unique_name = getattr(combobox, 'unique_name', None) + + if not combobox_script_file and hasattr(combobox, 'directory') and combobox.directory: + script_path = op.join(combobox.directory, 'script.py') + if op.exists(script_path): + combobox_script_file = script_path + mlogger.debug('COMBOBOX: Found script.py via directory fallback: %s', script_path) + + if combobox_script_file and combobox_unique_name: + current_paths = list(sys.path) + combobox_module_paths = getattr(combobox, 'module_paths', []) + for search_path in combobox_module_paths: + if search_path not in current_paths: + sys.path.append(search_path) + + imported_script = imp.load_source(combobox_unique_name, combobox_script_file) + sys.path = current_paths + + if hasattr(imported_script, '__selfinit__'): + res = imported_script.__selfinit__(combobox, combobox_ui, HOST_APP.uiapp) + if res is False: + combobox_ui.deactivate() + mlogger.warning('COMBOBOX: Ran __selfinit__ for script: %s', combobox_name) + else: + mlogger.debug('COMBOBOX: Script has no __selfinit__: %s', combobox_script_file) + else: + mlogger.debug('COMBOBOX: No script metadata available for: %s', combobox_name) + except Exception as init_err: + mlogger.error('COMBOBOX: Error in __selfinit__: %s', init_err) + import traceback + mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) + + # Ensure visible & enabled try: if hasattr(combobox_obj, 'Visible'): combobox_obj.Visible = True @@ -635,34 +605,26 @@ def on_combobox_changed(sender, args): mlogger.warning('COMBOBOX: Set Visible=True, Enabled=True') except Exception as vis_err: mlogger.debug('COMBOBOX: Could not set visibility: %s', vis_err) - - # Activate the ComboBox UI item + + # Activate UI item try: combobox_ui.activate() mlogger.warning('COMBOBOX: Activated ComboBox UI item') except Exception as activate_err: mlogger.warning('COMBOBOX: Could not activate: %s', activate_err) - - # Final verification - check if ComboBox is in the panel - try: - panel_items = parent_ribbon_panel.ribbon_item(combobox.name) - if panel_items: - mlogger.warning('COMBOBOX: Verified ComboBox exists in panel') - else: - mlogger.error('COMBOBOX: ComboBox not found in panel after creation!') - except Exception as verify_err: - mlogger.debug('COMBOBOX: Could not verify panel item: %s', verify_err) - - mlogger.warning('COMBOBOX: Created successfully: %s', combobox.name) - print("=== COMBOBOX: Created successfully: {} ===".format(combobox.name)) + + mlogger.warning('COMBOBOX: Created successfully: %s', combobox_name) return combobox_ui + except PyRevitException as err: mlogger.error('COMBOBOX: UI error creating: %s', err.msg) + import traceback + mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) return None except Exception as err: mlogger.error('COMBOBOX: Error creating: %s', err) import traceback - mlogger.error('COMBOBOX: %s', traceback.format_exc()) + mlogger.error('COMBOBOX: Full traceback: %s', traceback.format_exc()) return None From bd80e48627ff6a24f685ee8fbcdc749a8a0f108c Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Tue, 25 Nov 2025 13:45:41 +0000 Subject: [PATCH 06/12] feat: Complete ComboBox implementation with full API support - Added comprehensive ComboBox property support (Name, ToolTip, Image, ItemText, Current, etc.) - Implemented all ComboBox methods (add_item, add_items, add_separator, get_items, etc.) - Fixed member properties by preserving full member dictionaries in components.py (was converting to tuples) - Fixed member icon/tooltip/group properties by setting on ComboBoxMember object after AddItem - Added encoding declaration to uimaker.py to fix non-ASCII character error - Enhanced logging for ComboBox creation and member property setting - Updated add_item() to return ComboBoxMember for property setting - Full support for ComboBoxMemberData properties (icon, tooltip, group, tooltip_ext, tooltip_image) --- pyrevitlib/pyrevit/coreutils/ribbon.py | 304 +++++++++++++++++++- pyrevitlib/pyrevit/extensions/components.py | 21 +- pyrevitlib/pyrevit/loader/uimaker.py | 190 +++++++++++- 3 files changed, 493 insertions(+), 22 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index 864a0442b..d9df58080 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -886,7 +886,7 @@ def availability_class_name(self): class _PyRevitRibbonComboBox(GenericPyRevitUIContainer): - """Wrapper for Revit API ComboBox - bare minimum.""" + """Wrapper for Revit API ComboBox with full property support.""" def __init__(self, ribbon_combobox): GenericPyRevitUIContainer.__init__(self) @@ -901,8 +901,308 @@ def __init__(self, ribbon_combobox): if not self.itemdata_mode: self.ui_title = self._rvtapi_object.ItemText if hasattr(self._rvtapi_object, 'ItemText') else self.name - # Store event handler reference to prevent garbage collection + # Store deferred tooltip media + self.tooltip_image = self.tooltip_video = None + + # Store event handler references to prevent garbage collection self._current_changed_handler = None + self._dropdown_opened_handler = None + self._dropdown_closed_handler = None + + def set_rvtapi_object(self, rvtapi_obj): + """Set underlying Revit API object for this ComboBox. + + Args: + rvtapi_obj (obj): Revit API ComboBox object + """ + GenericPyRevitUIContainer.set_rvtapi_object(self, rvtapi_obj) + # update the ui title for the newly added rvtapi_obj + if not self.itemdata_mode and hasattr(self._rvtapi_object, 'ItemText'): + self._rvtapi_object.ItemText = self.ui_title + + def set_icon(self, icon_file, icon_size=ICON_MEDIUM): + """Set icon for the ComboBox. + + Args: + icon_file (str): Path to icon image file + icon_size (int, optional): Icon size. Defaults to ICON_MEDIUM. + """ + try: + button_icon = ButtonIcons(icon_file) + rvtapi_obj = self.get_rvtapi_object() + if hasattr(rvtapi_obj, 'Image'): + rvtapi_obj.Image = button_icon.small_bitmap + if hasattr(rvtapi_obj, 'LargeImage'): + if icon_size == ICON_LARGE: + rvtapi_obj.LargeImage = button_icon.large_bitmap + else: + rvtapi_obj.LargeImage = button_icon.medium_bitmap + self._dirty = True + except Exception as icon_err: + raise PyRevitUIError('Error in applying icon to ComboBox > {} : {}' + .format(icon_file, icon_err)) + + def set_tooltip(self, tooltip): + """Set tooltip for the ComboBox. + + Args: + tooltip (str): Tooltip text + """ + try: + if tooltip: + self.get_rvtapi_object().ToolTip = tooltip + else: + adwindows_obj = self.get_adwindows_object() + if adwindows_obj and adwindows_obj.ToolTip: + adwindows_obj.ToolTip.Content = None + self._dirty = True + except Exception as tooltip_err: + raise PyRevitUIError('Item does not have tooltip property: {}' + .format(tooltip_err)) + + def set_tooltip_ext(self, tooltip_ext): + """Set extended tooltip (long description) for the ComboBox. + + Args: + tooltip_ext (str): Extended tooltip text + """ + try: + if tooltip_ext: + self.get_rvtapi_object().LongDescription = tooltip_ext + else: + adwindows_obj = self.get_adwindows_object() + if adwindows_obj and adwindows_obj.ToolTip: + adwindows_obj.ToolTip.ExpandedContent = None + self._dirty = True + except Exception as tooltip_err: + raise PyRevitUIError('Item does not have extended ' + 'tooltip property: {}'.format(tooltip_err)) + + def set_tooltip_image(self, tooltip_image): + """Set tooltip image for the ComboBox. + + Args: + tooltip_image (str): Path to tooltip image file + """ + try: + adwindows_obj = self.get_adwindows_object() + if adwindows_obj: + exToolTip = self.get_rvtapi_object().ToolTip + if not isinstance(exToolTip, str): + exToolTip = None + adwindows_obj.ToolTip = AdWindows.RibbonToolTip() + adwindows_obj.ToolTip.Title = self.ui_title + adwindows_obj.ToolTip.Content = exToolTip + _StackPanel = System.Windows.Controls.StackPanel() + _image = System.Windows.Controls.Image() + _image.Source = load_bitmapimage(tooltip_image) + _StackPanel.Children.Add(_image) + adwindows_obj.ToolTip.ExpandedContent = _StackPanel + adwindows_obj.ResolveToolTip() + else: + self.tooltip_image = tooltip_image + except Exception as ttimage_err: + raise PyRevitUIError('Error setting tooltip image {} | {} ' + .format(tooltip_image, ttimage_err)) + + def set_tooltip_video(self, tooltip_video): + """Set tooltip video for the ComboBox. + + Args: + tooltip_video (str): Path to tooltip video file + """ + try: + adwindows_obj = self.get_adwindows_object() + if adwindows_obj: + exToolTip = self.get_rvtapi_object().ToolTip + if not isinstance(exToolTip, str): + exToolTip = None + adwindows_obj.ToolTip = AdWindows.RibbonToolTip() + adwindows_obj.ToolTip.Title = self.ui_title + adwindows_obj.ToolTip.Content = exToolTip + _StackPanel = System.Windows.Controls.StackPanel() + _video = System.Windows.Controls.MediaElement() + _video.Source = Uri(tooltip_video) + _video.LoadedBehavior = System.Windows.Controls.MediaState.Manual + _video.UnloadedBehavior = System.Windows.Controls.MediaState.Manual + + def on_media_ended(sender, args): + sender.Position = System.TimeSpan.Zero + sender.Play() + _video.MediaEnded += on_media_ended + + def on_loaded(sender, args): + sender.Play() + _video.Loaded += on_loaded + _StackPanel.Children.Add(_video) + adwindows_obj.ToolTip.ExpandedContent = _StackPanel + adwindows_obj.ResolveToolTip() + else: + self.tooltip_video = tooltip_video + except Exception as ttvideo_err: + raise PyRevitUIError('Error setting tooltip video {} | {} ' + .format(tooltip_video, ttvideo_err)) + + def set_tooltip_media(self, tooltip_media): + """Set tooltip media (image or video) for the ComboBox. + + Args: + tooltip_media (str): Path to tooltip media file + """ + if tooltip_media.endswith(DEFAULT_TOOLTIP_IMAGE_FORMAT): + self.set_tooltip_image(tooltip_media) + elif tooltip_media.endswith(DEFAULT_TOOLTIP_VIDEO_FORMAT): + self.set_tooltip_video(tooltip_media) + + def set_title(self, ui_title): + """Set the display title (ItemText) for the ComboBox. + + Args: + ui_title (str): Display title text + """ + if self.itemdata_mode: + self.ui_title = ui_title + self._dirty = True + else: + if hasattr(self._rvtapi_object, 'ItemText'): + self._rvtapi_object.ItemText = self.ui_title = ui_title + self._dirty = True + + def get_title(self): + """Get the display title (ItemText) for the ComboBox. + + Returns: + (str): Display title text + """ + if self.itemdata_mode: + return self.ui_title + else: + if hasattr(self._rvtapi_object, 'ItemText'): + return self._rvtapi_object.ItemText + return self.ui_title + + @property + def current(self): + """Get or set the current selected ComboBox member. + + Returns: + (UI.ComboBoxMember): Current selected member, or None + """ + if self.itemdata_mode: + return None + try: + return self._rvtapi_object.Current + except Exception: + return None + + @current.setter + def current(self, value): + """Set the current selected ComboBox member. + + Args: + value (UI.ComboBoxMember): ComboBox member to select + """ + if not self.itemdata_mode and hasattr(self._rvtapi_object, 'Current'): + try: + self._rvtapi_object.Current = value + if value and hasattr(self._rvtapi_object, 'ItemText'): + self._rvtapi_object.ItemText = value.ItemText + self._dirty = True + except Exception as current_err: + raise PyRevitUIError('Error setting current item: {}' + .format(current_err)) + + def add_item(self, member_data): + """Add a new item to the ComboBox. + + Args: + member_data (UI.ComboBoxMemberData): Member data to add + + Returns: + (UI.ComboBoxMember): The created ComboBoxMember object, or None + """ + if not self.itemdata_mode: + try: + member = self._rvtapi_object.AddItem(member_data) + self._dirty = True + return member + except Exception as add_err: + raise PyRevitUIError('Error adding item to ComboBox: {}' + .format(add_err)) + return None + + def add_items(self, member_data_list): + """Add multiple items to the ComboBox. + + Args: + member_data_list (list): List of UI.ComboBoxMemberData objects + """ + if not self.itemdata_mode: + try: + self._rvtapi_object.AddItems(member_data_list) + self._dirty = True + except Exception as add_err: + raise PyRevitUIError('Error adding items to ComboBox: {}' + .format(add_err)) + + def add_separator(self): + """Add a separator to the ComboBox dropdown list.""" + if not self.itemdata_mode: + try: + self._rvtapi_object.AddSeparator() + self._dirty = True + except Exception as sep_err: + raise PyRevitUIError('Error adding separator to ComboBox: {}' + .format(sep_err)) + + def get_items(self): + """Get a copy of the collection of ComboBoxMembers. + + Returns: + (list): List of UI.ComboBoxMember objects + """ + if self.itemdata_mode: + return [] + try: + return list(self._rvtapi_object.GetItems()) + except Exception: + return [] + + def get_contexthelp(self): + """Get contextual help for the ComboBox. + + Returns: + (UI.ContextualHelp): Contextual help object + """ + return self.get_rvtapi_object().GetContextualHelp() + + def set_contexthelp(self, ctxhelpurl): + """Set contextual help for the ComboBox. + + Args: + ctxhelpurl (str): URL for contextual help + """ + if ctxhelpurl: + ch = UI.ContextualHelp(UI.ContextualHelpType.Url, ctxhelpurl) + self.get_rvtapi_object().SetContextualHelp(ch) + + def process_deferred(self): + """Process deferred tooltip media settings.""" + GenericPyRevitUIContainer.process_deferred(self) + + try: + if self.tooltip_image: + self.set_tooltip_image(self.tooltip_image) + except Exception as ttimage_err: + raise PyRevitUIError('Error setting deferred tooltip image {} | {} ' + .format(self.tooltip_image, ttimage_err)) + + try: + if self.tooltip_video: + self.set_tooltip_video(self.tooltip_video) + except Exception as ttvideo_err: + raise PyRevitUIError('Error setting deferred tooltip video {} | {} ' + .format(self.tooltip_video, ttvideo_err)) def reset_highlights(self): """Reset highlight state.""" diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index eb71b3212..dda0920be 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -312,24 +312,23 @@ def __init__(self, cmp_path=None): raw_members = self.meta.get('members', []) mlogger.warning('ComboBoxGroup %s metadata members: %s', self.name, raw_members) if isinstance(raw_members, list): - # Process list of members - handle OrderedDict, dict, tuple, list, or string + # Process list of members - preserve full dict for rich metadata (icons, tooltips, etc.) processed_members = [] for m in raw_members: if isinstance(m, dict) or (hasattr(m, 'get') and hasattr(m, 'keys')): - # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings'} - member_id = m.get('id', m.get('name', '')) - member_text = m.get('text', m.get('title', member_id)) - processed_members.append((member_id, member_text)) + # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings', 'icon': '...', ...} + # Preserve the full dictionary to keep all properties (icon, tooltip, group, etc.) + processed_members.append(m) elif isinstance(m, (list, tuple)) and len(m) >= 2: - # Tuple/list format: ('id', 'text') - processed_members.append((m[0], m[1])) + # Tuple/list format: ('id', 'text') - convert to dict for consistency + processed_members.append({'id': m[0], 'text': m[1]}) elif isinstance(m, str): - # String format: 'Option 1' - processed_members.append((m, m)) + # String format: 'Option 1' - convert to dict for consistency + processed_members.append({'id': m, 'text': m}) self.members = processed_members elif isinstance(raw_members, dict): - # Dict format: {'A': 'Option A'} - self.members = [(k, v) for k, v in raw_members.items()] + # Dict format: {'A': 'Option A'} - convert to list of dicts + self.members = [{'id': k, 'text': v} for k, v in raw_members.items()] else: mlogger.warning('ComboBoxGroup %s has no metadata', self.name) diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 16e0b4834..7d430870b 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """UI maker.""" import sys import imp @@ -10,6 +11,7 @@ from pyrevit.api import UI from pyrevit.coreutils import ribbon +from pyrevit.coreutils.ribbon import ICON_MEDIUM #pylint: disable=W0703,C0302,C0103,C0413 import pyrevit.extensions as exts @@ -486,8 +488,10 @@ def _produce_ui_combobox(ui_maker_params): # Create combobox mlogger.warning('COMBOBOX: About to create ComboBox: %s', combobox_name) + mlogger.warning('COMBOBOX: combobox.directory: %s', getattr(combobox, 'directory', 'N/A')) try: parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) + mlogger.warning('COMBOBOX: create_combobox() completed') except Exception as create_err: mlogger.error('COMBOBOX: Error calling create_combobox: %s', create_err) import traceback @@ -498,10 +502,12 @@ def _produce_ui_combobox(ui_maker_params): if not combobox_ui: mlogger.error('COMBOBOX: Failed to get ComboBox UI item: %s', combobox_name) return None + mlogger.warning('COMBOBOX: Got combobox_ui: %s', type(combobox_ui).__name__) # Get the Revit API ComboBox object try: combobox_obj = combobox_ui.get_rvtapi_object() + mlogger.warning('COMBOBOX: Got combobox_obj: %s', type(combobox_obj).__name__ if combobox_obj else 'None') except Exception as rvtapi_err: mlogger.error( 'COMBOBOX: get_rvtapi_object() failed for %s: %s', @@ -530,17 +536,93 @@ def _produce_ui_combobox(ui_maker_params): # From here on we have a real Autodesk.Revit.UI.ComboBox - # Set ItemText - combobox_obj.ItemText = getattr(combobox, 'ui_title', None) or combobox_name - + # Set ItemText/Title + # Note: In Revit, ComboBox.ItemText displays the current selected item's text in the dropdown + # There is no separate visible "title" label for ComboBoxes like buttons have + # The title from bundle.yaml is used for tooltip and identification + # We'll set ItemText to the title initially, but it will be overwritten when current item is set + combobox_title = getattr(combobox, 'ui_title', None) or combobox_name + if combobox_title: + try: + # Set initial ItemText to title (will be overwritten when current item is set) + combobox_obj.ItemText = combobox_title + mlogger.warning('COMBOBOX: Set initial ItemText to title: %s', combobox_title) + except Exception as title_err: + mlogger.debug('COMBOBOX: Could not set ItemText: %s', title_err) + + # Set icon if available + parent = ui_maker_params.parent_cmp + icon_file = getattr(combobox, 'icon_file', None) or getattr(parent, 'icon_file', None) + if icon_file: + try: + combobox_ui.set_icon(icon_file, icon_size=ICON_MEDIUM) + mlogger.debug('COMBOBOX: Set icon: %s', icon_file) + except Exception as icon_err: + mlogger.debug('COMBOBOX: Error setting icon: %s', icon_err) + + # Set tooltip if available + tooltip = getattr(combobox, 'tooltip', None) + if tooltip: + try: + combobox_ui.set_tooltip(tooltip) + mlogger.debug('COMBOBOX: Set tooltip') + except Exception as tooltip_err: + mlogger.debug('COMBOBOX: Error setting tooltip: %s', tooltip_err) + + # Set extended tooltip if available + tooltip_ext = getattr(combobox, 'tooltip_ext', None) + if tooltip_ext: + try: + combobox_ui.set_tooltip_ext(tooltip_ext) + mlogger.debug('COMBOBOX: Set extended tooltip') + except Exception as tooltip_ext_err: + mlogger.debug('COMBOBOX: Error setting extended tooltip: %s', tooltip_ext_err) + + # Set tooltip media (image/video) if available + tooltip_media = getattr(combobox, 'media_file', None) + if tooltip_media: + try: + combobox_ui.set_tooltip_media(tooltip_media) + mlogger.debug('COMBOBOX: Set tooltip media: %s', tooltip_media) + except Exception as tooltip_media_err: + mlogger.debug('COMBOBOX: Error setting tooltip media: %s', tooltip_media_err) + + # Set contextual help if available + help_url = getattr(combobox, 'help_url', None) + if help_url: + try: + combobox_ui.set_contexthelp(help_url) + mlogger.debug('COMBOBOX: Set contextual help') + except Exception as help_err: + mlogger.debug('COMBOBOX: Error setting contextual help: %s', help_err) + + # Check if ComboBox already has members (from cache/previous load) + existing_items = combobox_ui.get_items() + if existing_items: + mlogger.warning('COMBOBOX: Found %d existing members - will add new ones', len(existing_items)) + # Add members from metadata if hasattr(combobox, 'members') and combobox.members: + mlogger.warning('COMBOBOX: Processing %d members from metadata', len(combobox.members)) for member in combobox.members: + member_id = None + member_text = None + member_icon = None + member_group = None + member_tooltip = None + member_tooltip_ext = None + member_tooltip_image = None + if isinstance(member, (list, tuple)) and len(member) >= 2: member_id, member_text = member[0], member[1] elif isinstance(member, dict) or (hasattr(member, 'get') and hasattr(member, 'keys')): member_id = member.get('id', member.get('name', '')) member_text = member.get('text', member.get('title', member_id)) + member_icon = member.get('icon', None) + member_group = member.get('group', member.get('groupName', None)) + member_tooltip = member.get('tooltip', None) + member_tooltip_ext = member.get('tooltip_ext', member.get('longDescription', None)) + member_tooltip_image = member.get('tooltip_image', member.get('tooltipImage', None)) elif isinstance(member, str): member_id = member_text = member else: @@ -550,16 +632,106 @@ def _produce_ui_combobox(ui_maker_params): ) continue + if not member_id or not member_text: + mlogger.warning('COMBOBOX: Skipping member with missing id or text') + continue + + # Create member data (minimal - just id and text) member_data = UI.ComboBoxMemberData(member_id, member_text) - combobox_obj.AddItem(member_data) - mlogger.warning('COMBOBOX: Added member: %s (%s)', member_text, member_id) + + # Add member to ComboBox first (returns ComboBoxMember object) + try: + member = combobox_ui.add_item(member_data) + if not member: + mlogger.warning('COMBOBOX: AddItem returned None for: %s', member_text) + continue + + mlogger.warning('COMBOBOX: Added member: %s (%s), type: %s', member_text, member_id, type(member).__name__) + + # Now set properties on the actual ComboBoxMember object (not the data) + + # Set member icon if available + if member_icon: + try: + # Resolve icon path (relative to bundle directory or absolute) + if combobox.directory and not op.isabs(member_icon): + icon_path = op.join(combobox.directory, member_icon) + else: + icon_path = member_icon + + mlogger.warning('COMBOBOX: Attempting to set icon for %s: %s (exists: %s)', + member_text, icon_path, op.exists(icon_path)) + + if op.exists(icon_path): + button_icon = ribbon.ButtonIcons(icon_path) + mlogger.warning('COMBOBOX: Created ButtonIcons, setting member.Image...') + member.Image = button_icon.small_bitmap + mlogger.warning('COMBOBOX: [OK] Set icon for member: %s (Image type: %s)', + member_text, type(member.Image).__name__ if hasattr(member, 'Image') else 'N/A') + else: + mlogger.warning('COMBOBOX: [ERROR] Icon file not found: %s (directory: %s)', + icon_path, combobox.directory) + except Exception as member_icon_err: + mlogger.warning('COMBOBOX: [ERROR] Error setting member icon for %s: %s', + member_text, member_icon_err) + import traceback + mlogger.warning('COMBOBOX: Traceback: %s', traceback.format_exc()) + + # Set member group if available + if member_group and hasattr(member, 'GroupName'): + try: + member.GroupName = member_group + mlogger.debug('COMBOBOX: Set group for member: %s', member_text) + except Exception as group_err: + mlogger.debug('COMBOBOX: Error setting member group: %s', group_err) + + # Set member tooltip if available + if member_tooltip and hasattr(member, 'ToolTip'): + try: + member.ToolTip = member_tooltip + mlogger.debug('COMBOBOX: Set tooltip for member: %s', member_text) + except Exception as tooltip_err: + mlogger.debug('COMBOBOX: Error setting member tooltip: %s', tooltip_err) + + # Set member extended tooltip if available + if member_tooltip_ext and hasattr(member, 'LongDescription'): + try: + member.LongDescription = member_tooltip_ext + mlogger.debug('COMBOBOX: Set extended tooltip for member: %s', member_text) + except Exception as tooltip_ext_err: + mlogger.debug('COMBOBOX: Error setting member extended tooltip: %s', tooltip_ext_err) + + # Set member tooltip image if available + if member_tooltip_image and hasattr(member, 'ToolTipImage'): + try: + # Resolve tooltip image path (relative to bundle directory or absolute) + if combobox.directory and not op.isabs(member_tooltip_image): + tooltip_image_path = op.join(combobox.directory, member_tooltip_image) + else: + tooltip_image_path = member_tooltip_image + + if op.exists(tooltip_image_path): + from pyrevit.coreutils.ribbon import load_bitmapimage + tooltip_bitmap = load_bitmapimage(tooltip_image_path) + member.ToolTipImage = tooltip_bitmap + mlogger.debug('COMBOBOX: Set tooltip image for member: %s', member_text) + except Exception as tooltip_image_err: + mlogger.debug('COMBOBOX: Error setting member tooltip image: %s', tooltip_image_err) + + except Exception as add_err: + mlogger.warning('COMBOBOX: Error adding member: %s', add_err) # Set Current to first item - items = combobox_obj.GetItems() + # Note: Setting current will overwrite ItemText with the selected item's text + # This is expected behavior - ComboBox.ItemText shows the current selection + items = combobox_ui.get_items() if items and len(items) > 0: - combobox_obj.Current = items[0] - combobox_obj.ItemText = items[0].ItemText - mlogger.warning('COMBOBOX: Set current item: %s', items[0].ItemText) + try: + combobox_ui.current = items[0] + mlogger.warning('COMBOBOX: Set current item: %s (ItemText now shows: %s)', + items[0].ItemText, combobox_obj.ItemText if hasattr(combobox_obj, 'ItemText') else 'N/A') + except Exception as current_err: + mlogger.debug('COMBOBOX: Error setting current item: %s', current_err) # Call __selfinit__ on script (SmartButton pattern) try: From 90bf7a3e5461a65556fbcde3e87cfd323e7ab07d Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Tue, 25 Nov 2025 14:14:18 +0000 Subject: [PATCH 07/12] feat: Complete ComboBox implementation with full API support - Added comprehensive ComboBox property support (Name, ToolTip, Image, ItemText, Current, etc.) - Implemented all ComboBox methods (add_item, add_items, add_separator, get_items, etc.) - Fixed member properties by preserving full member dictionaries in components.py (was converting to tuples) - Fixed member icon/tooltip/group properties by setting on ComboBoxMember object after AddItem - Added encoding declaration to uimaker.py to fix non-ASCII character error - Updated add_item() to return ComboBoxMember for property setting - Full support for ComboBoxMemberData properties (icon, tooltip, group, tooltip_ext, tooltip_image) - Removed all debugging logging statements --- pyrevitlib/pyrevit/extensions/components.py | 6 - pyrevitlib/pyrevit/loader/uimaker.py | 150 +++++--------------- 2 files changed, 35 insertions(+), 121 deletions(-) diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index dda0920be..b1689e45c 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -305,12 +305,10 @@ class ComboBoxGroup(GenericUICommandGroup): def __init__(self, cmp_path=None): GenericUICommandGroup.__init__(self, cmp_path=cmp_path) self.members = [] - mlogger.warning('=== ComboBoxGroup created: %s (path: %s) ===', self.name, cmp_path) # Read members from metadata if self.meta: raw_members = self.meta.get('members', []) - mlogger.warning('ComboBoxGroup %s metadata members: %s', self.name, raw_members) if isinstance(raw_members, list): # Process list of members - preserve full dict for rich metadata (icons, tooltips, etc.) processed_members = [] @@ -329,10 +327,6 @@ def __init__(self, cmp_path=None): elif isinstance(raw_members, dict): # Dict format: {'A': 'Option A'} - convert to list of dicts self.members = [{'id': k, 'text': v} for k, v in raw_members.items()] - else: - mlogger.warning('ComboBoxGroup %s has no metadata', self.name) - - mlogger.warning('ComboBoxGroup %s final members: %s', self.name, self.members) class SplitPushButtonGroup(GenericUICommandGroup): diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 7d430870b..3c9c96040 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -433,105 +433,64 @@ def _produce_ui_pulldown(ui_maker_params): return None def _produce_ui_combobox(ui_maker_params): - """Create a ComboBox - bare minimum implementation. + """Create a ComboBox with full property support. Args: ui_maker_params (UIMakerParams): Standard parameters for making ui item. """ - mlogger.warning('=== COMBOBOX: _produce_ui_combobox CALLED ===') try: parent_ribbon_panel = ui_maker_params.parent_ui combobox = ui_maker_params.component combobox_name = getattr(combobox, 'name', 'unknown') - mlogger.warning('COMBOBOX: Function called for: %s', combobox_name) - # Validate inputs first if not combobox: - mlogger.error('COMBOBOX: Component is None') + mlogger.error('Component is None') return None if not parent_ribbon_panel: - mlogger.error('COMBOBOX: Parent UI is None for: %s', combobox_name) + mlogger.error('Parent UI is None for: %s', combobox_name) return None - # Panel info - try: - panel_name = getattr(parent_ribbon_panel, 'name', 'unknown') - panel_type = str(type(parent_ribbon_panel)) - mlogger.warning('COMBOBOX: Parent panel: %s (type: %s)', panel_name, panel_type) - except Exception: - pass - # Get panel API object try: panel_rvtapi = parent_ribbon_panel.get_rvtapi_object() if not panel_rvtapi: - mlogger.error('COMBOBOX: Panel Revit API object is None for: %s', combobox_name) + mlogger.error('Panel Revit API object is None for: %s', combobox_name) return None except Exception as panel_err: - mlogger.error('COMBOBOX: Could not get panel Revit API object: %s', panel_err) + mlogger.error('Could not get panel Revit API object: %s', panel_err) return None - # Log items before - try: - existing_items = panel_rvtapi.GetItems() - if existing_items: - item_count = len(existing_items) - mlogger.warning('COMBOBOX: Panel has %d items before adding ComboBox', item_count) - for idx, item in enumerate(existing_items): - mlogger.warning( - 'COMBOBOX: Existing item %d: %s (type: %s)', - idx, getattr(item, 'Name', 'unknown'), type(item).__name__ - ) - except Exception as log_err: - mlogger.debug('COMBOBOX: Could not log existing items: %s', log_err) - # Create combobox - mlogger.warning('COMBOBOX: About to create ComboBox: %s', combobox_name) - mlogger.warning('COMBOBOX: combobox.directory: %s', getattr(combobox, 'directory', 'N/A')) try: parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) - mlogger.warning('COMBOBOX: create_combobox() completed') except Exception as create_err: - mlogger.error('COMBOBOX: Error calling create_combobox: %s', create_err) + mlogger.error('Error calling create_combobox: %s', create_err) import traceback - mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) + mlogger.error('Traceback: %s', traceback.format_exc()) return None combobox_ui = parent_ribbon_panel.ribbon_item(combobox_name) if not combobox_ui: - mlogger.error('COMBOBOX: Failed to get ComboBox UI item: %s', combobox_name) + mlogger.error('Failed to get ComboBox UI item: %s', combobox_name) return None - mlogger.warning('COMBOBOX: Got combobox_ui: %s', type(combobox_ui).__name__) # Get the Revit API ComboBox object try: combobox_obj = combobox_ui.get_rvtapi_object() - mlogger.warning('COMBOBOX: Got combobox_obj: %s', type(combobox_obj).__name__ if combobox_obj else 'None') except Exception as rvtapi_err: mlogger.error( - 'COMBOBOX: get_rvtapi_object() failed for %s: %s', + 'get_rvtapi_object() failed for %s: %s', combobox_name, rvtapi_err ) return None if not combobox_obj: - mlogger.error('COMBOBOX: get_rvtapi_object() returned None for: %s', combobox_name) + mlogger.error('get_rvtapi_object() returned None for: %s', combobox_name) return None - # Log type - try: - obj_type = combobox_obj.GetType() - mlogger.warning('COMBOBOX: rvtapi object type: %s', obj_type) - except Exception: - mlogger.warning('COMBOBOX: rvtapi object type (python): %s', type(combobox_obj)) - # IMPORTANT: only bail here if it is still ComboBoxData if isinstance(combobox_obj, UI.ComboBoxData): - mlogger.warning( - 'COMBOBOX: %s is still UI.ComboBoxData (itemdata_mode). ' - 'Skipping event wiring for now.', combobox_name - ) return combobox_ui # <- ONLY in this branch # From here on we have a real Autodesk.Revit.UI.ComboBox @@ -546,9 +505,8 @@ def _produce_ui_combobox(ui_maker_params): try: # Set initial ItemText to title (will be overwritten when current item is set) combobox_obj.ItemText = combobox_title - mlogger.warning('COMBOBOX: Set initial ItemText to title: %s', combobox_title) except Exception as title_err: - mlogger.debug('COMBOBOX: Could not set ItemText: %s', title_err) + mlogger.debug('Could not set ItemText: %s', title_err) # Set icon if available parent = ui_maker_params.parent_cmp @@ -556,54 +514,43 @@ def _produce_ui_combobox(ui_maker_params): if icon_file: try: combobox_ui.set_icon(icon_file, icon_size=ICON_MEDIUM) - mlogger.debug('COMBOBOX: Set icon: %s', icon_file) except Exception as icon_err: - mlogger.debug('COMBOBOX: Error setting icon: %s', icon_err) + mlogger.debug('Error setting icon: %s', icon_err) # Set tooltip if available tooltip = getattr(combobox, 'tooltip', None) if tooltip: try: combobox_ui.set_tooltip(tooltip) - mlogger.debug('COMBOBOX: Set tooltip') except Exception as tooltip_err: - mlogger.debug('COMBOBOX: Error setting tooltip: %s', tooltip_err) + mlogger.debug('Error setting tooltip: %s', tooltip_err) # Set extended tooltip if available tooltip_ext = getattr(combobox, 'tooltip_ext', None) if tooltip_ext: try: combobox_ui.set_tooltip_ext(tooltip_ext) - mlogger.debug('COMBOBOX: Set extended tooltip') except Exception as tooltip_ext_err: - mlogger.debug('COMBOBOX: Error setting extended tooltip: %s', tooltip_ext_err) + mlogger.debug('Error setting extended tooltip: %s', tooltip_ext_err) # Set tooltip media (image/video) if available tooltip_media = getattr(combobox, 'media_file', None) if tooltip_media: try: combobox_ui.set_tooltip_media(tooltip_media) - mlogger.debug('COMBOBOX: Set tooltip media: %s', tooltip_media) except Exception as tooltip_media_err: - mlogger.debug('COMBOBOX: Error setting tooltip media: %s', tooltip_media_err) + mlogger.debug('Error setting tooltip media: %s', tooltip_media_err) # Set contextual help if available help_url = getattr(combobox, 'help_url', None) if help_url: try: combobox_ui.set_contexthelp(help_url) - mlogger.debug('COMBOBOX: Set contextual help') except Exception as help_err: - mlogger.debug('COMBOBOX: Error setting contextual help: %s', help_err) + mlogger.debug('Error setting contextual help: %s', help_err) - # Check if ComboBox already has members (from cache/previous load) - existing_items = combobox_ui.get_items() - if existing_items: - mlogger.warning('COMBOBOX: Found %d existing members - will add new ones', len(existing_items)) - # Add members from metadata if hasattr(combobox, 'members') and combobox.members: - mlogger.warning('COMBOBOX: Processing %d members from metadata', len(combobox.members)) for member in combobox.members: member_id = None member_text = None @@ -627,13 +574,13 @@ def _produce_ui_combobox(ui_maker_params): member_id = member_text = member else: mlogger.warning( - 'COMBOBOX: Skipping invalid member format: %s (type: %s)', + 'Skipping invalid member format: %s (type: %s)', member, type(member) ) continue if not member_id or not member_text: - mlogger.warning('COMBOBOX: Skipping member with missing id or text') + mlogger.warning('Skipping member with missing id or text') continue # Create member data (minimal - just id and text) @@ -643,11 +590,9 @@ def _produce_ui_combobox(ui_maker_params): try: member = combobox_ui.add_item(member_data) if not member: - mlogger.warning('COMBOBOX: AddItem returned None for: %s', member_text) + mlogger.warning('AddItem returned None for: %s', member_text) continue - mlogger.warning('COMBOBOX: Added member: %s (%s), type: %s', member_text, member_id, type(member).__name__) - # Now set properties on the actual ComboBoxMember object (not the data) # Set member icon if available @@ -659,47 +604,34 @@ def _produce_ui_combobox(ui_maker_params): else: icon_path = member_icon - mlogger.warning('COMBOBOX: Attempting to set icon for %s: %s (exists: %s)', - member_text, icon_path, op.exists(icon_path)) - if op.exists(icon_path): button_icon = ribbon.ButtonIcons(icon_path) - mlogger.warning('COMBOBOX: Created ButtonIcons, setting member.Image...') member.Image = button_icon.small_bitmap - mlogger.warning('COMBOBOX: [OK] Set icon for member: %s (Image type: %s)', - member_text, type(member.Image).__name__ if hasattr(member, 'Image') else 'N/A') else: - mlogger.warning('COMBOBOX: [ERROR] Icon file not found: %s (directory: %s)', - icon_path, combobox.directory) + mlogger.warning('Icon file not found: %s', icon_path) except Exception as member_icon_err: - mlogger.warning('COMBOBOX: [ERROR] Error setting member icon for %s: %s', - member_text, member_icon_err) - import traceback - mlogger.warning('COMBOBOX: Traceback: %s', traceback.format_exc()) + mlogger.debug('Error setting member icon: %s', member_icon_err) # Set member group if available if member_group and hasattr(member, 'GroupName'): try: member.GroupName = member_group - mlogger.debug('COMBOBOX: Set group for member: %s', member_text) except Exception as group_err: - mlogger.debug('COMBOBOX: Error setting member group: %s', group_err) + mlogger.debug('Error setting member group: %s', group_err) # Set member tooltip if available if member_tooltip and hasattr(member, 'ToolTip'): try: member.ToolTip = member_tooltip - mlogger.debug('COMBOBOX: Set tooltip for member: %s', member_text) except Exception as tooltip_err: - mlogger.debug('COMBOBOX: Error setting member tooltip: %s', tooltip_err) + mlogger.debug('Error setting member tooltip: %s', tooltip_err) # Set member extended tooltip if available if member_tooltip_ext and hasattr(member, 'LongDescription'): try: member.LongDescription = member_tooltip_ext - mlogger.debug('COMBOBOX: Set extended tooltip for member: %s', member_text) except Exception as tooltip_ext_err: - mlogger.debug('COMBOBOX: Error setting member extended tooltip: %s', tooltip_ext_err) + mlogger.debug('Error setting member extended tooltip: %s', tooltip_ext_err) # Set member tooltip image if available if member_tooltip_image and hasattr(member, 'ToolTipImage'): @@ -714,12 +646,11 @@ def _produce_ui_combobox(ui_maker_params): from pyrevit.coreutils.ribbon import load_bitmapimage tooltip_bitmap = load_bitmapimage(tooltip_image_path) member.ToolTipImage = tooltip_bitmap - mlogger.debug('COMBOBOX: Set tooltip image for member: %s', member_text) except Exception as tooltip_image_err: - mlogger.debug('COMBOBOX: Error setting member tooltip image: %s', tooltip_image_err) + mlogger.debug('Error setting member tooltip image: %s', tooltip_image_err) except Exception as add_err: - mlogger.warning('COMBOBOX: Error adding member: %s', add_err) + mlogger.warning('Error adding member: %s', add_err) # Set Current to first item # Note: Setting current will overwrite ItemText with the selected item's text @@ -728,10 +659,8 @@ def _produce_ui_combobox(ui_maker_params): if items and len(items) > 0: try: combobox_ui.current = items[0] - mlogger.warning('COMBOBOX: Set current item: %s (ItemText now shows: %s)', - items[0].ItemText, combobox_obj.ItemText if hasattr(combobox_obj, 'ItemText') else 'N/A') except Exception as current_err: - mlogger.debug('COMBOBOX: Error setting current item: %s', current_err) + mlogger.debug('Error setting current item: %s', current_err) # Call __selfinit__ on script (SmartButton pattern) try: @@ -742,7 +671,6 @@ def _produce_ui_combobox(ui_maker_params): script_path = op.join(combobox.directory, 'script.py') if op.exists(script_path): combobox_script_file = script_path - mlogger.debug('COMBOBOX: Found script.py via directory fallback: %s', script_path) if combobox_script_file and combobox_unique_name: current_paths = list(sys.path) @@ -758,15 +686,10 @@ def _produce_ui_combobox(ui_maker_params): res = imported_script.__selfinit__(combobox, combobox_ui, HOST_APP.uiapp) if res is False: combobox_ui.deactivate() - mlogger.warning('COMBOBOX: Ran __selfinit__ for script: %s', combobox_name) - else: - mlogger.debug('COMBOBOX: Script has no __selfinit__: %s', combobox_script_file) - else: - mlogger.debug('COMBOBOX: No script metadata available for: %s', combobox_name) except Exception as init_err: - mlogger.error('COMBOBOX: Error in __selfinit__: %s', init_err) + mlogger.error('Error in __selfinit__: %s', init_err) import traceback - mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) + mlogger.error('Traceback: %s', traceback.format_exc()) # Ensure visible & enabled try: @@ -774,29 +697,26 @@ def _produce_ui_combobox(ui_maker_params): combobox_obj.Visible = True if hasattr(combobox_obj, 'Enabled'): combobox_obj.Enabled = True - mlogger.warning('COMBOBOX: Set Visible=True, Enabled=True') except Exception as vis_err: - mlogger.debug('COMBOBOX: Could not set visibility: %s', vis_err) + mlogger.debug('Could not set visibility: %s', vis_err) # Activate UI item try: combobox_ui.activate() - mlogger.warning('COMBOBOX: Activated ComboBox UI item') except Exception as activate_err: - mlogger.warning('COMBOBOX: Could not activate: %s', activate_err) + mlogger.debug('Could not activate: %s', activate_err) - mlogger.warning('COMBOBOX: Created successfully: %s', combobox_name) return combobox_ui except PyRevitException as err: - mlogger.error('COMBOBOX: UI error creating: %s', err.msg) + mlogger.error('UI error creating ComboBox: %s', err.msg) import traceback - mlogger.error('COMBOBOX: Traceback: %s', traceback.format_exc()) + mlogger.error('Traceback: %s', traceback.format_exc()) return None except Exception as err: - mlogger.error('COMBOBOX: Error creating: %s', err) + mlogger.error('Error creating ComboBox: %s', err) import traceback - mlogger.error('COMBOBOX: Full traceback: %s', traceback.format_exc()) + mlogger.error('Full traceback: %s', traceback.format_exc()) return None From 748fa40f035dad411d708335b783d9d05efcafce Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Tue, 25 Nov 2025 14:26:43 +0000 Subject: [PATCH 08/12] style: Format ComboBox implementation files with Black - Formatted components.py, ribbon.py, and uimaker.py with Black - Ensures code follows PEP 8 style guidelines --- pyrevitlib/pyrevit/coreutils/ribbon.py | 986 +++++++++++--------- pyrevitlib/pyrevit/extensions/components.py | 302 +++--- pyrevitlib/pyrevit/loader/uimaker.py | 605 ++++++------ 3 files changed, 1071 insertions(+), 822 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index d9df58080..1834b3484 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -1,7 +1,8 @@ """Base module to interact with Revit ribbon.""" + from collections import OrderedDict -#pylint: disable=W0703,C0302,C0103 +# pylint: disable=W0703,C0302,C0103 from pyrevit import HOST_APP, EXEC_PARAMS, PyRevitException from pyrevit.compat import safe_strtype from pyrevit import coreutils @@ -20,7 +21,7 @@ mlogger = get_logger(__name__) -PYREVIT_TAB_IDENTIFIER = 'pyrevit_tab' +PYREVIT_TAB_IDENTIFIER = "pyrevit_tab" ICON_SMALL = 16 ICON_MEDIUM = 24 @@ -28,10 +29,10 @@ DEFAULT_DPI = 96 -DEFAULT_TOOLTIP_IMAGE_FORMAT = '.png' -DEFAULT_TOOLTIP_VIDEO_FORMAT = '.swf' +DEFAULT_TOOLTIP_IMAGE_FORMAT = ".png" +DEFAULT_TOOLTIP_VIDEO_FORMAT = ".swf" if HOST_APP.is_newer_than(2019, or_equal=True): - DEFAULT_TOOLTIP_VIDEO_FORMAT = '.mp4' + DEFAULT_TOOLTIP_VIDEO_FORMAT = ".mp4" def argb_to_brush(argb_color): @@ -43,13 +44,14 @@ def argb_to_brush(argb_color): r = argb_color[-6:-4] if len(argb_color) > 7: a = argb_color[-8:-6] - return Media.SolidColorBrush(Media.Color.FromArgb( + return Media.SolidColorBrush( + Media.Color.FromArgb( Convert.ToInt32("0x" + a, 16), Convert.ToInt32("0x" + r, 16), Convert.ToInt32("0x" + g, 16), - Convert.ToInt32("0x" + b, 16) - ) + Convert.ToInt32("0x" + b, 16), ) + ) except Exception as color_ex: mlogger.error("Bad color format %s | %s", argb_color, color_ex) @@ -76,6 +78,7 @@ def load_bitmapimage(image_file): # Helper classes and functions ------------------------------------------------- class PyRevitUIError(PyRevitException): """Common base class for all pyRevit ui-related exceptions.""" + pass @@ -92,12 +95,13 @@ class ButtonIcons(object): icon_file_path (str): icon image file path filestream (IO.FileStream): io stream containing image binary data """ + def __init__(self, image_file): self.icon_file_path = image_file self.check_icon_size() - self.filestream = IO.FileStream(image_file, - IO.FileMode.Open, - IO.FileAccess.Read) + self.filestream = IO.FileStream( + image_file, IO.FileMode.Open, IO.FileAccess.Read + ) @staticmethod def recolour(image_data, size, stride, color): @@ -111,20 +115,22 @@ def recolour(image_data, size, stride, color): # G = image_data[idx+1] # B = image_data[idx] # luminance = (0.299*R + 0.587*G + 0.114*B) - image_data[idx] = color >> 0 & 0xff # blue - image_data[idx+1] = color >> 8 & 0xff # green - image_data[idx+2] = color >> 16 & 0xff # red + image_data[idx] = color >> 0 & 0xFF # blue + image_data[idx + 1] = color >> 8 & 0xFF # green + image_data[idx + 2] = color >> 16 & 0xFF # red def check_icon_size(self): """Verify icon size is within acceptable range.""" image = System.Drawing.Image.FromFile(self.icon_file_path) image_size = max(image.Width, image.Height) if image_size > 96: - mlogger.warning('Icon file is too large. Large icons adversely ' - 'affect the load time since they need to be ' - 'processed and adjusted for screen scaling. ' - 'Keep icons at max 96x96 pixels: %s', - self.icon_file_path) + mlogger.warning( + "Icon file is too large. Large icons adversely " + "affect the load time since they need to be " + "processed and adjusted for screen scaling. " + "Keep icons at max 96x96 pixels: %s", + self.icon_file_path, + ) def create_bitmap(self, icon_size): """Resamples image and creates bitmap for the given size. @@ -137,8 +143,9 @@ def create_bitmap(self, icon_size): Returns: (Imaging.BitmapSource): object containing image data at given size """ - mlogger.debug('Creating %sx%s bitmap from: %s', - icon_size, icon_size, self.icon_file_path) + mlogger.debug( + "Creating %sx%s bitmap from: %s", icon_size, icon_size, self.icon_file_path + ) adjusted_icon_size = icon_size * 2 adjusted_dpi = DEFAULT_DPI * 2 screen_scaling = HOST_APP.proc_screen_scalefactor @@ -163,13 +170,16 @@ def create_bitmap(self, icon_size): scaled_size = int(adjusted_icon_size * screen_scaling) scaled_dpi = int(adjusted_dpi * screen_scaling) - bitmap_source = \ - Imaging.BitmapSource.Create(scaled_size, scaled_size, - scaled_dpi, scaled_dpi, - image_format, - palette, - image_data, - stride) + bitmap_source = Imaging.BitmapSource.Create( + scaled_size, + scaled_size, + scaled_dpi, + scaled_dpi, + image_format, + palette, + image_data, + stride, + ) return bitmap_source @property @@ -208,8 +218,9 @@ class GenericPyRevitUIContainer(object): name (str): container name itemdata_mode (bool): if container is wrapping UI.*ItemData """ + def __init__(self): - self.name = '' + self.name = "" self._rvtapi_object = None self._sub_pyrvt_components = OrderedDict() self.itemdata_mode = False @@ -221,15 +232,15 @@ def __iter__(self): return iter(self._sub_pyrvt_components.values()) def __repr__(self): - return 'Name: {} RevitAPIObject: {}'.format(self.name, - self._rvtapi_object) + return "Name: {} RevitAPIObject: {}".format(self.name, self._rvtapi_object) def _get_component(self, cmp_name): try: return self._sub_pyrvt_components[cmp_name] except KeyError: - raise PyRevitUIError('Can not retrieve item {} from {}' - .format(cmp_name, self)) + raise PyRevitUIError( + "Can not retrieve item {} from {}".format(cmp_name, self) + ) def _add_component(self, new_component): self._sub_pyrvt_components[new_component.name] = new_component @@ -238,24 +249,25 @@ def _remove_component(self, expired_cmp_name): try: self._sub_pyrvt_components.pop(expired_cmp_name) except KeyError: - raise PyRevitUIError('Can not remove item {} from {}' - .format(expired_cmp_name, self)) + raise PyRevitUIError( + "Can not remove item {} from {}".format(expired_cmp_name, self) + ) @property def visible(self): """Is container visible.""" - if hasattr(self._rvtapi_object, 'Visible'): + if hasattr(self._rvtapi_object, "Visible"): return self._rvtapi_object.Visible - elif hasattr(self._rvtapi_object, 'IsVisible'): + elif hasattr(self._rvtapi_object, "IsVisible"): return self._rvtapi_object.IsVisible else: return self._visible @visible.setter def visible(self, value): - if hasattr(self._rvtapi_object, 'Visible'): + if hasattr(self._rvtapi_object, "Visible"): self._rvtapi_object.Visible = value - elif hasattr(self._rvtapi_object, 'IsVisible'): + elif hasattr(self._rvtapi_object, "IsVisible"): self._rvtapi_object.IsVisible = value else: self._visible = value @@ -263,18 +275,18 @@ def visible(self, value): @property def enabled(self): """Is container enabled.""" - if hasattr(self._rvtapi_object, 'Enabled'): + if hasattr(self._rvtapi_object, "Enabled"): return self._rvtapi_object.Enabled - elif hasattr(self._rvtapi_object, 'IsEnabled'): + elif hasattr(self._rvtapi_object, "IsEnabled"): return self._rvtapi_object.IsEnabled else: return self._enabled @enabled.setter def enabled(self, value): - if hasattr(self._rvtapi_object, 'Enabled'): + if hasattr(self._rvtapi_object, "Enabled"): self._rvtapi_object.Enabled = value - elif hasattr(self._rvtapi_object, 'IsEnabled'): + elif hasattr(self._rvtapi_object, "IsEnabled"): self._rvtapi_object.IsEnabled = value else: self._enabled = value @@ -284,15 +296,17 @@ def process_deferred(self): if self._visible is not None: self.visible = self._visible except Exception as visible_err: - raise PyRevitUIError('Error setting .visible {} | {} ' - .format(self, visible_err)) + raise PyRevitUIError( + "Error setting .visible {} | {} ".format(self, visible_err) + ) try: if self._enabled is not None: self.enabled = self._enabled except Exception as enable_err: - raise PyRevitUIError('Error setting .enabled {} | {} ' - .format(self, enable_err)) + raise PyRevitUIError( + "Error setting .enabled {} | {} ".format(self, enable_err) + ) def get_rvtapi_object(self): """Return underlying Revit API object for this container.""" @@ -314,11 +328,9 @@ def get_adwindows_object(self): """Return underlying AdWindows API object for this container.""" # FIXME: return type rvtapi_obj = self._rvtapi_object - getRibbonItemMethod = \ - rvtapi_obj.GetType().GetMethod( - 'getRibbonItem', - BindingFlags.NonPublic | BindingFlags.Instance - ) + getRibbonItemMethod = rvtapi_obj.GetType().GetMethod( + "getRibbonItem", BindingFlags.NonPublic | BindingFlags.Instance + ) if getRibbonItemMethod: return getRibbonItemMethod.Invoke(rvtapi_obj, None) @@ -397,8 +409,7 @@ def find_child(self, child_name): for sub_cmp in self._sub_pyrvt_components.values(): if child_name == sub_cmp.name: return sub_cmp - elif hasattr(sub_cmp, 'ui_title') \ - and child_name == sub_cmp.ui_title: + elif hasattr(sub_cmp, "ui_title") and child_name == sub_cmp.ui_title: return sub_cmp component = sub_cmp.find_child(child_name) @@ -414,7 +425,7 @@ def activate(self): self.visible = True self._dirty = True except Exception: - raise PyRevitUIError('Can not activate: {}'.format(self)) + raise PyRevitUIError("Can not activate: {}".format(self)) def deactivate(self): """Deactivate this container in ui.""" @@ -423,7 +434,7 @@ def deactivate(self): self.visible = False self._dirty = True except Exception: - raise PyRevitUIError('Can not deactivate: {}'.format(self)) + raise PyRevitUIError("Can not deactivate: {}".format(self)) def get_updated_items(self): # FIXME: reduntant, this is a use case and should be on uimaker side? @@ -442,7 +453,7 @@ def reorder_before(self, item_name, ritem_name): """ apiobj = self.get_rvtapi_object() litem_idx = ritem_idx = None - if hasattr(apiobj, 'Panels'): + if hasattr(apiobj, "Panels"): for item in apiobj.Panels: if item.Source.AutomationName == item_name: litem_idx = apiobj.Panels.IndexOf(item) @@ -463,7 +474,7 @@ def reorder_beforeall(self, item_name): # FIXME: verify docs description is correct apiobj = self.get_rvtapi_object() litem_idx = None - if hasattr(apiobj, 'Panels'): + if hasattr(apiobj, "Panels"): for item in apiobj.Panels: if item.Source.AutomationName == item_name: litem_idx = apiobj.Panels.IndexOf(item) @@ -479,7 +490,7 @@ def reorder_after(self, item_name, ritem_name): """ apiobj = self.get_rvtapi_object() litem_idx = ritem_idx = None - if hasattr(apiobj, 'Panels'): + if hasattr(apiobj, "Panels"): for item in apiobj.Panels: if item.Source.AutomationName == item_name: litem_idx = apiobj.Panels.IndexOf(item) @@ -499,7 +510,7 @@ def reorder_afterall(self, item_name): """ apiobj = self.get_rvtapi_object() litem_idx = None - if hasattr(apiobj, 'Panels'): + if hasattr(apiobj, "Panels"): for item in apiobj.Panels: if item.Source.AutomationName == item_name: litem_idx = apiobj.Panels.IndexOf(item) @@ -512,6 +523,7 @@ def reorder_afterall(self, item_name): # (These elements are native and can not be modified) -------------------------- class GenericRevitNativeUIContainer(GenericPyRevitUIContainer): """Common base type for native Revit API UI containers.""" + def __init__(self): GenericPyRevitUIContainer.__init__(self) @@ -534,23 +546,24 @@ def deactivate(self): Under current implementation, raises PyRevitUIError exception as native Revit API UI components should not be changed. """ - raise PyRevitUIError('Can not de/activate native item: {}' - .format(self)) + raise PyRevitUIError("Can not de/activate native item: {}".format(self)) class RevitNativeRibbonButton(GenericRevitNativeUIContainer): """Revit API UI native ribbon button.""" + def __init__(self, adwnd_ribbon_button): GenericRevitNativeUIContainer.__init__(self) - self.name = \ - safe_strtype(adwnd_ribbon_button.AutomationName)\ - .replace('\r\n', ' ') + self.name = safe_strtype(adwnd_ribbon_button.AutomationName).replace( + "\r\n", " " + ) self._rvtapi_object = adwnd_ribbon_button class RevitNativeRibbonGroupItem(GenericRevitNativeUIContainer): """Revit API UI native ribbon button.""" + def __init__(self, adwnd_ribbon_item): GenericRevitNativeUIContainer.__init__(self) @@ -575,6 +588,7 @@ def button(self, name): class RevitNativeRibbonPanel(GenericRevitNativeUIContainer): """Revit API UI native ribbon button.""" + def __init__(self, adwnd_ribbon_panel): GenericRevitNativeUIContainer.__init__(self) @@ -594,34 +608,35 @@ def __init__(self, adwnd_ribbon_panel): for sub_rvtapi_item in adwnd_ribbon_item.Items: all_adwnd_ribbon_items.append(sub_rvtapi_item) except Exception as append_err: - mlogger.debug('Can not get RibbonFoldPanel children: %s ' - '| %s', adwnd_ribbon_item, append_err) + mlogger.debug( + "Can not get RibbonFoldPanel children: %s " "| %s", + adwnd_ribbon_item, + append_err, + ) else: all_adwnd_ribbon_items.append(adwnd_ribbon_item) # processing the panel slideout for exising ribbon items - for adwnd_slideout_item \ - in adwnd_ribbon_panel.Source.SlideOutPanelItemsView: + for adwnd_slideout_item in adwnd_ribbon_panel.Source.SlideOutPanelItemsView: all_adwnd_ribbon_items.append(adwnd_slideout_item) # processing the cleaned children list and # creating pyRevit native ribbon objects for adwnd_ribbon_item in all_adwnd_ribbon_items: try: - if isinstance(adwnd_ribbon_item, - AdWindows.RibbonButton) \ - or isinstance(adwnd_ribbon_item, - AdWindows.RibbonToggleButton): - self._add_component( - RevitNativeRibbonButton(adwnd_ribbon_item)) - elif isinstance(adwnd_ribbon_item, - AdWindows.RibbonSplitButton): - self._add_component( - RevitNativeRibbonGroupItem(adwnd_ribbon_item)) + if isinstance(adwnd_ribbon_item, AdWindows.RibbonButton) or isinstance( + adwnd_ribbon_item, AdWindows.RibbonToggleButton + ): + self._add_component(RevitNativeRibbonButton(adwnd_ribbon_item)) + elif isinstance(adwnd_ribbon_item, AdWindows.RibbonSplitButton): + self._add_component(RevitNativeRibbonGroupItem(adwnd_ribbon_item)) except Exception as append_err: - mlogger.debug('Can not create native ribbon item: %s ' - '| %s', adwnd_ribbon_item, append_err) + mlogger.debug( + "Can not create native ribbon item: %s " "| %s", + adwnd_ribbon_item, + append_err, + ) def ribbon_item(self, item_name): """Get panel item with given name. @@ -639,6 +654,7 @@ def ribbon_item(self, item_name): class RevitNativeRibbonTab(GenericRevitNativeUIContainer): """Revit API UI native ribbon tab.""" + def __init__(self, adwnd_ribbon_tab): GenericRevitNativeUIContainer.__init__(self) @@ -650,12 +666,13 @@ def __init__(self, adwnd_ribbon_tab): for adwnd_ribbon_panel in adwnd_ribbon_tab.Panels: # only listing visible panels if adwnd_ribbon_panel.IsVisible: - self._add_component( - RevitNativeRibbonPanel(adwnd_ribbon_panel) - ) + self._add_component(RevitNativeRibbonPanel(adwnd_ribbon_panel)) except Exception as append_err: - mlogger.debug('Can not get native panels for this native tab: %s ' - '| %s', adwnd_ribbon_tab, append_err) + mlogger.debug( + "Can not get native panels for this native tab: %s " "| %s", + adwnd_ribbon_tab, + append_err, + ) def ribbon_panel(self, panel_name): """Get panel with given name. @@ -692,8 +709,7 @@ def __init__(self, ribbon_button): # when container is in itemdata_mode, self._rvtapi_object is a # RibbonItemData and not an actual ui item a sunsequent call to # create_data_items will create ui for RibbonItemData objects - self.itemdata_mode = isinstance(self._rvtapi_object, - UI.RibbonItemData) + self.itemdata_mode = isinstance(self._rvtapi_object, UI.RibbonItemData) self.ui_title = self.name if not self.itemdata_mode: @@ -717,8 +733,9 @@ def set_icon(self, icon_file, icon_size=ICON_MEDIUM): rvtapi_obj.LargeImage = button_icon.medium_bitmap self._dirty = True except Exception as icon_err: - raise PyRevitUIError('Error in applying icon to button > {} : {}' - .format(icon_file, icon_err)) + raise PyRevitUIError( + "Error in applying icon to button > {} : {}".format(icon_file, icon_err) + ) def set_tooltip(self, tooltip): try: @@ -730,8 +747,9 @@ def set_tooltip(self, tooltip): adwindows_obj.ToolTip.Content = None self._dirty = True except Exception as tooltip_err: - raise PyRevitUIError('Item does not have tooltip property: {}' - .format(tooltip_err)) + raise PyRevitUIError( + "Item does not have tooltip property: {}".format(tooltip_err) + ) def set_tooltip_ext(self, tooltip_ext): try: @@ -743,8 +761,10 @@ def set_tooltip_ext(self, tooltip_ext): adwindows_obj.ToolTip.ExpandedContent = None self._dirty = True except Exception as tooltip_err: - raise PyRevitUIError('Item does not have extended ' - 'tooltip property: {}'.format(tooltip_err)) + raise PyRevitUIError( + "Item does not have extended " + "tooltip property: {}".format(tooltip_err) + ) def set_tooltip_image(self, tooltip_image): try: @@ -765,8 +785,11 @@ def set_tooltip_image(self, tooltip_image): else: self.tooltip_image = tooltip_image except Exception as ttimage_err: - raise PyRevitUIError('Error setting tooltip image {} | {} ' - .format(tooltip_image, ttimage_err)) + raise PyRevitUIError( + "Error setting tooltip image {} | {} ".format( + tooltip_image, ttimage_err + ) + ) def set_tooltip_video(self, tooltip_video): try: @@ -787,10 +810,12 @@ def set_tooltip_video(self, tooltip_video): def on_media_ended(sender, args): sender.Position = System.TimeSpan.Zero sender.Play() + _video.MediaEnded += on_media_ended def on_loaded(sender, args): sender.Play() + _video.Loaded += on_loaded _StackPanel.Children.Add(_video) adwindows_obj.ToolTip.ExpandedContent = _StackPanel @@ -798,8 +823,11 @@ def on_loaded(sender, args): else: self.tooltip_video = tooltip_video except Exception as ttvideo_err: - raise PyRevitUIError('Error setting tooltip video {} | {} ' - .format(tooltip_video, ttvideo_err)) + raise PyRevitUIError( + "Error setting tooltip video {} | {} ".format( + tooltip_video, ttvideo_err + ) + ) def set_tooltip_media(self, tooltip_media): if tooltip_media.endswith(DEFAULT_TOOLTIP_IMAGE_FORMAT): @@ -808,25 +836,24 @@ def set_tooltip_media(self, tooltip_media): self.set_tooltip_video(tooltip_media) def reset_highlights(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.Highlight = \ - coreutils.get_enum_none(AdInternal.Windows.HighlightMode) + adwindows_obj.Highlight = coreutils.get_enum_none( + AdInternal.Windows.HighlightMode + ) def highlight_as_new(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.Highlight = \ - AdInternal.Windows.HighlightMode.New + adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.New def highlight_as_updated(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.Highlight = \ - AdInternal.Windows.HighlightMode.Updated + adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.Updated def process_deferred(self): GenericPyRevitUIContainer.process_deferred(self) @@ -835,15 +862,21 @@ def process_deferred(self): if self.tooltip_image: self.set_tooltip_image(self.tooltip_image) except Exception as ttvideo_err: - raise PyRevitUIError('Error setting deffered tooltip image {} | {} ' - .format(self.tooltip_video, ttvideo_err)) + raise PyRevitUIError( + "Error setting deffered tooltip image {} | {} ".format( + self.tooltip_video, ttvideo_err + ) + ) try: if self.tooltip_video: self.set_tooltip_video(self.tooltip_video) except Exception as ttvideo_err: - raise PyRevitUIError('Error setting deffered tooltip video {} | {} ' - .format(self.tooltip_video, ttvideo_err)) + raise PyRevitUIError( + "Error setting deffered tooltip video {} | {} ".format( + self.tooltip_video, ttvideo_err + ) + ) def get_contexthelp(self): return self.get_rvtapi_object().GetContextualHelp() @@ -869,8 +902,8 @@ def get_title(self): def get_control_id(self): adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, 'Id'): - return getattr(adwindows_obj, 'Id', '') + if adwindows_obj and hasattr(adwindows_obj, "Id"): + return getattr(adwindows_obj, "Id", "") @property def assembly_name(self): @@ -887,42 +920,46 @@ def availability_class_name(self): class _PyRevitRibbonComboBox(GenericPyRevitUIContainer): """Wrapper for Revit API ComboBox with full property support.""" - + def __init__(self, ribbon_combobox): GenericPyRevitUIContainer.__init__(self) - + self.name = ribbon_combobox.Name self._rvtapi_object = ribbon_combobox - + # Check if it's ComboBoxData (itemdata_mode) or actual ComboBox self.itemdata_mode = isinstance(self._rvtapi_object, UI.ComboBoxData) - + self.ui_title = self.name if not self.itemdata_mode: - self.ui_title = self._rvtapi_object.ItemText if hasattr(self._rvtapi_object, 'ItemText') else self.name - + self.ui_title = ( + self._rvtapi_object.ItemText + if hasattr(self._rvtapi_object, "ItemText") + else self.name + ) + # Store deferred tooltip media self.tooltip_image = self.tooltip_video = None - + # Store event handler references to prevent garbage collection self._current_changed_handler = None self._dropdown_opened_handler = None self._dropdown_closed_handler = None - + def set_rvtapi_object(self, rvtapi_obj): """Set underlying Revit API object for this ComboBox. - + Args: rvtapi_obj (obj): Revit API ComboBox object """ GenericPyRevitUIContainer.set_rvtapi_object(self, rvtapi_obj) # update the ui title for the newly added rvtapi_obj - if not self.itemdata_mode and hasattr(self._rvtapi_object, 'ItemText'): + if not self.itemdata_mode and hasattr(self._rvtapi_object, "ItemText"): self._rvtapi_object.ItemText = self.ui_title - + def set_icon(self, icon_file, icon_size=ICON_MEDIUM): """Set icon for the ComboBox. - + Args: icon_file (str): Path to icon image file icon_size (int, optional): Icon size. Defaults to ICON_MEDIUM. @@ -930,21 +967,24 @@ def set_icon(self, icon_file, icon_size=ICON_MEDIUM): try: button_icon = ButtonIcons(icon_file) rvtapi_obj = self.get_rvtapi_object() - if hasattr(rvtapi_obj, 'Image'): + if hasattr(rvtapi_obj, "Image"): rvtapi_obj.Image = button_icon.small_bitmap - if hasattr(rvtapi_obj, 'LargeImage'): + if hasattr(rvtapi_obj, "LargeImage"): if icon_size == ICON_LARGE: rvtapi_obj.LargeImage = button_icon.large_bitmap else: rvtapi_obj.LargeImage = button_icon.medium_bitmap self._dirty = True except Exception as icon_err: - raise PyRevitUIError('Error in applying icon to ComboBox > {} : {}' - .format(icon_file, icon_err)) - + raise PyRevitUIError( + "Error in applying icon to ComboBox > {} : {}".format( + icon_file, icon_err + ) + ) + def set_tooltip(self, tooltip): """Set tooltip for the ComboBox. - + Args: tooltip (str): Tooltip text """ @@ -957,12 +997,13 @@ def set_tooltip(self, tooltip): adwindows_obj.ToolTip.Content = None self._dirty = True except Exception as tooltip_err: - raise PyRevitUIError('Item does not have tooltip property: {}' - .format(tooltip_err)) - + raise PyRevitUIError( + "Item does not have tooltip property: {}".format(tooltip_err) + ) + def set_tooltip_ext(self, tooltip_ext): """Set extended tooltip (long description) for the ComboBox. - + Args: tooltip_ext (str): Extended tooltip text """ @@ -975,12 +1016,14 @@ def set_tooltip_ext(self, tooltip_ext): adwindows_obj.ToolTip.ExpandedContent = None self._dirty = True except Exception as tooltip_err: - raise PyRevitUIError('Item does not have extended ' - 'tooltip property: {}'.format(tooltip_err)) - + raise PyRevitUIError( + "Item does not have extended " + "tooltip property: {}".format(tooltip_err) + ) + def set_tooltip_image(self, tooltip_image): """Set tooltip image for the ComboBox. - + Args: tooltip_image (str): Path to tooltip image file """ @@ -1002,12 +1045,15 @@ def set_tooltip_image(self, tooltip_image): else: self.tooltip_image = tooltip_image except Exception as ttimage_err: - raise PyRevitUIError('Error setting tooltip image {} | {} ' - .format(tooltip_image, ttimage_err)) - + raise PyRevitUIError( + "Error setting tooltip image {} | {} ".format( + tooltip_image, ttimage_err + ) + ) + def set_tooltip_video(self, tooltip_video): """Set tooltip video for the ComboBox. - + Args: tooltip_video (str): Path to tooltip video file """ @@ -1029,10 +1075,12 @@ def set_tooltip_video(self, tooltip_video): def on_media_ended(sender, args): sender.Position = System.TimeSpan.Zero sender.Play() + _video.MediaEnded += on_media_ended def on_loaded(sender, args): sender.Play() + _video.Loaded += on_loaded _StackPanel.Children.Add(_video) adwindows_obj.ToolTip.ExpandedContent = _StackPanel @@ -1040,12 +1088,15 @@ def on_loaded(sender, args): else: self.tooltip_video = tooltip_video except Exception as ttvideo_err: - raise PyRevitUIError('Error setting tooltip video {} | {} ' - .format(tooltip_video, ttvideo_err)) - + raise PyRevitUIError( + "Error setting tooltip video {} | {} ".format( + tooltip_video, ttvideo_err + ) + ) + def set_tooltip_media(self, tooltip_media): """Set tooltip media (image or video) for the ComboBox. - + Args: tooltip_media (str): Path to tooltip media file """ @@ -1053,10 +1104,10 @@ def set_tooltip_media(self, tooltip_media): self.set_tooltip_image(tooltip_media) elif tooltip_media.endswith(DEFAULT_TOOLTIP_VIDEO_FORMAT): self.set_tooltip_video(tooltip_media) - + def set_title(self, ui_title): """Set the display title (ItemText) for the ComboBox. - + Args: ui_title (str): Display title text """ @@ -1064,27 +1115,27 @@ def set_title(self, ui_title): self.ui_title = ui_title self._dirty = True else: - if hasattr(self._rvtapi_object, 'ItemText'): + if hasattr(self._rvtapi_object, "ItemText"): self._rvtapi_object.ItemText = self.ui_title = ui_title self._dirty = True - + def get_title(self): """Get the display title (ItemText) for the ComboBox. - + Returns: (str): Display title text """ if self.itemdata_mode: return self.ui_title else: - if hasattr(self._rvtapi_object, 'ItemText'): + if hasattr(self._rvtapi_object, "ItemText"): return self._rvtapi_object.ItemText return self.ui_title - + @property def current(self): """Get or set the current selected ComboBox member. - + Returns: (UI.ComboBoxMember): Current selected member, or None """ @@ -1094,30 +1145,31 @@ def current(self): return self._rvtapi_object.Current except Exception: return None - + @current.setter def current(self, value): """Set the current selected ComboBox member. - + Args: value (UI.ComboBoxMember): ComboBox member to select """ - if not self.itemdata_mode and hasattr(self._rvtapi_object, 'Current'): + if not self.itemdata_mode and hasattr(self._rvtapi_object, "Current"): try: self._rvtapi_object.Current = value - if value and hasattr(self._rvtapi_object, 'ItemText'): + if value and hasattr(self._rvtapi_object, "ItemText"): self._rvtapi_object.ItemText = value.ItemText self._dirty = True except Exception as current_err: - raise PyRevitUIError('Error setting current item: {}' - .format(current_err)) - + raise PyRevitUIError( + "Error setting current item: {}".format(current_err) + ) + def add_item(self, member_data): """Add a new item to the ComboBox. - + Args: member_data (UI.ComboBoxMemberData): Member data to add - + Returns: (UI.ComboBoxMember): The created ComboBoxMember object, or None """ @@ -1127,13 +1179,14 @@ def add_item(self, member_data): self._dirty = True return member except Exception as add_err: - raise PyRevitUIError('Error adding item to ComboBox: {}' - .format(add_err)) + raise PyRevitUIError( + "Error adding item to ComboBox: {}".format(add_err) + ) return None - + def add_items(self, member_data_list): """Add multiple items to the ComboBox. - + Args: member_data_list (list): List of UI.ComboBoxMemberData objects """ @@ -1142,9 +1195,10 @@ def add_items(self, member_data_list): self._rvtapi_object.AddItems(member_data_list) self._dirty = True except Exception as add_err: - raise PyRevitUIError('Error adding items to ComboBox: {}' - .format(add_err)) - + raise PyRevitUIError( + "Error adding items to ComboBox: {}".format(add_err) + ) + def add_separator(self): """Add a separator to the ComboBox dropdown list.""" if not self.itemdata_mode: @@ -1152,12 +1206,13 @@ def add_separator(self): self._rvtapi_object.AddSeparator() self._dirty = True except Exception as sep_err: - raise PyRevitUIError('Error adding separator to ComboBox: {}' - .format(sep_err)) - + raise PyRevitUIError( + "Error adding separator to ComboBox: {}".format(sep_err) + ) + def get_items(self): """Get a copy of the collection of ComboBoxMembers. - + Returns: (list): List of UI.ComboBoxMember objects """ @@ -1167,25 +1222,25 @@ def get_items(self): return list(self._rvtapi_object.GetItems()) except Exception: return [] - + def get_contexthelp(self): """Get contextual help for the ComboBox. - + Returns: (UI.ContextualHelp): Contextual help object """ return self.get_rvtapi_object().GetContextualHelp() - + def set_contexthelp(self, ctxhelpurl): """Set contextual help for the ComboBox. - + Args: ctxhelpurl (str): URL for contextual help """ if ctxhelpurl: ch = UI.ContextualHelp(UI.ContextualHelpType.Url, ctxhelpurl) self.get_rvtapi_object().SetContextualHelp(ch) - + def process_deferred(self): """Process deferred tooltip media settings.""" GenericPyRevitUIContainer.process_deferred(self) @@ -1194,46 +1249,51 @@ def process_deferred(self): if self.tooltip_image: self.set_tooltip_image(self.tooltip_image) except Exception as ttimage_err: - raise PyRevitUIError('Error setting deferred tooltip image {} | {} ' - .format(self.tooltip_image, ttimage_err)) + raise PyRevitUIError( + "Error setting deferred tooltip image {} | {} ".format( + self.tooltip_image, ttimage_err + ) + ) try: if self.tooltip_video: self.set_tooltip_video(self.tooltip_video) except Exception as ttvideo_err: - raise PyRevitUIError('Error setting deferred tooltip video {} | {} ' - .format(self.tooltip_video, ttvideo_err)) - + raise PyRevitUIError( + "Error setting deferred tooltip video {} | {} ".format( + self.tooltip_video, ttvideo_err + ) + ) + def reset_highlights(self): """Reset highlight state.""" try: - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): - adwindows_obj.Highlight = \ - coreutils.get_enum_none(AdInternal.Windows.HighlightMode) + if adwindows_obj and hasattr(adwindows_obj, "Highlight"): + adwindows_obj.Highlight = coreutils.get_enum_none( + AdInternal.Windows.HighlightMode + ) except Exception: pass # Highlights are optional, fail silently - + def highlight_as_new(self): """Highlight as new item.""" try: - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): - adwindows_obj.Highlight = \ - AdInternal.Windows.HighlightMode.New + if adwindows_obj and hasattr(adwindows_obj, "Highlight"): + adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.New except Exception: pass # Highlights are optional, fail silently - + def highlight_as_updated(self): """Highlight as updated item.""" try: - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, 'Highlight'): - adwindows_obj.Highlight = \ - AdInternal.Windows.HighlightMode.Updated + if adwindows_obj and hasattr(adwindows_obj, "Highlight"): + adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.Updated except Exception: pass # Highlights are optional, fail silently @@ -1253,8 +1313,7 @@ def __init__(self, ribbon_item): # itemdata_mode, only the necessary RibbonItemData objects will # be created for children a sunsequent call to create_data_items # will create ui for RibbonItemData objects - self.itemdata_mode = isinstance(self._rvtapi_object, - UI.RibbonItemData) + self.itemdata_mode = isinstance(self._rvtapi_object, UI.RibbonItemData) # if button group shows the active button icon, then the child # buttons need to have large icons @@ -1271,14 +1330,16 @@ def __init__(self, ribbon_item): self._add_component(_PyRevitRibbonButton(revit_button)) def is_splitbutton(self): - return isinstance(self._rvtapi_object, UI.SplitButton) \ - or isinstance(self._rvtapi_object, UI.SplitButtonData) + return isinstance(self._rvtapi_object, UI.SplitButton) or isinstance( + self._rvtapi_object, UI.SplitButtonData + ) def set_rvtapi_object(self, rvtapi_obj): GenericPyRevitUIContainer.set_rvtapi_object(self, rvtapi_obj) if self.is_splitbutton(): - self.get_rvtapi_object().IsSynchronizedWithCurrentItem = \ + self.get_rvtapi_object().IsSynchronizedWithCurrentItem = ( self._sync_with_cur_item + ) def create_data_items(self): # iterate through data items and their associated revit @@ -1288,8 +1349,9 @@ def create_data_items(self): # create item in ui and get correspoding revit ui objects if isinstance(pyrvt_ui_item, _PyRevitRibbonButton): - rvtapi_ribbon_item = \ - self.get_rvtapi_object().AddPushButton(rvtapi_data_obj) + rvtapi_ribbon_item = self.get_rvtapi_object().AddPushButton( + rvtapi_data_obj + ) rvtapi_ribbon_item.ItemText = pyrvt_ui_item.get_title() # replace data object with the newly create ribbon item @@ -1311,8 +1373,9 @@ def sync_with_current_item(self, state): self._sync_with_cur_item = state self._dirty = True except Exception as sync_item_err: - raise PyRevitUIError('Item is not a split button. ' - '| {}'.format(sync_item_err)) + raise PyRevitUIError( + "Item is not a split button. " "| {}".format(sync_item_err) + ) def set_icon(self, icon_file, icon_size=ICON_LARGE): try: @@ -1325,8 +1388,9 @@ def set_icon(self, icon_file, icon_size=ICON_LARGE): rvtapi_obj.LargeImage = button_icon.medium_bitmap self._dirty = True except Exception as icon_err: - raise PyRevitUIError('Error in applying icon to button > {} : {}' - .format(icon_file, icon_err)) + raise PyRevitUIError( + "Error in applying icon to button > {} : {}".format(icon_file, icon_err) + ) def get_contexthelp(self): return self.get_rvtapi_object().GetContextualHelp() @@ -1337,32 +1401,41 @@ def set_contexthelp(self, ctxhelpurl): self.get_rvtapi_object().SetContextualHelp(ch) def reset_highlights(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.HighlightDropDown = \ - coreutils.get_enum_none(AdInternal.Windows.HighlightMode) + adwindows_obj.HighlightDropDown = coreutils.get_enum_none( + AdInternal.Windows.HighlightMode + ) def highlight_as_new(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.HighlightDropDown = \ - AdInternal.Windows.HighlightMode.New + adwindows_obj.HighlightDropDown = AdInternal.Windows.HighlightMode.New def highlight_as_updated(self): - if hasattr(AdInternal.Windows, 'HighlightMode'): + if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj: - adwindows_obj.HighlightDropDown = \ + adwindows_obj.HighlightDropDown = ( AdInternal.Windows.HighlightMode.Updated + ) - def create_push_button(self, button_name, asm_location, class_name, - icon_path='', - tooltip='', tooltip_ext='', tooltip_media='', - ctxhelpurl=None, - avail_class_name=None, - update_if_exists=False, ui_title=None): + def create_push_button( + self, + button_name, + asm_location, + class_name, + icon_path="", + tooltip="", + tooltip_ext="", + tooltip_media="", + ctxhelpurl=None, + avail_class_name=None, + update_if_exists=False, + ui_title=None, + ): if self.contains(button_name): if update_if_exists: existing_item = self._get_component(button_name) @@ -1374,25 +1447,34 @@ def create_push_button(self, button_name, asm_location, class_name, rvtapi_obj.AssemblyName = asm_location rvtapi_obj.ClassName = class_name if avail_class_name: - existing_item.get_rvtapi_object() \ - .AvailabilityClassName = avail_class_name + existing_item.get_rvtapi_object().AvailabilityClassName = ( + avail_class_name + ) except Exception as asm_update_err: - mlogger.debug('Error updating button asm info: %s ' - '| %s', button_name, asm_update_err) + mlogger.debug( + "Error updating button asm info: %s " "| %s", + button_name, + asm_update_err, + ) if not icon_path: - mlogger.debug('Icon not set for %s', button_name) + mlogger.debug("Icon not set for %s", button_name) else: try: # if button group shows the active button icon, # then the child buttons need to have large icons - existing_item.set_icon(icon_path, - icon_size=ICON_LARGE - if self._use_active_item_icon - else ICON_MEDIUM) + existing_item.set_icon( + icon_path, + icon_size=( + ICON_LARGE + if self._use_active_item_icon + else ICON_MEDIUM + ), + ) except PyRevitUIError as iconerr: - mlogger.error('Error adding icon for %s | %s', - button_name, iconerr) + mlogger.error( + "Error adding icon for %s | %s", button_name, iconerr + ) existing_item.set_tooltip(tooltip) existing_item.set_tooltip_ext(tooltip_ext) @@ -1403,9 +1485,12 @@ def create_push_button(self, button_name, asm_location, class_name, # update self ctx before changing the existing item ctx help self_ctxhelp = self.get_contexthelp() ctx_help = existing_item.get_contexthelp() - if self_ctxhelp and ctx_help \ - and self_ctxhelp.HelpType == ctx_help.HelpType \ - and self_ctxhelp.HelpPath == ctx_help.HelpPath: + if ( + self_ctxhelp + and ctx_help + and self_ctxhelp.HelpType == ctx_help.HelpType + and self_ctxhelp.HelpPath == ctx_help.HelpPath + ): self.set_contexthelp(ctxhelpurl) # now change the existing item ctx help existing_item.set_contexthelp(ctxhelpurl) @@ -1416,22 +1501,20 @@ def create_push_button(self, button_name, asm_location, class_name, existing_item.activate() return else: - raise PyRevitUIError('Push button already exits and update ' - 'is not allowed: {}'.format(button_name)) + raise PyRevitUIError( + "Push button already exits and update " + "is not allowed: {}".format(button_name) + ) - mlogger.debug('Parent does not include this button. Creating: %s', - button_name) + mlogger.debug("Parent does not include this button. Creating: %s", button_name) try: - button_data = \ - UI.PushButtonData(button_name, - button_name, - asm_location, - class_name) + button_data = UI.PushButtonData( + button_name, button_name, asm_location, class_name + ) if avail_class_name: button_data.AvailabilityClassName = avail_class_name if not self.itemdata_mode: - ribbon_button = \ - self.get_rvtapi_object().AddPushButton(button_data) + ribbon_button = self.get_rvtapi_object().AddPushButton(button_data) new_button = _PyRevitRibbonButton(ribbon_button) else: new_button = _PyRevitRibbonButton(button_data) @@ -1440,20 +1523,29 @@ def create_push_button(self, button_name, asm_location, class_name, new_button.set_title(ui_title) if not icon_path: - mlogger.debug('Icon not set for %s', button_name) + mlogger.debug("Icon not set for %s", button_name) else: - mlogger.debug('Creating icon for push button %s from file: %s', - button_name, icon_path) + mlogger.debug( + "Creating icon for push button %s from file: %s", + button_name, + icon_path, + ) try: # if button group shows the active button icon, # then the child buttons need to have large icons new_button.set_icon( icon_path, - icon_size=ICON_LARGE - if self._use_active_item_icon else ICON_MEDIUM) + icon_size=( + ICON_LARGE if self._use_active_item_icon else ICON_MEDIUM + ), + ) except PyRevitUIError as iconerr: - mlogger.debug('Error adding icon for %s from %s ' - '| %s', button_name, icon_path, iconerr) + mlogger.debug( + "Error adding icon for %s from %s " "| %s", + button_name, + icon_path, + iconerr, + ) new_button.set_tooltip(tooltip) new_button.set_tooltip_ext(tooltip_ext) @@ -1463,15 +1555,14 @@ def create_push_button(self, button_name, asm_location, class_name, new_button.set_contexthelp(ctxhelpurl) # if this is the first button being added if not self.keys(): - mlogger.debug('Setting ctx help on parent: %s', ctxhelpurl) + mlogger.debug("Setting ctx help on parent: %s", ctxhelpurl) self.set_contexthelp(ctxhelpurl) new_button.set_dirty_flag() self._add_component(new_button) except Exception as create_err: - raise PyRevitUIError('Can not create button ' - '| {}'.format(create_err)) + raise PyRevitUIError("Can not create button " "| {}".format(create_err)) def add_separator(self): if not self.itemdata_mode: @@ -1508,13 +1599,13 @@ def __init__(self, rvt_ribbon_panel, parent_tab): # _PyRevitRibbonGroupItem for existing group items # _PyRevitRibbonPanel will find its existing ribbon items internally if isinstance(revit_ribbon_item, UI.PulldownButton): - self._add_component( - _PyRevitRibbonGroupItem(revit_ribbon_item)) + self._add_component(_PyRevitRibbonGroupItem(revit_ribbon_item)) elif isinstance(revit_ribbon_item, UI.PushButton): self._add_component(_PyRevitRibbonButton(revit_ribbon_item)) else: - raise PyRevitUIError('Can not determin ribbon item type: {}' - .format(revit_ribbon_item)) + raise PyRevitUIError( + "Can not determin ribbon item type: {}".format(revit_ribbon_item) + ) def get_adwindows_object(self): for panel in self.parent_tab.Panels: @@ -1536,18 +1627,15 @@ def reset_backgrounds(self): def set_panel_background(self, argb_color): panel_adwnd_obj = self.get_adwindows_object() - panel_adwnd_obj.CustomPanelBackground = \ - argb_to_brush(argb_color) + panel_adwnd_obj.CustomPanelBackground = argb_to_brush(argb_color) def set_title_background(self, argb_color): panel_adwnd_obj = self.get_adwindows_object() - panel_adwnd_obj.CustomPanelTitleBarBackground = \ - argb_to_brush(argb_color) + panel_adwnd_obj.CustomPanelTitleBarBackground = argb_to_brush(argb_color) def set_slideout_background(self, argb_color): panel_adwnd_obj = self.get_adwindows_object() - panel_adwnd_obj.CustomSlideOutPanelBackground = \ - argb_to_brush(argb_color) + panel_adwnd_obj.CustomSlideOutPanelBackground = argb_to_brush(argb_color) def reset_highlights(self): # no highlighting options for panels @@ -1584,8 +1672,7 @@ def add_slideout(self): self.get_rvtapi_object().AddSlideOut() self._dirty = True except Exception as slideout_err: - raise PyRevitUIError('Error adding slide out: {}' - .format(slideout_err)) + raise PyRevitUIError("Error adding slide out: {}".format(slideout_err)) def _create_data_items(self): # FIXME: if one item changes in stack and others dont change, @@ -1595,8 +1682,7 @@ def _create_data_items(self): # get a list of data item names and the # associated revit api data objects pyrvt_data_item_names = [x.name for x in self if x.itemdata_mode] - rvtapi_data_objs = [x.get_rvtapi_object() - for x in self if x.itemdata_mode] + rvtapi_data_objs = [x.get_rvtapi_object() for x in self if x.itemdata_mode] # list of newly created revit_api ribbon items created_rvtapi_ribbon_items = [] @@ -1606,32 +1692,35 @@ def _create_data_items(self): # if there are two or 3 items, create a proper stack if data_obj_count == 2 or data_obj_count == 3: - created_rvtapi_ribbon_items = \ - self.get_rvtapi_object().AddStackedItems(*rvtapi_data_objs) + created_rvtapi_ribbon_items = self.get_rvtapi_object().AddStackedItems( + *rvtapi_data_objs + ) # if there is only one item added, # add that to panel and forget about stacking elif data_obj_count == 1: - rvtapi_pushbutton = \ - self.get_rvtapi_object().AddItem(*rvtapi_data_objs) + rvtapi_pushbutton = self.get_rvtapi_object().AddItem(*rvtapi_data_objs) created_rvtapi_ribbon_items.append(rvtapi_pushbutton) # if no items have been added, log the empty stack and return elif data_obj_count == 0: - mlogger.debug('No new items has been added to stack. ' - 'Skipping stack creation.') + mlogger.debug( + "No new items has been added to stack. " "Skipping stack creation." + ) # if none of the above, more than 3 items have been added. # Cleanup data item cache and raise an error. else: for pyrvt_data_item_name in pyrvt_data_item_names: self._remove_component(pyrvt_data_item_name) - raise PyRevitUIError('Can not create stack of {}. ' - 'Stack can only have 2 or 3 items.' - .format(data_obj_count)) + raise PyRevitUIError( + "Can not create stack of {}. " + "Stack can only have 2 or 3 items.".format(data_obj_count) + ) # now that items are created and revit api objects are ready # iterate over the ribbon items and inject revit api objects # into the child pyrevit items - for rvtapi_ribbon_item, pyrvt_data_item_name \ - in zip(created_rvtapi_ribbon_items, pyrvt_data_item_names): + for rvtapi_ribbon_item, pyrvt_data_item_name in zip( + created_rvtapi_ribbon_items, pyrvt_data_item_names + ): pyrvt_ui_item = self._get_component(pyrvt_data_item_name) # pyrvt_ui_item only had button data info. # Now that ui ribbon item has created, update pyrvt_ui_item @@ -1655,14 +1744,22 @@ def set_dlglauncher(self, dlg_button): button_adwnd_obj = dlg_button.get_adwindows_object() panel_adwnd_obj.Source.Items.Remove(button_adwnd_obj) panel_adwnd_obj.Source.DialogLauncher = button_adwnd_obj - mlogger.debug('Added panel dialog button %s', dlg_button.name) - - def create_push_button(self, button_name, asm_location, class_name, - icon_path='', - tooltip='', tooltip_ext='', tooltip_media='', - ctxhelpurl=None, - avail_class_name=None, - update_if_exists=False, ui_title=None): + mlogger.debug("Added panel dialog button %s", dlg_button.name) + + def create_push_button( + self, + button_name, + asm_location, + class_name, + icon_path="", + tooltip="", + tooltip_ext="", + tooltip_media="", + ctxhelpurl=None, + avail_class_name=None, + update_if_exists=False, + ui_title=None, + ): if self.contains(button_name): if update_if_exists: existing_item = self._get_component(button_name) @@ -1676,8 +1773,11 @@ def create_push_button(self, button_name, asm_location, class_name, if avail_class_name: rvtapi_obj.AvailabilityClassName = avail_class_name except Exception as asm_update_err: - mlogger.debug('Error updating button asm info: %s ' - '| %s', button_name, asm_update_err) + mlogger.debug( + "Error updating button asm info: %s " "| %s", + button_name, + asm_update_err, + ) existing_item.set_tooltip(tooltip) existing_item.set_tooltip_ext(tooltip_ext) @@ -1690,31 +1790,32 @@ def create_push_button(self, button_name, asm_location, class_name, existing_item.set_title(ui_title) if not icon_path: - mlogger.debug('Icon not set for %s', button_name) + mlogger.debug("Icon not set for %s", button_name) else: try: existing_item.set_icon(icon_path, icon_size=ICON_LARGE) except PyRevitUIError as iconerr: - mlogger.error('Error adding icon for %s ' - '| %s', button_name, iconerr) + mlogger.error( + "Error adding icon for %s " "| %s", button_name, iconerr + ) existing_item.activate() else: - raise PyRevitUIError('Push button already exits and update ' - 'is not allowed: {}'.format(button_name)) + raise PyRevitUIError( + "Push button already exits and update " + "is not allowed: {}".format(button_name) + ) else: - mlogger.debug('Parent does not include this button. Creating: %s', - button_name) + mlogger.debug( + "Parent does not include this button. Creating: %s", button_name + ) try: - button_data = \ - UI.PushButtonData(button_name, - button_name, - asm_location, - class_name) + button_data = UI.PushButtonData( + button_name, button_name, asm_location, class_name + ) if avail_class_name: button_data.AvailabilityClassName = avail_class_name if not self.itemdata_mode: - ribbon_button = \ - self.get_rvtapi_object().AddItem(button_data) + ribbon_button = self.get_rvtapi_object().AddItem(button_data) new_button = _PyRevitRibbonButton(ribbon_button) else: new_button = _PyRevitRibbonButton(button_data) @@ -1723,16 +1824,24 @@ def create_push_button(self, button_name, asm_location, class_name, new_button.set_title(ui_title) if not icon_path: - mlogger.debug('Parent ui item is a panel and ' - 'panels don\'t have icons.') + mlogger.debug( + "Parent ui item is a panel and " "panels don't have icons." + ) else: - mlogger.debug('Creating icon for push button %s ' - 'from file: %s', button_name, icon_path) + mlogger.debug( + "Creating icon for push button %s " "from file: %s", + button_name, + icon_path, + ) try: new_button.set_icon(icon_path, icon_size=ICON_LARGE) except PyRevitUIError as iconerr: - mlogger.error('Error adding icon for %s from %s ' - '| %s', button_name, icon_path, iconerr) + mlogger.error( + "Error adding icon for %s from %s " "| %s", + button_name, + icon_path, + iconerr, + ) new_button.set_tooltip(tooltip) new_button.set_tooltip_ext(tooltip_ext) @@ -1745,11 +1854,11 @@ def create_push_button(self, button_name, asm_location, class_name, self._add_component(new_button) except Exception as create_err: - raise PyRevitUIError('Can not create button | {}' - .format(create_err)) + raise PyRevitUIError("Can not create button | {}".format(create_err)) - def _create_button_group(self, pulldowndata_type, item_name, icon_path, - update_if_exists=False): + def _create_button_group( + self, pulldowndata_type, item_name, icon_path, update_if_exists=False + ): if self.contains(item_name): if update_if_exists: exiting_item = self._get_component(item_name) @@ -1757,71 +1866,88 @@ def _create_button_group(self, pulldowndata_type, item_name, icon_path, if icon_path: exiting_item.set_icon(icon_path) else: - raise PyRevitUIError('Pull down button already exits and ' - 'update is not allowed: {}' - .format(item_name)) + raise PyRevitUIError( + "Pull down button already exits and " + "update is not allowed: {}".format(item_name) + ) else: - mlogger.debug('Panel does not include this pull down button. ' - 'Creating: %s', item_name) + mlogger.debug( + "Panel does not include this pull down button. " "Creating: %s", + item_name, + ) try: # creating pull down button data and add to child list pdbutton_data = pulldowndata_type(item_name, item_name) if not self.itemdata_mode: - mlogger.debug('Creating pull down button: %s in %s', - item_name, self) - new_push_button = \ - self.get_rvtapi_object().AddItem(pdbutton_data) + mlogger.debug( + "Creating pull down button: %s in %s", item_name, self + ) + new_push_button = self.get_rvtapi_object().AddItem(pdbutton_data) pyrvt_pdbutton = _PyRevitRibbonGroupItem(new_push_button) try: pyrvt_pdbutton.set_icon(icon_path) except PyRevitUIError as iconerr: - mlogger.debug('Error adding icon for %s from %s ' - '| %s', item_name, icon_path, iconerr) + mlogger.debug( + "Error adding icon for %s from %s " "| %s", + item_name, + icon_path, + iconerr, + ) else: - mlogger.debug('Creating pull down button under stack: ' - '%s in %s', item_name, self) + mlogger.debug( + "Creating pull down button under stack: " "%s in %s", + item_name, + self, + ) pyrvt_pdbutton = _PyRevitRibbonGroupItem(pdbutton_data) try: pyrvt_pdbutton.set_icon(icon_path) except PyRevitUIError as iconerr: - mlogger.debug('Error adding icon for %s from %s ' - '| %s', item_name, icon_path, iconerr) + mlogger.debug( + "Error adding icon for %s from %s " "| %s", + item_name, + icon_path, + iconerr, + ) pyrvt_pdbutton.set_dirty_flag() self._add_component(pyrvt_pdbutton) except Exception as button_err: - raise PyRevitUIError('Can not create pull down button: {}' - .format(button_err)) - - def create_pulldown_button(self, item_name, icon_path, - update_if_exists=False): - self._create_button_group(UI.PulldownButtonData, item_name, icon_path, - update_if_exists) - - def create_split_button(self, item_name, icon_path, - update_if_exists=False): - if self.itemdata_mode and HOST_APP.is_older_than('2017'): - raise PyRevitUIError('Revits earlier than 2017 do not support ' - 'split buttons in a stack.') + raise PyRevitUIError( + "Can not create pull down button: {}".format(button_err) + ) + + def create_pulldown_button(self, item_name, icon_path, update_if_exists=False): + self._create_button_group( + UI.PulldownButtonData, item_name, icon_path, update_if_exists + ) + + def create_split_button(self, item_name, icon_path, update_if_exists=False): + if self.itemdata_mode and HOST_APP.is_older_than("2017"): + raise PyRevitUIError( + "Revits earlier than 2017 do not support " "split buttons in a stack." + ) else: - self._create_button_group(UI.SplitButtonData, item_name, icon_path, - update_if_exists) + self._create_button_group( + UI.SplitButtonData, item_name, icon_path, update_if_exists + ) self.ribbon_item(item_name).sync_with_current_item(True) - def create_splitpush_button(self, item_name, icon_path, - update_if_exists=False): - if self.itemdata_mode and HOST_APP.is_older_than('2017'): - raise PyRevitUIError('Revits earlier than 2017 do not support ' - 'split buttons in a stack.') + def create_splitpush_button(self, item_name, icon_path, update_if_exists=False): + if self.itemdata_mode and HOST_APP.is_older_than("2017"): + raise PyRevitUIError( + "Revits earlier than 2017 do not support " "split buttons in a stack." + ) else: - self._create_button_group(UI.SplitButtonData, item_name, icon_path, - update_if_exists) + self._create_button_group( + UI.SplitButtonData, item_name, icon_path, update_if_exists + ) self.ribbon_item(item_name).sync_with_current_item(False) def create_combobox(self, item_name, update_if_exists=False): """Create a ComboBox in the ribbon panel. - + Args: item_name (str): Name of the ComboBox update_if_exists (bool, optional): Update if exists. Defaults to False. @@ -1833,13 +1959,14 @@ def create_combobox(self, item_name, update_if_exists=False): # Return existing item so caller can update members return existing_item else: - raise PyRevitUIError('ComboBox already exists and ' - 'update is not allowed: {}' - .format(item_name)) - + raise PyRevitUIError( + "ComboBox already exists and " + "update is not allowed: {}".format(item_name) + ) + # Create ComboBoxData combobox_data = UI.ComboBoxData(item_name) - + if not self.itemdata_mode: # Add to panel new_combobox = self.get_rvtapi_object().AddItem(combobox_data) @@ -1847,32 +1974,40 @@ def create_combobox(self, item_name, update_if_exists=False): else: # Create under stack pyrvt_combobox = _PyRevitRibbonComboBox(combobox_data) - + pyrvt_combobox.set_dirty_flag() self._add_component(pyrvt_combobox) pyrvt_combobox.activate() - def create_panel_push_button(self, button_name, asm_location, class_name, - tooltip='', tooltip_ext='', tooltip_media='', - ctxhelpurl=None, - avail_class_name=None, - update_if_exists=False, - ui_title=None): - self.create_push_button(button_name=button_name, - asm_location=asm_location, - class_name=class_name, - icon_path=None, - tooltip=tooltip, - tooltip_ext=tooltip_ext, - tooltip_media=tooltip_media, - ctxhelpurl=ctxhelpurl, - avail_class_name=avail_class_name, - update_if_exists=update_if_exists, - ui_title=ui_title) + def create_panel_push_button( + self, + button_name, + asm_location, + class_name, + tooltip="", + tooltip_ext="", + tooltip_media="", + ctxhelpurl=None, + avail_class_name=None, + update_if_exists=False, + ui_title=None, + ): + self.create_push_button( + button_name=button_name, + asm_location=asm_location, + class_name=class_name, + icon_path=None, + tooltip=tooltip, + tooltip_ext=tooltip_ext, + tooltip_media=tooltip_media, + ctxhelpurl=ctxhelpurl, + avail_class_name=avail_class_name, + update_if_exists=update_if_exists, + ui_title=ui_title, + ) self.set_dlglauncher(self.button(button_name)) - class _PyRevitRibbonTab(GenericPyRevitUIContainer): ribbon_panel = GenericPyRevitUIContainer._get_component @@ -1892,13 +2027,15 @@ def __init__(self, revit_ribbon_tab, is_pyrvt_tab=False): # feeding _sub_pyrvt_ribbon_panels with an instance of # _PyRevitRibbonPanel for existing panels _PyRevitRibbonPanel # will find its existing ribbon items internally - new_pyrvt_panel = _PyRevitRibbonPanel(revit_ui_panel, - self._rvtapi_object) + new_pyrvt_panel = _PyRevitRibbonPanel( + revit_ui_panel, self._rvtapi_object + ) self._add_component(new_pyrvt_panel) except: # if .GetRibbonPanels fails, this tab is an existing native tab - raise PyRevitUIError('Can not get panels for this tab: {}' - .format(self._rvtapi_object)) + raise PyRevitUIError( + "Can not get panels for this tab: {}".format(self._rvtapi_object) + ) def get_adwindows_object(self): return self.get_rvtapi_object() @@ -1917,8 +2054,9 @@ def highlight_as_updated(self): @staticmethod def check_pyrevit_tab(revit_ui_tab): - return hasattr(revit_ui_tab, 'Tag') \ - and revit_ui_tab.Tag == PYREVIT_TAB_IDENTIFIER + return ( + hasattr(revit_ui_tab, "Tag") and revit_ui_tab.Tag == PYREVIT_TAB_IDENTIFIER + ) def is_pyrevit_tab(self): return self.get_rvtapi_object().Tag == PYREVIT_TAB_IDENTIFIER @@ -1933,23 +2071,24 @@ def create_ribbon_panel(self, panel_name, update_if_exists=False): exiting_pyrvt_panel = self._get_component(panel_name) exiting_pyrvt_panel.activate() else: - raise PyRevitUIError('RibbonPanel already exits and update ' - 'is not allowed: {}'.format(panel_name)) + raise PyRevitUIError( + "RibbonPanel already exits and update " + "is not allowed: {}".format(panel_name) + ) else: try: # creating panel in tab - ribbon_panel = \ - HOST_APP.uiapp.CreateRibbonPanel(self.name, panel_name) + ribbon_panel = HOST_APP.uiapp.CreateRibbonPanel(self.name, panel_name) # creating _PyRevitRibbonPanel object and # add new panel to list of current panels - pyrvt_ribbon_panel = _PyRevitRibbonPanel(ribbon_panel, - self._rvtapi_object) + pyrvt_ribbon_panel = _PyRevitRibbonPanel( + ribbon_panel, self._rvtapi_object + ) pyrvt_ribbon_panel.set_dirty_flag() self._add_component(pyrvt_ribbon_panel) except Exception as panel_err: - raise PyRevitUIError('Can not create panel: {}' - .format(panel_err)) + raise PyRevitUIError("Can not create panel: {}".format(panel_err)) class _PyRevitUI(GenericPyRevitUIContainer): @@ -1976,14 +2115,12 @@ def __init__(self, all_native=False): # pyrevit tabs (PYREVIT_TAB_IDENTIFIER) anyway. # if revit_ui_tab.IsVisible try: - if not all_native \ - and _PyRevitRibbonTab.check_pyrevit_tab(revit_ui_tab): + if not all_native and _PyRevitRibbonTab.check_pyrevit_tab(revit_ui_tab): new_pyrvt_tab = _PyRevitRibbonTab(revit_ui_tab) else: new_pyrvt_tab = RevitNativeRibbonTab(revit_ui_tab) self._add_component(new_pyrvt_tab) - mlogger.debug('Tab added to the list of tabs: %s', - new_pyrvt_tab.name) + mlogger.debug("Tab added to the list of tabs: %s", new_pyrvt_tab.name) except PyRevitUIError: # if _PyRevitRibbonTab(revit_ui_tab) fails, # Revit restricts access to its panels RevitNativeRibbonTab @@ -1991,19 +2128,19 @@ def __init__(self, all_native=False): # to interact with existing native ui new_pyrvt_tab = RevitNativeRibbonTab(revit_ui_tab) self._add_component(new_pyrvt_tab) - mlogger.debug('Native tab added to the list of tabs: %s', - new_pyrvt_tab.name) + mlogger.debug( + "Native tab added to the list of tabs: %s", new_pyrvt_tab.name + ) def get_adwindows_ribbon_control(self): return AdWindows.ComponentManager.Ribbon @staticmethod - def toggle_ribbon_updator( - state, - flow_direction=Windows.FlowDirection.LeftToRight): + def toggle_ribbon_updator(state, flow_direction=Windows.FlowDirection.LeftToRight): # cancel out the ribbon updator from previous runtime version - current_ribbon_updator = \ - envvars.get_pyrevit_env_var(envvars.RIBBONUPDATOR_ENVVAR) + current_ribbon_updator = envvars.get_pyrevit_env_var( + envvars.RIBBONUPDATOR_ENVVAR + ) if current_ribbon_updator: current_ribbon_updator.StopUpdatingRibbon() @@ -2015,29 +2152,26 @@ def toggle_ribbon_updator( try: main_wnd = ui.get_mainwindow() ribbon_root_type = ui.get_ribbon_roottype() - panel_set = \ - main_wnd.FindFirstChild[ribbon_root_type](main_wnd) + panel_set = main_wnd.FindFirstChild[ribbon_root_type](main_wnd) except Exception as raex: - mlogger.error('Error activating ribbon updator. | %s', raex) + mlogger.error("Error activating ribbon updator. | %s", raex) return if panel_set: types.RibbonEventUtils.StartUpdatingRibbon( panelSet=panel_set, flowDir=flow_direction, - tagTag=PYREVIT_TAB_IDENTIFIER + tagTag=PYREVIT_TAB_IDENTIFIER, ) # set the new colorizer envvars.set_pyrevit_env_var( - envvars.RIBBONUPDATOR_ENVVAR, - types.RibbonEventUtils - ) + envvars.RIBBONUPDATOR_ENVVAR, types.RibbonEventUtils + ) def set_RTL_flow(self): _PyRevitUI.toggle_ribbon_updator( - state=True, - flow_direction=Windows.FlowDirection.RightToLeft - ) + state=True, flow_direction=Windows.FlowDirection.RightToLeft + ) def set_LTR_flow(self): # default is LTR, make sure any existing is stopped @@ -2059,8 +2193,10 @@ def create_ribbon_tab(self, tab_name, update_if_exists=False): existing_pyrvt_tab = self._get_component(tab_name) existing_pyrvt_tab.activate() else: - raise PyRevitUIError('RibbonTab already exits and update is ' - 'not allowed: {}'.format(tab_name)) + raise PyRevitUIError( + "RibbonTab already exits and update is " + "not allowed: {}".format(tab_name) + ) else: try: # creating tab in Revit ui @@ -2069,8 +2205,7 @@ def create_ribbon_tab(self, tab_name, update_if_exists=False): # not return the created tab object. # so find the tab object in exiting ui revit_tab_ctrl = None - for exiting_rvt_ribbon_tab in \ - AdWindows.ComponentManager.Ribbon.Tabs: + for exiting_rvt_ribbon_tab in AdWindows.ComponentManager.Ribbon.Tabs: if exiting_rvt_ribbon_tab.Title == tab_name: revit_tab_ctrl = exiting_rvt_ribbon_tab @@ -2078,17 +2213,18 @@ def create_ribbon_tab(self, tab_name, update_if_exists=False): # the recovered RibbonTab object # and add new _PyRevitRibbonTab to list of current tabs if revit_tab_ctrl: - pyrvt_ribbon_tab = _PyRevitRibbonTab(revit_tab_ctrl, - is_pyrvt_tab=True) + pyrvt_ribbon_tab = _PyRevitRibbonTab( + revit_tab_ctrl, is_pyrvt_tab=True + ) pyrvt_ribbon_tab.set_dirty_flag() self._add_component(pyrvt_ribbon_tab) else: - raise PyRevitUIError('Tab created but can not ' - 'be obtained from ui.') + raise PyRevitUIError( + "Tab created but can not " "be obtained from ui." + ) except Exception as tab_create_err: - raise PyRevitUIError('Can not create tab: {}' - .format(tab_create_err)) + raise PyRevitUIError("Can not create tab: {}".format(tab_create_err)) # Public function to return an instance of _PyRevitUI which is used diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index b1689e45c..c0314a2ef 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -1,4 +1,5 @@ """Base classes for pyRevit extension components.""" + import os import os.path as op import json @@ -20,8 +21,8 @@ mlogger = get_logger(__name__) -EXT_HASH_VALUE_KEY = 'dir_hash_value' -EXT_HASH_VERSION_KEY = 'pyrvt_version' +EXT_HASH_VALUE_KEY = "dir_hash_value" +EXT_HASH_VERSION_KEY = "pyrvt_version" # Derived classes here correspond to similar elements in Revit ui. @@ -31,11 +32,13 @@ # ------------------------------------------------------------------------------ class NoButton(GenericUICommand): """This is not a button.""" + type_id = exts.NOGUI_COMMAND_POSTFIX class NoScriptButton(GenericUICommand): """Base for buttons that doesn't run a script.""" + def __init__(self, cmp_path=None, needs_commandclass=False): # using classname otherwise exceptions in superclasses won't show GenericUICommand.__init__(self, cmp_path=cmp_path, needs_script=False) @@ -43,16 +46,17 @@ def __init__(self, cmp_path=None, needs_commandclass=False): # read metadata from metadata file if self.meta: # get the target assembly from metadata - self.assembly = \ - self.meta.get(exts.MDATA_LINK_BUTTON_ASSEMBLY, None) + self.assembly = self.meta.get(exts.MDATA_LINK_BUTTON_ASSEMBLY, None) # get the target command class from metadata - self.command_class = \ - self.meta.get(exts.MDATA_LINK_BUTTON_COMMAND_CLASS, None) + self.command_class = self.meta.get( + exts.MDATA_LINK_BUTTON_COMMAND_CLASS, None + ) # get the target command class from metadata - self.avail_command_class = \ - self.meta.get(exts.MDATA_LINK_BUTTON_AVAIL_COMMAND_CLASS, None) + self.avail_command_class = self.meta.get( + exts.MDATA_LINK_BUTTON_AVAIL_COMMAND_CLASS, None + ) # for invoke buttons there is no script source so # assign the metadata file to the script @@ -66,13 +70,14 @@ def __init__(self, cmp_path=None, needs_commandclass=False): if self.directory and needs_commandclass and not self.command_class: mlogger.error("%s does not specify target command class.", self) - mlogger.debug('%s assembly.class: %s.%s', - self, self.assembly, self.command_class) + mlogger.debug( + "%s assembly.class: %s.%s", self, self.assembly, self.command_class + ) def get_target_assembly(self, required=False): assm_file = self.assembly.lower() if not assm_file.endswith(framework.ASSEMBLY_FILE_TYPE): - assm_file += '.' + framework.ASSEMBLY_FILE_TYPE + assm_file += "." + framework.ASSEMBLY_FILE_TYPE # try finding assembly for this specific host version target_asm_by_host = self.find_bundle_module(assm_file, by_host=True) @@ -86,32 +91,31 @@ def get_target_assembly(self, required=False): if required: mlogger.error("%s can not find target assembly.", self) - return '' + return "" class LinkButton(NoScriptButton): """Link button.""" + type_id = exts.LINK_BUTTON_POSTFIX def __init__(self, cmp_path=None): # using classname otherwise exceptions in superclasses won't show - NoScriptButton.__init__( - self, - cmp_path=cmp_path, - needs_commandclass=True - ) + NoScriptButton.__init__(self, cmp_path=cmp_path, needs_commandclass=True) if self.context: mlogger.warn( - "Linkbutton bundles do not support \"context:\". " - "Use \"availability_class:\" instead and specify name of " - "availability class in target assembly | %s", self - ) + 'Linkbutton bundles do not support "context:". ' + 'Use "availability_class:" instead and specify name of ' + "availability class in target assembly | %s", + self, + ) self.context = None class InvokeButton(NoScriptButton): """Invoke button.""" + type_id = exts.INVOKE_BUTTON_POSTFIX def __init__(self, cmp_path=None): @@ -121,30 +125,30 @@ def __init__(self, cmp_path=None): class PushButton(GenericUICommand): """Push button.""" + type_id = exts.PUSH_BUTTON_POSTFIX class PanelPushButton(GenericUICommand): """Panel push button.""" + type_id = exts.PANEL_PUSH_BUTTON_POSTFIX class SmartButton(GenericUICommand): """Smart button.""" + type_id = exts.SMART_BUTTON_POSTFIX class ContentButton(GenericUICommand): """Content Button.""" + type_id = exts.CONTENT_BUTTON_POSTFIX def __init__(self, cmp_path=None): # using classname otherwise exceptions in superclasses won't show - GenericUICommand.__init__( - self, - cmp_path=cmp_path, - needs_script=False - ) + GenericUICommand.__init__(self, cmp_path=cmp_path, needs_script=False) # Initialize content paths self.content = None @@ -158,8 +162,10 @@ def __init__(self, cmp_path=None): if resolved_path: self.script_file = resolved_path else: - mlogger.error('Content file specified in metadata not found: %s', - content_from_meta) + mlogger.error( + "Content file specified in metadata not found: %s", + content_from_meta, + ) alt_content_from_meta = self.meta.get(exts.MDATA_CONTENT_ALT, None) if alt_content_from_meta: @@ -167,41 +173,43 @@ def __init__(self, cmp_path=None): if resolved_alt_path: self.config_script_file = resolved_alt_path else: - mlogger.error('Alternative content file specified in metadata not found: %s', - alt_content_from_meta) + mlogger.error( + "Alternative content file specified in metadata not found: %s", + alt_content_from_meta, + ) # Fall back to naming convention if not found in metadata # find content file if not self.script_file: - self.script_file = \ - self.find_bundle_file([ - exts.CONTENT_VERSION_POSTFIX.format( - version=HOST_APP.version - ), - ]) + self.script_file = self.find_bundle_file( + [ + exts.CONTENT_VERSION_POSTFIX.format(version=HOST_APP.version), + ] + ) if not self.script_file: - self.script_file = \ - self.find_bundle_file([ + self.script_file = self.find_bundle_file( + [ exts.CONTENT_POSTFIX, - ]) + ] + ) # requires at least one bundles if self.directory and not self.script_file: - mlogger.error('Command %s: Does not have content file.', self) - self.script_file = '' + mlogger.error("Command %s: Does not have content file.", self) + self.script_file = "" # find alternative content file if not self.config_script_file: - self.config_script_file = \ - self.find_bundle_file([ - exts.ALT_CONTENT_VERSION_POSTFIX.format( - version=HOST_APP.version - ), - ]) + self.config_script_file = self.find_bundle_file( + [ + exts.ALT_CONTENT_VERSION_POSTFIX.format(version=HOST_APP.version), + ] + ) if not self.config_script_file: - self.config_script_file = \ - self.find_bundle_file([ + self.config_script_file = self.find_bundle_file( + [ exts.ALT_CONTENT_POSTFIX, - ]) + ] + ) if not self.config_script_file: self.config_script_file = self.script_file @@ -210,37 +218,44 @@ def _resolve_content_path(self, path): if op.isabs(path): if op.exists(path): if not path.lower().endswith(exts.CONTENT_FILE_FORMAT): - mlogger.error('Content file must be a Revit family (.rfa): %s', - path) + mlogger.error( + "Content file must be a Revit family (.rfa): %s", path + ) return None return path else: - mlogger.error('Content file specified in metadata not found: %s', - path) + mlogger.error("Content file specified in metadata not found: %s", path) return None - + # Treat as relative to bundle directory if self.directory: # Normalize the path to handle .. and . properly bundle_path = op.normpath(op.join(self.directory, path)) if op.exists(bundle_path): if not bundle_path.lower().endswith(exts.CONTENT_FILE_FORMAT): - mlogger.error('Content file must be a Revit family (.rfa): %s', - bundle_path) + mlogger.error( + "Content file must be a Revit family (.rfa): %s", bundle_path + ) return None return bundle_path else: - mlogger.error('Content file specified in metadata not found: %s (resolved to: %s)', - path, bundle_path) + mlogger.error( + "Content file specified in metadata not found: %s (resolved to: %s)", + path, + bundle_path, + ) return None - - mlogger.error('Content file specified in metadata not found: %s (no bundle directory)', - path) + + mlogger.error( + "Content file specified in metadata not found: %s (no bundle directory)", + path, + ) return None class URLButton(GenericUICommand): """URL button.""" + type_id = exts.URL_BUTTON_POSTFIX def __init__(self, cmp_path=None): @@ -250,8 +265,7 @@ def __init__(self, cmp_path=None): # read metadata from metadata file if self.meta: # get the target url from metadata - self.target_url = \ - self.meta.get(exts.MDATA_URL_BUTTON_HYPERLINK, None) + self.target_url = self.meta.get(exts.MDATA_URL_BUTTON_HYPERLINK, None) # for url buttons there is no script source so # assign the metadata file to the script self.script_file = self.config_script_file = self.meta_file @@ -261,7 +275,7 @@ def __init__(self, cmp_path=None): if self.directory and not self.target_url: mlogger.error("%s does not specify target url.", self) - mlogger.debug('%s target url: %s', self, self.target_url) + mlogger.debug("%s target url: %s", self, self.target_url) def get_target_url(self): return self.target_url or "" @@ -273,6 +287,7 @@ class GenericUICommandGroup(GenericUIContainer): Command groups only include commands. These classes can include GenericUICommand as sub components. """ + allowed_sub_cmps = [GenericUICommand, NoScriptButton] @property @@ -280,12 +295,11 @@ def control_id(self): # stacks don't have control id if self.parent_ctrl_id: deepend_parent_id = self.parent_ctrl_id.replace( - '_%CustomCtrl', - '_%CustomCtrl_%CustomCtrl' + "_%CustomCtrl", "_%CustomCtrl_%CustomCtrl" ) - return deepend_parent_id + '%{}'.format(self.name) + return deepend_parent_id + "%{}".format(self.name) else: - return '%{}%'.format(self.name) + return "%{}%".format(self.name) def has_commands(self): for component in self: @@ -295,47 +309,53 @@ def has_commands(self): class PullDownButtonGroup(GenericUICommandGroup): """Pulldown button group.""" + type_id = exts.PULLDOWN_BUTTON_POSTFIX class ComboBoxGroup(GenericUICommandGroup): """ComboBox group.""" + type_id = exts.COMBOBOX_POSTFIX - + def __init__(self, cmp_path=None): GenericUICommandGroup.__init__(self, cmp_path=cmp_path) self.members = [] - + # Read members from metadata if self.meta: - raw_members = self.meta.get('members', []) + raw_members = self.meta.get("members", []) if isinstance(raw_members, list): # Process list of members - preserve full dict for rich metadata (icons, tooltips, etc.) processed_members = [] for m in raw_members: - if isinstance(m, dict) or (hasattr(m, 'get') and hasattr(m, 'keys')): + if isinstance(m, dict) or ( + hasattr(m, "get") and hasattr(m, "keys") + ): # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings', 'icon': '...', ...} # Preserve the full dictionary to keep all properties (icon, tooltip, group, etc.) processed_members.append(m) elif isinstance(m, (list, tuple)) and len(m) >= 2: # Tuple/list format: ('id', 'text') - convert to dict for consistency - processed_members.append({'id': m[0], 'text': m[1]}) + processed_members.append({"id": m[0], "text": m[1]}) elif isinstance(m, str): # String format: 'Option 1' - convert to dict for consistency - processed_members.append({'id': m, 'text': m}) + processed_members.append({"id": m, "text": m}) self.members = processed_members elif isinstance(raw_members, dict): # Dict format: {'A': 'Option A'} - convert to list of dicts - self.members = [{'id': k, 'text': v} for k, v in raw_members.items()] + self.members = [{"id": k, "text": v} for k, v in raw_members.items()] class SplitPushButtonGroup(GenericUICommandGroup): """Split push button group.""" + type_id = exts.SPLITPUSH_BUTTON_POSTFIX class SplitButtonGroup(GenericUICommandGroup): """Split button group.""" + type_id = exts.SPLIT_BUTTON_POSTFIX @@ -344,15 +364,15 @@ class GenericStack(GenericUIContainer): Stacks include GenericUICommand, or GenericUICommandGroup. """ + type_id = exts.STACK_BUTTON_POSTFIX - allowed_sub_cmps = \ - [GenericUICommandGroup, GenericUICommand, NoScriptButton] + allowed_sub_cmps = [GenericUICommandGroup, GenericUICommand, NoScriptButton] @property def control_id(self): # stacks don't have control id - return self.parent_ctrl_id if self.parent_ctrl_id else '' + return self.parent_ctrl_id if self.parent_ctrl_id else "" def has_commands(self): for component in self: @@ -366,6 +386,7 @@ def has_commands(self): class StackButtonGroup(GenericStack): """Stack buttons group.""" + type_id = exts.STACK_BUTTON_POSTFIX @@ -374,32 +395,36 @@ class Panel(GenericUIContainer): Panels include GenericStack, GenericUICommand, or GenericUICommandGroup """ + type_id = exts.PANEL_POSTFIX - allowed_sub_cmps = \ - [GenericStack, GenericUICommandGroup, GenericUICommand, NoScriptButton] + allowed_sub_cmps = [ + GenericStack, + GenericUICommandGroup, + GenericUICommand, + NoScriptButton, + ] def __init__(self, cmp_path=None): # using classname otherwise exceptions in superclasses won't show GenericUIContainer.__init__(self, cmp_path=cmp_path) - self.panel_background = \ - self.title_background = \ - self.slideout_background = None + self.panel_background = self.title_background = self.slideout_background = None # read metadata from metadata file if self.meta: # check for background color configs - self.panel_background = \ - self.meta.get(exts.MDATA_BACKGROUND_KEY, None) + self.panel_background = self.meta.get(exts.MDATA_BACKGROUND_KEY, None) if self.panel_background: if isinstance(self.panel_background, dict): self.title_background = self.panel_background.get( - exts.MDATA_BACKGROUND_TITLE_KEY, None) + exts.MDATA_BACKGROUND_TITLE_KEY, None + ) self.slideout_background = self.panel_background.get( - exts.MDATA_BACKGROUND_SLIDEOUT_KEY, None) + exts.MDATA_BACKGROUND_SLIDEOUT_KEY, None + ) self.panel_background = self.panel_background.get( - exts.MDATA_BACKGROUND_PANEL_KEY, None) + exts.MDATA_BACKGROUND_PANEL_KEY, None + ) elif not isinstance(self.panel_background, str): - mlogger.error( - "%s bad background definition in metadata.", self) + mlogger.error("%s bad background definition in metadata.", self) def has_commands(self): for component in self: @@ -423,13 +448,15 @@ def contains(self, item_name): else: # if child is a stack item, check its children too for component in self: - if isinstance(component, GenericStack) \ - and component.contains(item_name): + if isinstance(component, GenericStack) and component.contains( + item_name + ): return True class Tab(GenericUIContainer): """Tab container for Panels.""" + type_id = exts.TAB_POSTFIX allowed_sub_cmps = [Panel] @@ -442,6 +469,7 @@ def has_commands(self): class Extension(GenericUIContainer): """UI Tools extension.""" + type_id = exts.ExtensionTypes.UI_EXTENSION.POSTFIX allowed_sub_cmps = [Tab] @@ -458,36 +486,39 @@ def _calculate_extension_dir_hash(self): # cache only saves the png address and not the contents so they'll # get loaded everytime # see http://stackoverflow.com/a/5141710/2350244 - pat = '(\\' + exts.TAB_POSTFIX + ')|(\\' + exts.PANEL_POSTFIX + ')' - pat += '|(\\' + exts.PULLDOWN_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.SPLIT_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.SPLITPUSH_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.STACK_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.PUSH_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.SMART_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.LINK_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.PANEL_PUSH_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.CONTENT_BUTTON_POSTFIX + ')' - pat += '|(\\' + exts.COMBOBOX_POSTFIX + ')' + pat = "(\\" + exts.TAB_POSTFIX + ")|(\\" + exts.PANEL_POSTFIX + ")" + pat += "|(\\" + exts.PULLDOWN_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.SPLIT_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.SPLITPUSH_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.STACK_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.PUSH_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.SMART_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.LINK_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.PANEL_PUSH_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.PANEL_PUSH_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.CONTENT_BUTTON_POSTFIX + ")" + pat += "|(\\" + exts.COMBOBOX_POSTFIX + ")" # tnteresting directories - pat += '|(\\' + exts.COMP_LIBRARY_DIR_NAME + ')' - pat += '|(\\' + exts.COMP_HOOKS_DIR_NAME + ')' + pat += "|(\\" + exts.COMP_LIBRARY_DIR_NAME + ")" + pat += "|(\\" + exts.COMP_HOOKS_DIR_NAME + ")" # search for scripts, setting files (future support), and layout files - patfile = '(\\' + exts.PYTHON_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.CSHARP_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.VB_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.RUBY_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.DYNAMO_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.GRASSHOPPER_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.GRASSHOPPERX_SCRIPT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.CONTENT_FILE_FORMAT + ')' - patfile += '|(\\' + exts.YAML_FILE_FORMAT + ')' - patfile += '|(\\' + exts.JSON_FILE_FORMAT + ')' + patfile = "(\\" + exts.PYTHON_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.CSHARP_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.VB_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.RUBY_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.DYNAMO_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.GRASSHOPPER_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.GRASSHOPPERX_SCRIPT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.CONTENT_FILE_FORMAT + ")" + patfile += "|(\\" + exts.YAML_FILE_FORMAT + ")" + patfile += "|(\\" + exts.JSON_FILE_FORMAT + ")" from pyrevit.revit import ui - return coreutils.calculate_dir_hash(self.directory, pat, patfile) + str(ui.get_current_theme()) - def _update_from_directory(self): #pylint: disable=W0221 + return coreutils.calculate_dir_hash(self.directory, pat, patfile) + str( + ui.get_current_theme() + ) + + def _update_from_directory(self): # pylint: disable=W0221 # using classname otherwise exceptions in superclasses won't show GenericUIContainer._update_from_directory(self) self.pyrvt_version = versionmgr.get_pyrevit_version().get_formatted() @@ -510,12 +541,14 @@ def control_id(self): @property def startup_script(self): - return self.find_bundle_file([ - exts.PYTHON_EXT_STARTUP_FILE, - exts.CSHARP_EXT_STARTUP_FILE, - exts.VB_EXT_STARTUP_FILE, - exts.RUBY_EXT_STARTUP_FILE, - ]) + return self.find_bundle_file( + [ + exts.PYTHON_EXT_STARTUP_FILE, + exts.CSHARP_EXT_STARTUP_FILE, + exts.VB_EXT_STARTUP_FILE, + exts.RUBY_EXT_STARTUP_FILE, + ] + ) def get_hash(self): return coreutils.get_str_hash(safe_strtype(self.get_cache_data())) @@ -529,13 +562,15 @@ def get_manifest_file(self): def get_manifest(self): manifest_file = self.get_manifest_file() if manifest_file: - with codecs.open(manifest_file, 'r', 'utf-8') as mfile: + with codecs.open(manifest_file, "r", "utf-8") as mfile: try: manifest_cfg = json.load(mfile) return manifest_cfg except Exception as manfload_err: - print('Can not parse ext manifest file: {} ' - '| {}'.format(manifest_file, manfload_err)) + print( + "Can not parse ext manifest file: {} " + "| {}".format(manifest_file, manfload_err) + ) return def configure(self): @@ -550,8 +585,9 @@ def get_extension_modules(self): for item in os.listdir(self.binary_path): item_path = op.join(self.binary_path, item) item_name = item.lower() - if op.isfile(item_path) \ - and item_name.endswith(framework.ASSEMBLY_FILE_TYPE): + if op.isfile(item_path) and item_name.endswith( + framework.ASSEMBLY_FILE_TYPE + ): modules.append(item_path) return modules @@ -575,6 +611,7 @@ def get_checks(self): class LibraryExtension(GenericComponent): """Library extension.""" + type_id = exts.ExtensionTypes.LIB_EXTENSION.POSTFIX def __init__(self, cmp_path=None): @@ -586,8 +623,9 @@ def __init__(self, cmp_path=None): self.name = op.splitext(op.basename(self.directory))[0] def __repr__(self): - return ''\ - .format(self.type_id, self.name, self.directory) + return "".format( + self.type_id, self.name, self.directory + ) @classmethod def matches(cls, component_path): diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 3c9c96040..3ef2591f4 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -13,7 +13,7 @@ from pyrevit.coreutils import ribbon from pyrevit.coreutils.ribbon import ICON_MEDIUM -#pylint: disable=W0703,C0302,C0103,C0413 +# pylint: disable=W0703,C0302,C0103,C0413 import pyrevit.extensions as exts from pyrevit.extensions import components from pyrevit.userconfig import user_config @@ -26,7 +26,7 @@ # mlogger.set_verbose_mode() -CONFIG_SCRIPT_TITLE_POSTFIX = u'\u25CF' +CONFIG_SCRIPT_TITLE_POSTFIX = "\u25CF" class UIMakerParams: @@ -39,6 +39,7 @@ class UIMakerParams: asm_info (AssemblyInfo): Assembly info create_beta (bool, optional): Create beta button. Defaults to False """ + def __init__(self, par_ui, par_cmp, cmp_item, asm_info, create_beta=False): self.parent_ui = par_ui self.parent_cmp = par_cmp @@ -48,49 +49,51 @@ def __init__(self, par_ui, par_cmp, cmp_item, asm_info, create_beta=False): def _make_button_tooltip(button): - tooltip = button.tooltip + '\n\n' if button.tooltip else '' - tooltip += 'Bundle Name:\n{} ({})'\ - .format(button.name, button.type_id.replace('.', '')) + tooltip = button.tooltip + "\n\n" if button.tooltip else "" + tooltip += "Bundle Name:\n{} ({})".format( + button.name, button.type_id.replace(".", "") + ) if button.author: - tooltip += '\n\nAuthor(s):\n{}'.format(button.author) + tooltip += "\n\nAuthor(s):\n{}".format(button.author) return tooltip def _make_button_tooltip_ext(button, asm_name): - tooltip_ext = '' + tooltip_ext = "" if button.min_revit_ver and not button.max_revit_ver: - tooltip_ext += 'Compatible with {} {} and above\n\n'\ - .format(HOST_APP.proc_name, - button.min_revit_ver) + tooltip_ext += "Compatible with {} {} and above\n\n".format( + HOST_APP.proc_name, button.min_revit_ver + ) if button.max_revit_ver and not button.min_revit_ver: - tooltip_ext += 'Compatible with {} {} and earlier\n\n'\ - .format(HOST_APP.proc_name, - button.max_revit_ver) + tooltip_ext += "Compatible with {} {} and earlier\n\n".format( + HOST_APP.proc_name, button.max_revit_ver + ) if button.min_revit_ver and button.max_revit_ver: if int(button.min_revit_ver) != int(button.max_revit_ver): - tooltip_ext += 'Compatible with {} {} to {}\n\n'\ - .format(HOST_APP.proc_name, - button.min_revit_ver, button.max_revit_ver) + tooltip_ext += "Compatible with {} {} to {}\n\n".format( + HOST_APP.proc_name, button.min_revit_ver, button.max_revit_ver + ) else: - tooltip_ext += 'Compatible with {} {} only\n\n'\ - .format(HOST_APP.proc_name, - button.min_revit_ver) + tooltip_ext += "Compatible with {} {} only\n\n".format( + HOST_APP.proc_name, button.min_revit_ver + ) if isinstance(button, (components.LinkButton, components.InvokeButton)): - tooltip_ext += 'Class Name:\n{}\n\nAssembly Name:\n{}\n\n'.format( - button.command_class or 'Runs first matching DB.IExternalCommand', - button.assembly) + tooltip_ext += "Class Name:\n{}\n\nAssembly Name:\n{}\n\n".format( + button.command_class or "Runs first matching DB.IExternalCommand", + button.assembly, + ) else: - tooltip_ext += 'Class Name:\n{}\n\nAssembly Name:\n{}\n\n'\ - .format(button.unique_name, asm_name) + tooltip_ext += "Class Name:\n{}\n\nAssembly Name:\n{}\n\n".format( + button.unique_name, asm_name + ) if button.control_id: - tooltip_ext += 'Control Id:\n{}'\ - .format(button.control_id) + tooltip_ext += "Control Id:\n{}".format(button.control_id) return tooltip_ext @@ -102,14 +105,14 @@ def _make_tooltip_ext_if_requested(button, asm_name): def _make_ui_title(button): if button.has_config_script(): - return button.ui_title + ' {}'.format(CONFIG_SCRIPT_TITLE_POSTFIX) + return button.ui_title + " {}".format(CONFIG_SCRIPT_TITLE_POSTFIX) else: return button.ui_title def _make_full_class_name(asm_name, class_name): if asm_name and class_name: - return '{}.{}'.format(asm_name, class_name) + return "{}.{}".format(asm_name, class_name) return None @@ -126,7 +129,7 @@ def _get_effective_classname(button): This means that typemaker has created a executor type for this command. If class_name is not set, this function returns button.unique_name. - This allows for the UI button to be created and linked to the previously + This allows for the UI button to be created and linked to the previously created assembly. If the type does not exist in the assembly, the UI button will not work, however this allows updating the command with the correct executor type, @@ -151,12 +154,12 @@ def _produce_ui_separator(ui_maker_params): ext_asm_info = ui_maker_params.asm_info if not ext_asm_info.reloading: - mlogger.debug('Adding separator to: %s', parent_ui_item) + mlogger.debug("Adding separator to: %s", parent_ui_item) try: - if hasattr(parent_ui_item, 'add_separator'): # re issue #361 + if hasattr(parent_ui_item, "add_separator"): # re issue #361 parent_ui_item.add_separator() except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -173,21 +176,28 @@ def _produce_ui_slideout(ui_maker_params): if not ext_asm_info.reloading: # Log panel items before adding slideout (for debugging order issues) try: - if hasattr(parent_ui_item, 'get_rvtapi_object'): + if hasattr(parent_ui_item, "get_rvtapi_object"): existing_items = parent_ui_item.get_rvtapi_object().GetItems() - mlogger.warning('SLIDEOUT: Panel has %d items before adding slideout', len(existing_items)) + mlogger.warning( + "SLIDEOUT: Panel has %d items before adding slideout", + len(existing_items), + ) for idx, item in enumerate(existing_items): - mlogger.warning('SLIDEOUT: Existing item %d: %s (type: %s)', - idx, getattr(item, 'Name', 'unknown'), type(item).__name__) + mlogger.warning( + "SLIDEOUT: Existing item %d: %s (type: %s)", + idx, + getattr(item, "Name", "unknown"), + type(item).__name__, + ) except Exception as log_err: - mlogger.debug('SLIDEOUT: Could not log existing items: %s', log_err) - - mlogger.warning('SLIDEOUT: Adding slide out to: %s', parent_ui_item) + mlogger.debug("SLIDEOUT: Could not log existing items: %s", log_err) + + mlogger.warning("SLIDEOUT: Adding slide out to: %s", parent_ui_item) try: parent_ui_item.add_slideout() - mlogger.warning('SLIDEOUT: Slideout added successfully') + mlogger.warning("SLIDEOUT: Slideout added successfully") except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -209,7 +219,7 @@ def _produce_ui_smartbutton(ui_maker_params): if smartbutton.is_beta and not ui_maker_params.create_beta_cmds: return None - mlogger.debug('Producing smart button: %s', smartbutton) + mlogger.debug("Producing smart button: %s", smartbutton) try: parent_ui_item.create_push_button( button_name=smartbutton.name, @@ -217,42 +227,50 @@ def _produce_ui_smartbutton(ui_maker_params): class_name=_get_effective_classname(smartbutton), icon_path=smartbutton.icon_file or parent.icon_file, tooltip=_make_button_tooltip(smartbutton), - tooltip_ext=_make_tooltip_ext_if_requested(smartbutton, - ext_asm_info.name), + tooltip_ext=_make_tooltip_ext_if_requested(smartbutton, ext_asm_info.name), tooltip_media=smartbutton.media_file, ctxhelpurl=smartbutton.help_url, avail_class_name=smartbutton.avail_class_name, update_if_exists=True, - ui_title=_make_ui_title(smartbutton)) + ui_title=_make_ui_title(smartbutton), + ) except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None smartbutton_ui = parent_ui_item.button(smartbutton.name) - mlogger.debug('Importing smart button as module: %s', smartbutton) + mlogger.debug("Importing smart button as module: %s", smartbutton) try: # replacing EXEC_PARAMS.command_name value with button name so the # init script can log under its own name - prev_commandname = \ - __builtins__['__commandname__'] \ - if '__commandname__' in __builtins__ else None - prev_commandpath = \ - __builtins__['__commandpath__'] \ - if '__commandpath__' in __builtins__ else None - prev_shiftclick = \ - __builtins__['__shiftclick__'] \ - if '__shiftclick__' in __builtins__ else False - prev_debugmode = \ - __builtins__['__forceddebugmode__'] \ - if '__forceddebugmode__' in __builtins__ else False - - __builtins__['__commandname__'] = smartbutton.name - __builtins__['__commandpath__'] = smartbutton.script_file - __builtins__['__shiftclick__'] = False - __builtins__['__forceddebugmode__'] = False + prev_commandname = ( + __builtins__["__commandname__"] + if "__commandname__" in __builtins__ + else None + ) + prev_commandpath = ( + __builtins__["__commandpath__"] + if "__commandpath__" in __builtins__ + else None + ) + prev_shiftclick = ( + __builtins__["__shiftclick__"] + if "__shiftclick__" in __builtins__ + else False + ) + prev_debugmode = ( + __builtins__["__forceddebugmode__"] + if "__forceddebugmode__" in __builtins__ + else False + ) + + __builtins__["__commandname__"] = smartbutton.name + __builtins__["__commandpath__"] = smartbutton.script_file + __builtins__["__shiftclick__"] = False + __builtins__["__forceddebugmode__"] = False except Exception as err: - mlogger.error('Smart button setup error: %s | %s', smartbutton, err) + mlogger.error("Smart button setup error: %s | %s", smartbutton, err) return smartbutton_ui try: @@ -263,15 +281,16 @@ def _produce_ui_smartbutton(ui_maker_params): sys.path.append(search_path) # importing smart button script as a module - importedscript = imp.load_source(smartbutton.unique_name, - smartbutton.script_file) + importedscript = imp.load_source( + smartbutton.unique_name, smartbutton.script_file + ) # resetting EXEC_PARAMS.command_name to original - __builtins__['__commandname__'] = prev_commandname - __builtins__['__commandpath__'] = prev_commandpath - __builtins__['__shiftclick__'] = prev_shiftclick - __builtins__['__forceddebugmode__'] = prev_debugmode - mlogger.debug('Import successful: %s', importedscript) - mlogger.debug('Running self initializer: %s', smartbutton) + __builtins__["__commandname__"] = prev_commandname + __builtins__["__commandpath__"] = prev_commandpath + __builtins__["__shiftclick__"] = prev_shiftclick + __builtins__["__forceddebugmode__"] = prev_debugmode + mlogger.debug("Import successful: %s", importedscript) + mlogger.debug("Running self initializer: %s", smartbutton) # reset sys.paths back to normal sys.path = current_paths @@ -279,23 +298,23 @@ def _produce_ui_smartbutton(ui_maker_params): res = False try: # running the smart button initializer function - res = importedscript.__selfinit__(smartbutton, - smartbutton_ui, HOST_APP.uiapp) + res = importedscript.__selfinit__( + smartbutton, smartbutton_ui, HOST_APP.uiapp + ) except Exception as button_err: - mlogger.error('Error initializing smart button: %s | %s', - smartbutton, button_err) + mlogger.error( + "Error initializing smart button: %s | %s", smartbutton, button_err + ) # if the __selfinit__ function returns False # remove the button if res is False: - mlogger.debug('SelfInit returned False on Smartbutton: %s', - smartbutton_ui) + mlogger.debug("SelfInit returned False on Smartbutton: %s", smartbutton_ui) smartbutton_ui.deactivate() - mlogger.debug('SelfInit successful on Smartbutton: %s', smartbutton_ui) + mlogger.debug("SelfInit successful on Smartbutton: %s", smartbutton_ui) except Exception as err: - mlogger.error('Smart button script import error: %s | %s', - smartbutton, err) + mlogger.error("Smart button script import error: %s | %s", smartbutton, err) return smartbutton_ui _set_highlights(smartbutton, smartbutton_ui) @@ -320,7 +339,7 @@ def _produce_ui_linkbutton(ui_maker_params): if linkbutton.is_beta and not ui_maker_params.create_beta_cmds: return None - mlogger.debug('Producing button: %s', linkbutton) + mlogger.debug("Producing button: %s", linkbutton) try: linked_asm = None # attemp to find the assembly file @@ -341,29 +360,25 @@ def _produce_ui_linkbutton(ui_maker_params): parent_ui_item.create_push_button( button_name=linkbutton.name, asm_location=linked_asm.Location, - class_name=_make_full_class_name( - linked_asm_name, - linkbutton.command_class - ), + class_name=_make_full_class_name(linked_asm_name, linkbutton.command_class), icon_path=linkbutton.icon_file or parent.icon_file, tooltip=_make_button_tooltip(linkbutton), - tooltip_ext=_make_tooltip_ext_if_requested(linkbutton, - ext_asm_info.name), + tooltip_ext=_make_tooltip_ext_if_requested(linkbutton, ext_asm_info.name), tooltip_media=linkbutton.media_file, ctxhelpurl=linkbutton.help_url, avail_class_name=_make_full_class_name( - linked_asm_name, - linkbutton.avail_command_class - ), + linked_asm_name, linkbutton.avail_command_class + ), update_if_exists=True, - ui_title=_make_ui_title(linkbutton)) + ui_title=_make_ui_title(linkbutton), + ) linkbutton_ui = parent_ui_item.button(linkbutton.name) _set_highlights(linkbutton, linkbutton_ui) return linkbutton_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -384,7 +399,7 @@ def _produce_ui_pushbutton(ui_maker_params): if pushbutton.is_beta and not ui_maker_params.create_beta_cmds: return None - mlogger.debug('Producing button: %s', pushbutton) + mlogger.debug("Producing button: %s", pushbutton) try: parent_ui_item.create_push_button( button_name=pushbutton.name, @@ -392,20 +407,20 @@ def _produce_ui_pushbutton(ui_maker_params): class_name=_get_effective_classname(pushbutton), icon_path=pushbutton.icon_file or parent.icon_file, tooltip=_make_button_tooltip(pushbutton), - tooltip_ext=_make_tooltip_ext_if_requested(pushbutton, - ext_asm_info.name), + tooltip_ext=_make_tooltip_ext_if_requested(pushbutton, ext_asm_info.name), tooltip_media=pushbutton.media_file, ctxhelpurl=pushbutton.help_url, avail_class_name=pushbutton.avail_class_name, update_if_exists=True, - ui_title=_make_ui_title(pushbutton)) + ui_title=_make_ui_title(pushbutton), + ) pushbutton_ui = parent_ui_item.button(pushbutton.name) _set_highlights(pushbutton, pushbutton_ui) return pushbutton_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -418,20 +433,21 @@ def _produce_ui_pulldown(ui_maker_params): parent_ribbon_panel = ui_maker_params.parent_ui pulldown = ui_maker_params.component - mlogger.debug('Producing pulldown button: %s', pulldown) + mlogger.debug("Producing pulldown button: %s", pulldown) try: - parent_ribbon_panel.create_pulldown_button(pulldown.ui_title, - pulldown.icon_file, - update_if_exists=True) + parent_ribbon_panel.create_pulldown_button( + pulldown.ui_title, pulldown.icon_file, update_if_exists=True + ) pulldown_ui = parent_ribbon_panel.ribbon_item(pulldown.ui_title) _set_highlights(pulldown, pulldown_ui) return pulldown_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None + def _produce_ui_combobox(ui_maker_params): """Create a ComboBox with full property support. @@ -441,38 +457,39 @@ def _produce_ui_combobox(ui_maker_params): try: parent_ribbon_panel = ui_maker_params.parent_ui combobox = ui_maker_params.component - combobox_name = getattr(combobox, 'name', 'unknown') + combobox_name = getattr(combobox, "name", "unknown") # Validate inputs first if not combobox: - mlogger.error('Component is None') + mlogger.error("Component is None") return None if not parent_ribbon_panel: - mlogger.error('Parent UI is None for: %s', combobox_name) + mlogger.error("Parent UI is None for: %s", combobox_name) return None # Get panel API object try: panel_rvtapi = parent_ribbon_panel.get_rvtapi_object() if not panel_rvtapi: - mlogger.error('Panel Revit API object is None for: %s', combobox_name) + mlogger.error("Panel Revit API object is None for: %s", combobox_name) return None except Exception as panel_err: - mlogger.error('Could not get panel Revit API object: %s', panel_err) + mlogger.error("Could not get panel Revit API object: %s", panel_err) return None # Create combobox try: parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) except Exception as create_err: - mlogger.error('Error calling create_combobox: %s', create_err) + mlogger.error("Error calling create_combobox: %s", create_err) import traceback - mlogger.error('Traceback: %s', traceback.format_exc()) + + mlogger.error("Traceback: %s", traceback.format_exc()) return None combobox_ui = parent_ribbon_panel.ribbon_item(combobox_name) if not combobox_ui: - mlogger.error('Failed to get ComboBox UI item: %s', combobox_name) + mlogger.error("Failed to get ComboBox UI item: %s", combobox_name) return None # Get the Revit API ComboBox object @@ -480,18 +497,17 @@ def _produce_ui_combobox(ui_maker_params): combobox_obj = combobox_ui.get_rvtapi_object() except Exception as rvtapi_err: mlogger.error( - 'get_rvtapi_object() failed for %s: %s', - combobox_name, rvtapi_err + "get_rvtapi_object() failed for %s: %s", combobox_name, rvtapi_err ) return None if not combobox_obj: - mlogger.error('get_rvtapi_object() returned None for: %s', combobox_name) + mlogger.error("get_rvtapi_object() returned None for: %s", combobox_name) return None # IMPORTANT: only bail here if it is still ComboBoxData if isinstance(combobox_obj, UI.ComboBoxData): - return combobox_ui # <- ONLY in this branch + return combobox_ui # <- ONLY in this branch # From here on we have a real Autodesk.Revit.UI.ComboBox @@ -500,57 +516,59 @@ def _produce_ui_combobox(ui_maker_params): # There is no separate visible "title" label for ComboBoxes like buttons have # The title from bundle.yaml is used for tooltip and identification # We'll set ItemText to the title initially, but it will be overwritten when current item is set - combobox_title = getattr(combobox, 'ui_title', None) or combobox_name + combobox_title = getattr(combobox, "ui_title", None) or combobox_name if combobox_title: try: # Set initial ItemText to title (will be overwritten when current item is set) combobox_obj.ItemText = combobox_title except Exception as title_err: - mlogger.debug('Could not set ItemText: %s', title_err) + mlogger.debug("Could not set ItemText: %s", title_err) # Set icon if available parent = ui_maker_params.parent_cmp - icon_file = getattr(combobox, 'icon_file', None) or getattr(parent, 'icon_file', None) + icon_file = getattr(combobox, "icon_file", None) or getattr( + parent, "icon_file", None + ) if icon_file: try: combobox_ui.set_icon(icon_file, icon_size=ICON_MEDIUM) except Exception as icon_err: - mlogger.debug('Error setting icon: %s', icon_err) + mlogger.debug("Error setting icon: %s", icon_err) # Set tooltip if available - tooltip = getattr(combobox, 'tooltip', None) + tooltip = getattr(combobox, "tooltip", None) if tooltip: try: combobox_ui.set_tooltip(tooltip) except Exception as tooltip_err: - mlogger.debug('Error setting tooltip: %s', tooltip_err) + mlogger.debug("Error setting tooltip: %s", tooltip_err) # Set extended tooltip if available - tooltip_ext = getattr(combobox, 'tooltip_ext', None) + tooltip_ext = getattr(combobox, "tooltip_ext", None) if tooltip_ext: try: combobox_ui.set_tooltip_ext(tooltip_ext) except Exception as tooltip_ext_err: - mlogger.debug('Error setting extended tooltip: %s', tooltip_ext_err) + mlogger.debug("Error setting extended tooltip: %s", tooltip_ext_err) # Set tooltip media (image/video) if available - tooltip_media = getattr(combobox, 'media_file', None) + tooltip_media = getattr(combobox, "media_file", None) if tooltip_media: try: combobox_ui.set_tooltip_media(tooltip_media) except Exception as tooltip_media_err: - mlogger.debug('Error setting tooltip media: %s', tooltip_media_err) + mlogger.debug("Error setting tooltip media: %s", tooltip_media_err) # Set contextual help if available - help_url = getattr(combobox, 'help_url', None) + help_url = getattr(combobox, "help_url", None) if help_url: try: combobox_ui.set_contexthelp(help_url) except Exception as help_err: - mlogger.debug('Error setting contextual help: %s', help_err) + mlogger.debug("Error setting contextual help: %s", help_err) # Add members from metadata - if hasattr(combobox, 'members') and combobox.members: + if hasattr(combobox, "members") and combobox.members: for member in combobox.members: member_id = None member_text = None @@ -559,42 +577,49 @@ def _produce_ui_combobox(ui_maker_params): member_tooltip = None member_tooltip_ext = None member_tooltip_image = None - + if isinstance(member, (list, tuple)) and len(member) >= 2: member_id, member_text = member[0], member[1] - elif isinstance(member, dict) or (hasattr(member, 'get') and hasattr(member, 'keys')): - member_id = member.get('id', member.get('name', '')) - member_text = member.get('text', member.get('title', member_id)) - member_icon = member.get('icon', None) - member_group = member.get('group', member.get('groupName', None)) - member_tooltip = member.get('tooltip', None) - member_tooltip_ext = member.get('tooltip_ext', member.get('longDescription', None)) - member_tooltip_image = member.get('tooltip_image', member.get('tooltipImage', None)) + elif isinstance(member, dict) or ( + hasattr(member, "get") and hasattr(member, "keys") + ): + member_id = member.get("id", member.get("name", "")) + member_text = member.get("text", member.get("title", member_id)) + member_icon = member.get("icon", None) + member_group = member.get("group", member.get("groupName", None)) + member_tooltip = member.get("tooltip", None) + member_tooltip_ext = member.get( + "tooltip_ext", member.get("longDescription", None) + ) + member_tooltip_image = member.get( + "tooltip_image", member.get("tooltipImage", None) + ) elif isinstance(member, str): member_id = member_text = member else: mlogger.warning( - 'Skipping invalid member format: %s (type: %s)', - member, type(member) + "Skipping invalid member format: %s (type: %s)", + member, + type(member), ) continue if not member_id or not member_text: - mlogger.warning('Skipping member with missing id or text') + mlogger.warning("Skipping member with missing id or text") continue # Create member data (minimal - just id and text) member_data = UI.ComboBoxMemberData(member_id, member_text) - + # Add member to ComboBox first (returns ComboBoxMember object) try: member = combobox_ui.add_item(member_data) if not member: - mlogger.warning('AddItem returned None for: %s', member_text) + mlogger.warning("AddItem returned None for: %s", member_text) continue - + # Now set properties on the actual ComboBoxMember object (not the data) - + # Set member icon if available if member_icon: try: @@ -603,54 +628,69 @@ def _produce_ui_combobox(ui_maker_params): icon_path = op.join(combobox.directory, member_icon) else: icon_path = member_icon - + if op.exists(icon_path): button_icon = ribbon.ButtonIcons(icon_path) member.Image = button_icon.small_bitmap else: - mlogger.warning('Icon file not found: %s', icon_path) + mlogger.warning("Icon file not found: %s", icon_path) except Exception as member_icon_err: - mlogger.debug('Error setting member icon: %s', member_icon_err) - + mlogger.debug( + "Error setting member icon: %s", member_icon_err + ) + # Set member group if available - if member_group and hasattr(member, 'GroupName'): + if member_group and hasattr(member, "GroupName"): try: member.GroupName = member_group except Exception as group_err: - mlogger.debug('Error setting member group: %s', group_err) - + mlogger.debug("Error setting member group: %s", group_err) + # Set member tooltip if available - if member_tooltip and hasattr(member, 'ToolTip'): + if member_tooltip and hasattr(member, "ToolTip"): try: member.ToolTip = member_tooltip except Exception as tooltip_err: - mlogger.debug('Error setting member tooltip: %s', tooltip_err) - + mlogger.debug( + "Error setting member tooltip: %s", tooltip_err + ) + # Set member extended tooltip if available - if member_tooltip_ext and hasattr(member, 'LongDescription'): + if member_tooltip_ext and hasattr(member, "LongDescription"): try: member.LongDescription = member_tooltip_ext except Exception as tooltip_ext_err: - mlogger.debug('Error setting member extended tooltip: %s', tooltip_ext_err) - + mlogger.debug( + "Error setting member extended tooltip: %s", + tooltip_ext_err, + ) + # Set member tooltip image if available - if member_tooltip_image and hasattr(member, 'ToolTipImage'): + if member_tooltip_image and hasattr(member, "ToolTipImage"): try: # Resolve tooltip image path (relative to bundle directory or absolute) - if combobox.directory and not op.isabs(member_tooltip_image): - tooltip_image_path = op.join(combobox.directory, member_tooltip_image) + if combobox.directory and not op.isabs( + member_tooltip_image + ): + tooltip_image_path = op.join( + combobox.directory, member_tooltip_image + ) else: tooltip_image_path = member_tooltip_image - + if op.exists(tooltip_image_path): from pyrevit.coreutils.ribbon import load_bitmapimage + tooltip_bitmap = load_bitmapimage(tooltip_image_path) member.ToolTipImage = tooltip_bitmap except Exception as tooltip_image_err: - mlogger.debug('Error setting member tooltip image: %s', tooltip_image_err) - + mlogger.debug( + "Error setting member tooltip image: %s", + tooltip_image_err, + ) + except Exception as add_err: - mlogger.warning('Error adding member: %s', add_err) + mlogger.warning("Error adding member: %s", add_err) # Set Current to first item # Note: Setting current will overwrite ItemText with the selected item's text @@ -660,63 +700,74 @@ def _produce_ui_combobox(ui_maker_params): try: combobox_ui.current = items[0] except Exception as current_err: - mlogger.debug('Error setting current item: %s', current_err) + mlogger.debug("Error setting current item: %s", current_err) # Call __selfinit__ on script (SmartButton pattern) try: - combobox_script_file = getattr(combobox, 'script_file', None) - combobox_unique_name = getattr(combobox, 'unique_name', None) - - if not combobox_script_file and hasattr(combobox, 'directory') and combobox.directory: - script_path = op.join(combobox.directory, 'script.py') + combobox_script_file = getattr(combobox, "script_file", None) + combobox_unique_name = getattr(combobox, "unique_name", None) + + if ( + not combobox_script_file + and hasattr(combobox, "directory") + and combobox.directory + ): + script_path = op.join(combobox.directory, "script.py") if op.exists(script_path): combobox_script_file = script_path if combobox_script_file and combobox_unique_name: current_paths = list(sys.path) - combobox_module_paths = getattr(combobox, 'module_paths', []) + combobox_module_paths = getattr(combobox, "module_paths", []) for search_path in combobox_module_paths: if search_path not in current_paths: sys.path.append(search_path) - imported_script = imp.load_source(combobox_unique_name, combobox_script_file) + imported_script = imp.load_source( + combobox_unique_name, combobox_script_file + ) sys.path = current_paths - if hasattr(imported_script, '__selfinit__'): - res = imported_script.__selfinit__(combobox, combobox_ui, HOST_APP.uiapp) + if hasattr(imported_script, "__selfinit__"): + res = imported_script.__selfinit__( + combobox, combobox_ui, HOST_APP.uiapp + ) if res is False: combobox_ui.deactivate() except Exception as init_err: - mlogger.error('Error in __selfinit__: %s', init_err) + mlogger.error("Error in __selfinit__: %s", init_err) import traceback - mlogger.error('Traceback: %s', traceback.format_exc()) + + mlogger.error("Traceback: %s", traceback.format_exc()) # Ensure visible & enabled try: - if hasattr(combobox_obj, 'Visible'): + if hasattr(combobox_obj, "Visible"): combobox_obj.Visible = True - if hasattr(combobox_obj, 'Enabled'): + if hasattr(combobox_obj, "Enabled"): combobox_obj.Enabled = True except Exception as vis_err: - mlogger.debug('Could not set visibility: %s', vis_err) + mlogger.debug("Could not set visibility: %s", vis_err) # Activate UI item try: combobox_ui.activate() except Exception as activate_err: - mlogger.debug('Could not activate: %s', activate_err) + mlogger.debug("Could not activate: %s", activate_err) return combobox_ui except PyRevitException as err: - mlogger.error('UI error creating ComboBox: %s', err.msg) + mlogger.error("UI error creating ComboBox: %s", err.msg) import traceback - mlogger.error('Traceback: %s', traceback.format_exc()) + + mlogger.error("Traceback: %s", traceback.format_exc()) return None except Exception as err: - mlogger.error('Error creating ComboBox: %s', err) + mlogger.error("Error creating ComboBox: %s", err) import traceback - mlogger.error('Full traceback: %s', traceback.format_exc()) + + mlogger.error("Full traceback: %s", traceback.format_exc()) return None @@ -729,18 +780,18 @@ def _produce_ui_split(ui_maker_params): parent_ribbon_panel = ui_maker_params.parent_ui split = ui_maker_params.component - mlogger.debug('Producing split button: %s}', split) + mlogger.debug("Producing split button: %s}", split) try: - parent_ribbon_panel.create_split_button(split.ui_title, - split.icon_file, - update_if_exists=True) + parent_ribbon_panel.create_split_button( + split.ui_title, split.icon_file, update_if_exists=True + ) split_ui = parent_ribbon_panel.ribbon_item(split.ui_title) _set_highlights(split, split_ui) return split_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -753,18 +804,18 @@ def _produce_ui_splitpush(ui_maker_params): parent_ribbon_panel = ui_maker_params.parent_ui splitpush = ui_maker_params.component - mlogger.debug('Producing splitpush button: %s', splitpush) + mlogger.debug("Producing splitpush button: %s", splitpush) try: - parent_ribbon_panel.create_splitpush_button(splitpush.ui_title, - splitpush.icon_file, - update_if_exists=True) + parent_ribbon_panel.create_splitpush_button( + splitpush.ui_title, splitpush.icon_file, update_if_exists=True + ) splitpush_ui = parent_ribbon_panel.ribbon_item(splitpush.ui_title) _set_highlights(splitpush, splitpush_ui) return splitpush_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -785,40 +836,44 @@ def _produce_ui_stacks(ui_maker_params): # (parent_ui_item.close_stack) to finish adding items to the stack. try: parent_ui_panel.open_stack() - mlogger.debug('Opened stack: %s', stack_cmp.name) + mlogger.debug("Opened stack: %s", stack_cmp.name) - if HOST_APP.is_older_than('2017'): - _component_creation_dict[exts.SPLIT_BUTTON_POSTFIX] = \ - _produce_ui_pulldown - _component_creation_dict[exts.SPLITPUSH_BUTTON_POSTFIX] = \ + if HOST_APP.is_older_than("2017"): + _component_creation_dict[exts.SPLIT_BUTTON_POSTFIX] = _produce_ui_pulldown + _component_creation_dict[exts.SPLITPUSH_BUTTON_POSTFIX] = ( _produce_ui_pulldown + ) # capturing and logging any errors on stack item # (e.g when parent_ui_panel's stack is full and can not add any # more items it will raise an error) _recursively_produce_ui_items( - UIMakerParams(parent_ui_panel, - stack_parent, - stack_cmp, - ext_asm_info, - ui_maker_params.create_beta_cmds)) - - if HOST_APP.is_older_than('2017'): - _component_creation_dict[exts.SPLIT_BUTTON_POSTFIX] = \ - _produce_ui_split - _component_creation_dict[exts.SPLITPUSH_BUTTON_POSTFIX] = \ + UIMakerParams( + parent_ui_panel, + stack_parent, + stack_cmp, + ext_asm_info, + ui_maker_params.create_beta_cmds, + ) + ) + + if HOST_APP.is_older_than("2017"): + _component_creation_dict[exts.SPLIT_BUTTON_POSTFIX] = _produce_ui_split + _component_creation_dict[exts.SPLITPUSH_BUTTON_POSTFIX] = ( _produce_ui_splitpush + ) try: parent_ui_panel.close_stack() - mlogger.debug('Closed stack: %s', stack_cmp.name) + mlogger.debug("Closed stack: %s", stack_cmp.name) return stack_cmp except PyRevitException as err: - mlogger.error('Error creating stack | %s', err) + mlogger.error("Error creating stack | %s", err) except Exception as err: - mlogger.error('Can not create stack under this parent: %s | %s', - parent_ui_panel, err) + mlogger.error( + "Can not create stack under this parent: %s | %s", parent_ui_panel, err + ) def _produce_ui_panelpushbutton(ui_maker_params): @@ -835,20 +890,22 @@ def _produce_ui_panelpushbutton(ui_maker_params): if panelpushbutton.is_beta and not ui_maker_params.create_beta_cmds: return None - mlogger.debug('Producing panel button: %s', panelpushbutton) + mlogger.debug("Producing panel button: %s", panelpushbutton) try: parent_ui_item.create_panel_push_button( button_name=panelpushbutton.name, asm_location=ext_asm_info.location, class_name=_get_effective_classname(panelpushbutton), tooltip=_make_button_tooltip(panelpushbutton), - tooltip_ext=_make_tooltip_ext_if_requested(panelpushbutton, - ext_asm_info.name), + tooltip_ext=_make_tooltip_ext_if_requested( + panelpushbutton, ext_asm_info.name + ), tooltip_media=panelpushbutton.media_file, ctxhelpurl=panelpushbutton.help_url, avail_class_name=panelpushbutton.avail_class_name, update_if_exists=True, - ui_title=_make_ui_title(panelpushbutton)) + ui_title=_make_ui_title(panelpushbutton), + ) panelpushbutton_ui = parent_ui_item.button(panelpushbutton.name) @@ -856,7 +913,7 @@ def _produce_ui_panelpushbutton(ui_maker_params): return panelpushbutton_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -875,7 +932,7 @@ def _produce_ui_panels(ui_maker_params): if panel.is_beta and not ui_maker_params.create_beta_cmds: return None - mlogger.debug('Producing ribbon panel: %s', panel) + mlogger.debug("Producing ribbon panel: %s", panel) try: parent_ui_tab.create_ribbon_panel(panel.ui_title, update_if_exists=True) panel_ui = parent_ui_tab.ribbon_panel(panel.ui_title) @@ -897,7 +954,7 @@ def _produce_ui_panels(ui_maker_params): return panel_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -910,10 +967,10 @@ def _produce_ui_tab(ui_maker_params): parent_ui = ui_maker_params.parent_ui tab = ui_maker_params.component - mlogger.debug('Verifying tab: %s', tab) + mlogger.debug("Verifying tab: %s", tab) if tab.has_commands(): - mlogger.debug('Tabs has command: %s', tab) - mlogger.debug('Producing ribbon tab: %s', tab) + mlogger.debug("Tabs has command: %s", tab) + mlogger.debug("Producing ribbon tab: %s", tab) try: parent_ui.create_ribbon_tab(tab.name, update_if_exists=True) tab_ui = parent_ui.ribbon_tab(tab.name) @@ -923,13 +980,13 @@ def _produce_ui_tab(ui_maker_params): return tab_ui except PyRevitException as err: # If tab is native, log as warning instead of error - if 'native item' in err.msg.lower(): - mlogger.warning('UI warning (tab may be native): %s', err.msg) + if "native item" in err.msg.lower(): + mlogger.warning("UI warning (tab may be native): %s", err.msg) else: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None else: - mlogger.debug('Tab does not have any commands. Skipping: %s', tab.name) + mlogger.debug("Tab does not have any commands. Skipping: %s", tab.name) return None @@ -950,7 +1007,7 @@ def _produce_ui_tab(ui_maker_params): exts.SEPARATOR_IDENTIFIER: _produce_ui_separator, exts.SLIDEOUT_IDENTIFIER: _produce_ui_slideout, exts.PANEL_PUSH_BUTTON_POSTFIX: _produce_ui_panelpushbutton, - } +} def _recursively_produce_ui_items(ui_maker_params): @@ -959,47 +1016,60 @@ def _recursively_produce_ui_items(ui_maker_params): ui_item = None try: # Diagnostic logging to track panel placement issues - parent_name = getattr(ui_maker_params.parent_ui, 'name', None) + parent_name = getattr(ui_maker_params.parent_ui, "name", None) if not parent_name: try: parent_name = str(type(ui_maker_params.parent_ui)) except: - parent_name = 'unknown' - mlogger.warning('BUILDING COMPONENT: %s parent: %s', sub_cmp.name, parent_name) - - mlogger.debug('Calling create func %s for: %s', - _component_creation_dict[sub_cmp.type_id], - sub_cmp) + parent_name = "unknown" + mlogger.warning( + "BUILDING COMPONENT: %s parent: %s", sub_cmp.name, parent_name + ) + + mlogger.debug( + "Calling create func %s for: %s", + _component_creation_dict[sub_cmp.type_id], + sub_cmp, + ) ui_item = _component_creation_dict[sub_cmp.type_id]( - UIMakerParams(ui_maker_params.parent_ui, - ui_maker_params.component, - sub_cmp, - ui_maker_params.asm_info, - ui_maker_params.create_beta_cmds)) + UIMakerParams( + ui_maker_params.parent_ui, + ui_maker_params.component, + sub_cmp, + ui_maker_params.asm_info, + ui_maker_params.create_beta_cmds, + ) + ) if ui_item: cmp_count += 1 except KeyError: - mlogger.warning('Can not find create function for type_id: %s (component: %s)', - sub_cmp.type_id, sub_cmp) - except Exception as create_err: - mlogger.critical( - 'Error creating item: %s | %s', sub_cmp, create_err + mlogger.warning( + "Can not find create function for type_id: %s (component: %s)", + sub_cmp.type_id, + sub_cmp, ) + except Exception as create_err: + mlogger.critical("Error creating item: %s | %s", sub_cmp, create_err) - mlogger.debug('UI item created by create func is: %s', ui_item) + mlogger.debug("UI item created by create func is: %s", ui_item) # if component does not have any sub components hide it # Exclude GenericStack and ComboBoxGroup from deactivation check # (GenericStack is a special container, ComboBoxGroup has members not child components) - if ui_item \ - and not isinstance(ui_item, components.GenericStack) \ - and not isinstance(sub_cmp, components.ComboBoxGroup) \ - and sub_cmp.is_container: + if ( + ui_item + and not isinstance(ui_item, components.GenericStack) + and not isinstance(sub_cmp, components.ComboBoxGroup) + and sub_cmp.is_container + ): subcmp_count = _recursively_produce_ui_items( - UIMakerParams(ui_item, - ui_maker_params.component, - sub_cmp, - ui_maker_params.asm_info, - ui_maker_params.create_beta_cmds)) + UIMakerParams( + ui_item, + ui_maker_params.component, + sub_cmp, + ui_maker_params.asm_info, + ui_maker_params.create_beta_cmds, + ) + ) # if component does not have any sub components hide it if subcmp_count == 0: @@ -1019,10 +1089,11 @@ def update_pyrevit_ui(ui_ext, ext_asm_info, create_beta=False): ext_asm_info (AssemblyInfo): Assembly info. create_beta (bool, optional): Create beta ui. Defaults to False. """ - mlogger.debug('Creating/Updating ui for extension: %s', ui_ext) + mlogger.debug("Creating/Updating ui for extension: %s", ui_ext) cmp_count = _recursively_produce_ui_items( - UIMakerParams(current_ui, None, ui_ext, ext_asm_info, create_beta)) - mlogger.debug('%s components were created for: %s', cmp_count, ui_ext) + UIMakerParams(current_ui, None, ui_ext, ext_asm_info, create_beta) + ) + mlogger.debug("%s components were created for: %s", cmp_count, ui_ext) def sort_pyrevit_ui(ui_ext): @@ -1036,13 +1107,13 @@ def sort_pyrevit_ui(ui_ext): for tab in current_ui.get_pyrevit_tabs(): for litem in ui_ext.find_layout_items(): if litem.directive: - if litem.directive.directive_type == 'before': + if litem.directive.directive_type == "before": tab.reorder_before(litem.name, litem.directive.target) - elif litem.directive.directive_type == 'after': + elif litem.directive.directive_type == "after": tab.reorder_after(litem.name, litem.directive.target) - elif litem.directive.directive_type == 'afterall': + elif litem.directive.directive_type == "afterall": tab.reorder_afterall(litem.name) - elif litem.directive.directive_type == 'beforeall': + elif litem.directive.directive_type == "beforeall": tab.reorder_beforeall(litem.name) @@ -1057,11 +1128,15 @@ def cleanup_pyrevit_ui(): for item in untouched_items: if not item.is_native(): try: - mlogger.debug('Deactivating: %s', item) + mlogger.debug("Deactivating: %s", item) item.deactivate() except Exception as deact_err: # Log as debug to avoid cluttering output with expected errors - mlogger.debug('Could not deactivate item (may be native): %s | %s', item, deact_err) + mlogger.debug( + "Could not deactivate item (may be native): %s | %s", + item, + deact_err, + ) def reflow_pyrevit_ui(direction=applocales.DEFAULT_LANG_DIR): From 328ce2e3fbc5e95e75b494007ad39c8cb684faad Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Wed, 10 Dec 2025 10:43:24 +0000 Subject: [PATCH 09/12] Copilot auto checks resolutions - resolved auto generated Copilot checks --- pyrevitlib/pyrevit/coreutils/ribbon.py | 17 ++++++++++-- pyrevitlib/pyrevit/loader/uimaker.py | 38 ++++++++++++-------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index 1834b3484..c4c3e5d72 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -1171,7 +1171,10 @@ def add_item(self, member_data): member_data (UI.ComboBoxMemberData): Member data to add Returns: - (UI.ComboBoxMember): The created ComboBoxMember object, or None + (UI.ComboBoxMember): The created ComboBoxMember object, or None. + None is returned if the ComboBox is in itemdata_mode (i.e., when + the underlying Revit API object is not available and items cannot + be added directly). """ if not self.itemdata_mode: try: @@ -1189,6 +1192,9 @@ def add_items(self, member_data_list): Args: member_data_list (list): List of UI.ComboBoxMemberData objects + + Returns: + None """ if not self.itemdata_mode: try: @@ -1200,7 +1206,14 @@ def add_items(self, member_data_list): ) def add_separator(self): - """Add a separator to the ComboBox dropdown list.""" + """Add a separator to the ComboBox dropdown list. + + Returns: + None + + Raises: + PyRevitUIError: If adding the separator fails. + """ if not self.itemdata_mode: try: self._rvtapi_object.AddSeparator() diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 3ef2591f4..7e512462a 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -21,10 +21,6 @@ mlogger = get_logger(__name__) -# Enable verbose logging to see WARNING messages (can be removed later) -# Uncomment the line below to see all log messages: -# mlogger.set_verbose_mode() - CONFIG_SCRIPT_TITLE_POSTFIX = "\u25CF" @@ -178,12 +174,12 @@ def _produce_ui_slideout(ui_maker_params): try: if hasattr(parent_ui_item, "get_rvtapi_object"): existing_items = parent_ui_item.get_rvtapi_object().GetItems() - mlogger.warning( + mlogger.debug( "SLIDEOUT: Panel has %d items before adding slideout", len(existing_items), ) for idx, item in enumerate(existing_items): - mlogger.warning( + mlogger.debug( "SLIDEOUT: Existing item %d: %s (type: %s)", idx, getattr(item, "Name", "unknown"), @@ -192,10 +188,10 @@ def _produce_ui_slideout(ui_maker_params): except Exception as log_err: mlogger.debug("SLIDEOUT: Could not log existing items: %s", log_err) - mlogger.warning("SLIDEOUT: Adding slide out to: %s", parent_ui_item) + mlogger.debug("SLIDEOUT: Adding slide out to: %s", parent_ui_item) try: parent_ui_item.add_slideout() - mlogger.warning("SLIDEOUT: Slideout added successfully") + mlogger.debug("SLIDEOUT: Slideout added successfully") except PyRevitException as err: mlogger.error("UI error: %s", err.msg) @@ -613,8 +609,8 @@ def _produce_ui_combobox(ui_maker_params): # Add member to ComboBox first (returns ComboBoxMember object) try: - member = combobox_ui.add_item(member_data) - if not member: + member_obj = combobox_ui.add_item(member_data) + if not member_obj: mlogger.warning("AddItem returned None for: %s", member_text) continue @@ -631,7 +627,7 @@ def _produce_ui_combobox(ui_maker_params): if op.exists(icon_path): button_icon = ribbon.ButtonIcons(icon_path) - member.Image = button_icon.small_bitmap + member_obj.Image = button_icon.small_bitmap else: mlogger.warning("Icon file not found: %s", icon_path) except Exception as member_icon_err: @@ -640,25 +636,25 @@ def _produce_ui_combobox(ui_maker_params): ) # Set member group if available - if member_group and hasattr(member, "GroupName"): + if member_group and hasattr(member_obj, "GroupName"): try: - member.GroupName = member_group + member_obj.GroupName = member_group except Exception as group_err: mlogger.debug("Error setting member group: %s", group_err) # Set member tooltip if available - if member_tooltip and hasattr(member, "ToolTip"): + if member_tooltip and hasattr(member_obj, "ToolTip"): try: - member.ToolTip = member_tooltip + member_obj.ToolTip = member_tooltip except Exception as tooltip_err: mlogger.debug( "Error setting member tooltip: %s", tooltip_err ) # Set member extended tooltip if available - if member_tooltip_ext and hasattr(member, "LongDescription"): + if member_tooltip_ext and hasattr(member_obj, "LongDescription"): try: - member.LongDescription = member_tooltip_ext + member_obj.LongDescription = member_tooltip_ext except Exception as tooltip_ext_err: mlogger.debug( "Error setting member extended tooltip: %s", @@ -666,7 +662,7 @@ def _produce_ui_combobox(ui_maker_params): ) # Set member tooltip image if available - if member_tooltip_image and hasattr(member, "ToolTipImage"): + if member_tooltip_image and hasattr(member_obj, "ToolTipImage"): try: # Resolve tooltip image path (relative to bundle directory or absolute) if combobox.directory and not op.isabs( @@ -682,7 +678,7 @@ def _produce_ui_combobox(ui_maker_params): from pyrevit.coreutils.ribbon import load_bitmapimage tooltip_bitmap = load_bitmapimage(tooltip_image_path) - member.ToolTipImage = tooltip_bitmap + member_obj.ToolTipImage = tooltip_bitmap except Exception as tooltip_image_err: mlogger.debug( "Error setting member tooltip image: %s", @@ -1020,9 +1016,9 @@ def _recursively_produce_ui_items(ui_maker_params): if not parent_name: try: parent_name = str(type(ui_maker_params.parent_ui)) - except: + except Exception: parent_name = "unknown" - mlogger.warning( + mlogger.debug( "BUILDING COMPONENT: %s parent: %s", sub_cmp.name, parent_name ) From 28731e5334a67e6831276c95ebc9996347e048ef Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Wed, 10 Dec 2025 11:19:07 +0000 Subject: [PATCH 10/12] refactor: address code review feedback - Fix debug logging levels and exception handling - Improve code style: reduce nesting, extract helpers - Add missing docstring documentation - Remove redundant code and clarify comments Addresses Copilot AI and human review suggestions. --- pyrevitlib/pyrevit/coreutils/ribbon.py | 128 ++++++++--------- pyrevitlib/pyrevit/extensions/components.py | 45 +++--- pyrevitlib/pyrevit/loader/uimaker.py | 152 +++++++++----------- 3 files changed, 149 insertions(+), 176 deletions(-) diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index c4c3e5d72..2622c827b 100644 --- a/pyrevitlib/pyrevit/coreutils/ribbon.py +++ b/pyrevitlib/pyrevit/coreutils/ribbon.py @@ -897,8 +897,7 @@ def set_title(self, ui_title): def get_title(self): if self.itemdata_mode: return self.ui_title - else: - return self._rvtapi_object.ItemText + return self._rvtapi_object.ItemText def get_control_id(self): adwindows_obj = self.get_adwindows_object() @@ -1029,21 +1028,21 @@ def set_tooltip_image(self, tooltip_image): """ try: adwindows_obj = self.get_adwindows_object() - if adwindows_obj: - exToolTip = self.get_rvtapi_object().ToolTip - if not isinstance(exToolTip, str): - exToolTip = None - adwindows_obj.ToolTip = AdWindows.RibbonToolTip() - adwindows_obj.ToolTip.Title = self.ui_title - adwindows_obj.ToolTip.Content = exToolTip - _StackPanel = System.Windows.Controls.StackPanel() - _image = System.Windows.Controls.Image() - _image.Source = load_bitmapimage(tooltip_image) - _StackPanel.Children.Add(_image) - adwindows_obj.ToolTip.ExpandedContent = _StackPanel - adwindows_obj.ResolveToolTip() - else: + if not adwindows_obj: self.tooltip_image = tooltip_image + return + exToolTip = self.get_rvtapi_object().ToolTip + if not isinstance(exToolTip, str): + exToolTip = None + adwindows_obj.ToolTip = AdWindows.RibbonToolTip() + adwindows_obj.ToolTip.Title = self.ui_title + adwindows_obj.ToolTip.Content = exToolTip + _StackPanel = System.Windows.Controls.StackPanel() + _image = System.Windows.Controls.Image() + _image.Source = load_bitmapimage(tooltip_image) + _StackPanel.Children.Add(_image) + adwindows_obj.ToolTip.ExpandedContent = _StackPanel + adwindows_obj.ResolveToolTip() except Exception as ttimage_err: raise PyRevitUIError( "Error setting tooltip image {} | {} ".format( @@ -1059,34 +1058,34 @@ def set_tooltip_video(self, tooltip_video): """ try: adwindows_obj = self.get_adwindows_object() - if adwindows_obj: - exToolTip = self.get_rvtapi_object().ToolTip - if not isinstance(exToolTip, str): - exToolTip = None - adwindows_obj.ToolTip = AdWindows.RibbonToolTip() - adwindows_obj.ToolTip.Title = self.ui_title - adwindows_obj.ToolTip.Content = exToolTip - _StackPanel = System.Windows.Controls.StackPanel() - _video = System.Windows.Controls.MediaElement() - _video.Source = Uri(tooltip_video) - _video.LoadedBehavior = System.Windows.Controls.MediaState.Manual - _video.UnloadedBehavior = System.Windows.Controls.MediaState.Manual - - def on_media_ended(sender, args): - sender.Position = System.TimeSpan.Zero - sender.Play() - - _video.MediaEnded += on_media_ended - - def on_loaded(sender, args): - sender.Play() - - _video.Loaded += on_loaded - _StackPanel.Children.Add(_video) - adwindows_obj.ToolTip.ExpandedContent = _StackPanel - adwindows_obj.ResolveToolTip() - else: + if not adwindows_obj: self.tooltip_video = tooltip_video + return + exToolTip = self.get_rvtapi_object().ToolTip + if not isinstance(exToolTip, str): + exToolTip = None + adwindows_obj.ToolTip = AdWindows.RibbonToolTip() + adwindows_obj.ToolTip.Title = self.ui_title + adwindows_obj.ToolTip.Content = exToolTip + _StackPanel = System.Windows.Controls.StackPanel() + _video = System.Windows.Controls.MediaElement() + _video.Source = Uri(tooltip_video) + _video.LoadedBehavior = System.Windows.Controls.MediaState.Manual + _video.UnloadedBehavior = System.Windows.Controls.MediaState.Manual + + def on_media_ended(sender, args): + sender.Position = System.TimeSpan.Zero + sender.Play() + + _video.MediaEnded += on_media_ended + + def on_loaded(sender, args): + sender.Play() + + _video.Loaded += on_loaded + _StackPanel.Children.Add(_video) + adwindows_obj.ToolTip.ExpandedContent = _StackPanel + adwindows_obj.ResolveToolTip() except Exception as ttvideo_err: raise PyRevitUIError( "Error setting tooltip video {} | {} ".format( @@ -1278,37 +1277,31 @@ def process_deferred(self): ) ) - def reset_highlights(self): - """Reset highlight state.""" + def _set_highlight(self, highlight_value): + """Set highlight value on the adwindows object. + + Args: + highlight_value: The highlight mode value to set + """ try: if hasattr(AdInternal.Windows, "HighlightMode"): adwindows_obj = self.get_adwindows_object() if adwindows_obj and hasattr(adwindows_obj, "Highlight"): - adwindows_obj.Highlight = coreutils.get_enum_none( - AdInternal.Windows.HighlightMode - ) + adwindows_obj.Highlight = highlight_value except Exception: pass # Highlights are optional, fail silently + def reset_highlights(self): + """Reset highlight state.""" + self._set_highlight(coreutils.get_enum_none(AdInternal.Windows.HighlightMode)) + def highlight_as_new(self): """Highlight as new item.""" - try: - if hasattr(AdInternal.Windows, "HighlightMode"): - adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, "Highlight"): - adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.New - except Exception: - pass # Highlights are optional, fail silently + self._set_highlight(AdInternal.Windows.HighlightMode.New) def highlight_as_updated(self): """Highlight as updated item.""" - try: - if hasattr(AdInternal.Windows, "HighlightMode"): - adwindows_obj = self.get_adwindows_object() - if adwindows_obj and hasattr(adwindows_obj, "Highlight"): - adwindows_obj.Highlight = AdInternal.Windows.HighlightMode.Updated - except Exception: - pass # Highlights are optional, fail silently + self._set_highlight(AdInternal.Windows.HighlightMode.Updated) class _PyRevitRibbonGroupItem(GenericPyRevitUIContainer): @@ -1966,16 +1959,15 @@ def create_combobox(self, item_name, update_if_exists=False): update_if_exists (bool, optional): Update if exists. Defaults to False. """ if self.contains(item_name): - if update_if_exists: - existing_item = self._get_component(item_name) - existing_item.activate() - # Return existing item so caller can update members - return existing_item - else: + if not update_if_exists: raise PyRevitUIError( "ComboBox already exists and " "update is not allowed: {}".format(item_name) ) + existing_item = self._get_component(item_name) + existing_item.activate() + # Return existing item so caller can update members + return existing_item # Create ComboBoxData combobox_data = UI.ComboBoxData(item_name) diff --git a/pyrevitlib/pyrevit/extensions/components.py b/pyrevitlib/pyrevit/extensions/components.py index c0314a2ef..774dee9b4 100644 --- a/pyrevitlib/pyrevit/extensions/components.py +++ b/pyrevitlib/pyrevit/extensions/components.py @@ -323,28 +323,29 @@ def __init__(self, cmp_path=None): self.members = [] # Read members from metadata - if self.meta: - raw_members = self.meta.get("members", []) - if isinstance(raw_members, list): - # Process list of members - preserve full dict for rich metadata (icons, tooltips, etc.) - processed_members = [] - for m in raw_members: - if isinstance(m, dict) or ( - hasattr(m, "get") and hasattr(m, "keys") - ): - # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings', 'icon': '...', ...} - # Preserve the full dictionary to keep all properties (icon, tooltip, group, etc.) - processed_members.append(m) - elif isinstance(m, (list, tuple)) and len(m) >= 2: - # Tuple/list format: ('id', 'text') - convert to dict for consistency - processed_members.append({"id": m[0], "text": m[1]}) - elif isinstance(m, str): - # String format: 'Option 1' - convert to dict for consistency - processed_members.append({"id": m, "text": m}) - self.members = processed_members - elif isinstance(raw_members, dict): - # Dict format: {'A': 'Option A'} - convert to list of dicts - self.members = [{"id": k, "text": v} for k, v in raw_members.items()] + if not self.meta: + return + raw_members = self.meta.get("members", []) + if isinstance(raw_members, list): + # Process list of members - preserve full dict for rich metadata (icons, tooltips, etc.) + processed_members = [] + for m in raw_members: + if isinstance(m, dict) or ( + hasattr(m, "get") and hasattr(m, "keys") + ): + # OrderedDict or dict format: {'id': 'settings', 'text': 'Settings', 'icon': '...', ...} + # Preserve the full dictionary to keep all properties (icon, tooltip, group, etc.) + processed_members.append(m) + elif isinstance(m, (list, tuple)) and len(m) >= 2: + # Tuple/list format: ('id', 'text') - convert to dict for consistency + processed_members.append({"id": m[0], "text": m[1]}) + elif isinstance(m, str): + # String format: 'Option 1' - convert to dict for consistency + processed_members.append({"id": m, "text": m}) + self.members = processed_members + elif isinstance(raw_members, dict): + # Dict format: {'A': 'Option A'} - convert to list of dicts + self.members = [{"id": k, "text": v} for k, v in raw_members.items()] class SplitPushButtonGroup(GenericUICommandGroup): diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 7e512462a..843277e26 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -450,86 +450,82 @@ def _produce_ui_combobox(ui_maker_params): Args: ui_maker_params (UIMakerParams): Standard parameters for making ui item. """ - try: - parent_ribbon_panel = ui_maker_params.parent_ui - combobox = ui_maker_params.component - combobox_name = getattr(combobox, "name", "unknown") + parent_ribbon_panel = ui_maker_params.parent_ui + combobox = ui_maker_params.component + combobox_name = getattr(combobox, "name", "unknown") - # Validate inputs first - if not combobox: - mlogger.error("Component is None") - return None - if not parent_ribbon_panel: - mlogger.error("Parent UI is None for: %s", combobox_name) - return None + # Validate inputs first + if not combobox: + mlogger.error("Component is None") + return None + if not parent_ribbon_panel: + mlogger.error("Parent UI is None for: %s", combobox_name) + return None - # Get panel API object - try: - panel_rvtapi = parent_ribbon_panel.get_rvtapi_object() - if not panel_rvtapi: - mlogger.error("Panel Revit API object is None for: %s", combobox_name) - return None - except Exception as panel_err: - mlogger.error("Could not get panel Revit API object: %s", panel_err) + # Get panel API object + try: + panel_rvtapi = parent_ribbon_panel.get_rvtapi_object() + if not panel_rvtapi: + mlogger.error("Panel Revit API object is None for: %s", combobox_name) return None + except Exception as panel_err: + mlogger.error("Could not get panel Revit API object: %s", panel_err) + return None - # Create combobox - try: - parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) - except Exception as create_err: - mlogger.error("Error calling create_combobox: %s", create_err) - import traceback + # Create combobox + try: + parent_ribbon_panel.create_combobox(combobox_name, update_if_exists=True) + except Exception as create_err: + mlogger.exception("Error calling create_combobox: %s", create_err) + return None - mlogger.error("Traceback: %s", traceback.format_exc()) - return None + combobox_ui = parent_ribbon_panel.ribbon_item(combobox_name) + if not combobox_ui: + mlogger.error("Failed to get ComboBox UI item: %s", combobox_name) + return None - combobox_ui = parent_ribbon_panel.ribbon_item(combobox_name) - if not combobox_ui: - mlogger.error("Failed to get ComboBox UI item: %s", combobox_name) - return None + # Get the Revit API ComboBox object + try: + combobox_obj = combobox_ui.get_rvtapi_object() + except Exception as rvtapi_err: + mlogger.error( + "get_rvtapi_object() failed for %s: %s", combobox_name, rvtapi_err + ) + return None - # Get the Revit API ComboBox object - try: - combobox_obj = combobox_ui.get_rvtapi_object() - except Exception as rvtapi_err: - mlogger.error( - "get_rvtapi_object() failed for %s: %s", combobox_name, rvtapi_err - ) - return None + if not combobox_obj: + mlogger.error("get_rvtapi_object() returned None for: %s", combobox_name) + return None - if not combobox_obj: - mlogger.error("get_rvtapi_object() returned None for: %s", combobox_name) - return None + # Return early if ComboBox is still in data mode (not yet added to panel) + if isinstance(combobox_obj, UI.ComboBoxData): + return combobox_ui - # IMPORTANT: only bail here if it is still ComboBoxData - if isinstance(combobox_obj, UI.ComboBoxData): - return combobox_ui # <- ONLY in this branch + # From here on we have a real Autodesk.Revit.UI.ComboBox - # From here on we have a real Autodesk.Revit.UI.ComboBox + # Set ItemText/Title + # Note: In Revit, ComboBox.ItemText displays the current selected item's text in the dropdown + # There is no separate visible "title" label for ComboBoxes like buttons have + # The title from bundle.yaml is used for tooltip and identification + # We'll set ItemText to the title initially, but it will be overwritten when current item is set + combobox_title = getattr(combobox, "ui_title", None) or combobox_name + if combobox_title: + try: + # Set initial ItemText to title (will be overwritten when current item is set) + combobox_obj.ItemText = combobox_title + except Exception as title_err: + mlogger.debug("Could not set ItemText: %s", title_err) - # Set ItemText/Title - # Note: In Revit, ComboBox.ItemText displays the current selected item's text in the dropdown - # There is no separate visible "title" label for ComboBoxes like buttons have - # The title from bundle.yaml is used for tooltip and identification - # We'll set ItemText to the title initially, but it will be overwritten when current item is set - combobox_title = getattr(combobox, "ui_title", None) or combobox_name - if combobox_title: - try: - # Set initial ItemText to title (will be overwritten when current item is set) - combobox_obj.ItemText = combobox_title - except Exception as title_err: - mlogger.debug("Could not set ItemText: %s", title_err) - - # Set icon if available - parent = ui_maker_params.parent_cmp - icon_file = getattr(combobox, "icon_file", None) or getattr( - parent, "icon_file", None - ) - if icon_file: - try: - combobox_ui.set_icon(icon_file, icon_size=ICON_MEDIUM) - except Exception as icon_err: - mlogger.debug("Error setting icon: %s", icon_err) + # Set icon if available + parent = ui_maker_params.parent_cmp + icon_file = getattr(combobox, "icon_file", None) or getattr( + parent, "icon_file", None + ) + if icon_file: + try: + combobox_ui.set_icon(icon_file, icon_size=ICON_MEDIUM) + except Exception as icon_err: + mlogger.debug("Error setting icon: %s", icon_err) # Set tooltip if available tooltip = getattr(combobox, "tooltip", None) @@ -731,10 +727,7 @@ def _produce_ui_combobox(ui_maker_params): if res is False: combobox_ui.deactivate() except Exception as init_err: - mlogger.error("Error in __selfinit__: %s", init_err) - import traceback - - mlogger.error("Traceback: %s", traceback.format_exc()) + mlogger.exception("Error in __selfinit__: %s", init_err) # Ensure visible & enabled try: @@ -753,19 +746,6 @@ def _produce_ui_combobox(ui_maker_params): return combobox_ui - except PyRevitException as err: - mlogger.error("UI error creating ComboBox: %s", err.msg) - import traceback - - mlogger.error("Traceback: %s", traceback.format_exc()) - return None - except Exception as err: - mlogger.error("Error creating ComboBox: %s", err) - import traceback - - mlogger.error("Full traceback: %s", traceback.format_exc()) - return None - def _produce_ui_split(ui_maker_params): """Produce a split button. From ca0495a6b381fa954adbd02dcca0fcd59a4fad8a Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Wed, 10 Dec 2025 13:41:20 +0000 Subject: [PATCH 11/12] refactor: extract combobox setup and members logic into helper functions - Extract _setup_combobox_objects() to handle validation and object creation - Extract _add_combobox_members() to handle member addition logic - Refactor _produce_ui_combobox() for better readability and maintainability - Fix critical indentation bug: unindent code after icon check so tooltips, members, and activation run regardless of icon file presence This refactoring improves code organization by separating concerns: - Setup/validation logic is isolated and testable - Member addition logic is self-contained - Main function focuses on high-level configuration flow --- pyrevitlib/pyrevit/loader/uimaker.py | 461 +++++++++++++++------------ 1 file changed, 250 insertions(+), 211 deletions(-) diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 843277e26..a57db957c 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -444,11 +444,15 @@ def _produce_ui_pulldown(ui_maker_params): return None -def _produce_ui_combobox(ui_maker_params): - """Create a ComboBox with full property support. +def _setup_combobox_objects(ui_maker_params): + """Setup and validate combobox objects. Args: ui_maker_params (UIMakerParams): Standard parameters for making ui item. + + Returns: + tuple: (combobox_ui, combobox_obj) if successful, None otherwise. + If ComboBox is in data mode, returns (combobox_ui, None). """ parent_ribbon_panel = ui_maker_params.parent_ui combobox = ui_maker_params.component @@ -499,8 +503,165 @@ def _produce_ui_combobox(ui_maker_params): # Return early if ComboBox is still in data mode (not yet added to panel) if isinstance(combobox_obj, UI.ComboBoxData): + return (combobox_ui, None) + + return (combobox_ui, combobox_obj) + + +def _add_combobox_members(combobox_ui, combobox): + """Add members to a ComboBox from metadata. + + Args: + combobox_ui: The ComboBox UI object to add members to + combobox: The combobox component with members metadata + """ + if not hasattr(combobox, "members") or not combobox.members: + return + + for member in combobox.members: + member_id = None + member_text = None + member_icon = None + member_group = None + member_tooltip = None + member_tooltip_ext = None + member_tooltip_image = None + + if isinstance(member, (list, tuple)) and len(member) >= 2: + member_id, member_text = member[0], member[1] + elif isinstance(member, dict) or ( + hasattr(member, "get") and hasattr(member, "keys") + ): + member_id = member.get("id", member.get("name", "")) + member_text = member.get("text", member.get("title", member_id)) + member_icon = member.get("icon", None) + member_group = member.get("group", member.get("groupName", None)) + member_tooltip = member.get("tooltip", None) + member_tooltip_ext = member.get( + "tooltip_ext", member.get("longDescription", None) + ) + member_tooltip_image = member.get( + "tooltip_image", member.get("tooltipImage", None) + ) + elif isinstance(member, str): + member_id = member_text = member + else: + mlogger.warning( + "Skipping invalid member format: %s (type: %s)", + member, + type(member), + ) + continue + + if not member_id or not member_text: + mlogger.warning("Skipping member with missing id or text") + continue + + # Create member data (minimal - just id and text) + member_data = UI.ComboBoxMemberData(member_id, member_text) + + # Add member to ComboBox first (returns ComboBoxMember object) + try: + member_obj = combobox_ui.add_item(member_data) + if not member_obj: + mlogger.warning("AddItem returned None for: %s", member_text) + continue + + # Now set properties on the actual ComboBoxMember object (not the data) + + # Set member icon if available + if member_icon: + try: + # Resolve icon path (relative to bundle directory or absolute) + if combobox.directory and not op.isabs(member_icon): + icon_path = op.join(combobox.directory, member_icon) + else: + icon_path = member_icon + + if op.exists(icon_path): + button_icon = ribbon.ButtonIcons(icon_path) + member_obj.Image = button_icon.small_bitmap + else: + mlogger.warning("Icon file not found: %s", icon_path) + except Exception as member_icon_err: + mlogger.debug( + "Error setting member icon: %s", member_icon_err + ) + + # Set member group if available + if member_group and hasattr(member_obj, "GroupName"): + try: + member_obj.GroupName = member_group + except Exception as group_err: + mlogger.debug("Error setting member group: %s", group_err) + + # Set member tooltip if available + if member_tooltip and hasattr(member_obj, "ToolTip"): + try: + member_obj.ToolTip = member_tooltip + except Exception as tooltip_err: + mlogger.debug( + "Error setting member tooltip: %s", tooltip_err + ) + + # Set member extended tooltip if available + if member_tooltip_ext and hasattr(member_obj, "LongDescription"): + try: + member_obj.LongDescription = member_tooltip_ext + except Exception as tooltip_ext_err: + mlogger.debug( + "Error setting member extended tooltip: %s", + tooltip_ext_err, + ) + + # Set member tooltip image if available + if member_tooltip_image and hasattr(member_obj, "ToolTipImage"): + try: + # Resolve tooltip image path (relative to bundle directory or absolute) + if combobox.directory and not op.isabs( + member_tooltip_image + ): + tooltip_image_path = op.join( + combobox.directory, member_tooltip_image + ) + else: + tooltip_image_path = member_tooltip_image + + if op.exists(tooltip_image_path): + from pyrevit.coreutils.ribbon import load_bitmapimage + + tooltip_bitmap = load_bitmapimage(tooltip_image_path) + member_obj.ToolTipImage = tooltip_bitmap + except Exception as tooltip_image_err: + mlogger.debug( + "Error setting member tooltip image: %s", + tooltip_image_err, + ) + + except Exception as add_err: + mlogger.warning("Error adding member: %s", add_err) + + +def _produce_ui_combobox(ui_maker_params): + """Create a ComboBox with full property support. + + Args: + ui_maker_params (UIMakerParams): Standard parameters for making ui item. + """ + # Setup and get combobox objects + setup_result = _setup_combobox_objects(ui_maker_params) + if setup_result is None: + return None + + combobox_ui, combobox_obj = setup_result + + # If in data mode, return early + if combobox_obj is None: return combobox_ui + combobox = ui_maker_params.component + combobox_name = getattr(combobox, "name", "unknown") + # From here on we have a real Autodesk.Revit.UI.ComboBox # Set ItemText/Title @@ -527,224 +688,102 @@ def _produce_ui_combobox(ui_maker_params): except Exception as icon_err: mlogger.debug("Error setting icon: %s", icon_err) - # Set tooltip if available - tooltip = getattr(combobox, "tooltip", None) - if tooltip: - try: - combobox_ui.set_tooltip(tooltip) - except Exception as tooltip_err: - mlogger.debug("Error setting tooltip: %s", tooltip_err) - - # Set extended tooltip if available - tooltip_ext = getattr(combobox, "tooltip_ext", None) - if tooltip_ext: - try: - combobox_ui.set_tooltip_ext(tooltip_ext) - except Exception as tooltip_ext_err: - mlogger.debug("Error setting extended tooltip: %s", tooltip_ext_err) + # Set tooltip if available + tooltip = getattr(combobox, "tooltip", None) + if tooltip: + try: + combobox_ui.set_tooltip(tooltip) + except Exception as tooltip_err: + mlogger.debug("Error setting tooltip: %s", tooltip_err) - # Set tooltip media (image/video) if available - tooltip_media = getattr(combobox, "media_file", None) - if tooltip_media: - try: - combobox_ui.set_tooltip_media(tooltip_media) - except Exception as tooltip_media_err: - mlogger.debug("Error setting tooltip media: %s", tooltip_media_err) + # Set extended tooltip if available + tooltip_ext = getattr(combobox, "tooltip_ext", None) + if tooltip_ext: + try: + combobox_ui.set_tooltip_ext(tooltip_ext) + except Exception as tooltip_ext_err: + mlogger.debug("Error setting extended tooltip: %s", tooltip_ext_err) - # Set contextual help if available - help_url = getattr(combobox, "help_url", None) - if help_url: - try: - combobox_ui.set_contexthelp(help_url) - except Exception as help_err: - mlogger.debug("Error setting contextual help: %s", help_err) - - # Add members from metadata - if hasattr(combobox, "members") and combobox.members: - for member in combobox.members: - member_id = None - member_text = None - member_icon = None - member_group = None - member_tooltip = None - member_tooltip_ext = None - member_tooltip_image = None - - if isinstance(member, (list, tuple)) and len(member) >= 2: - member_id, member_text = member[0], member[1] - elif isinstance(member, dict) or ( - hasattr(member, "get") and hasattr(member, "keys") - ): - member_id = member.get("id", member.get("name", "")) - member_text = member.get("text", member.get("title", member_id)) - member_icon = member.get("icon", None) - member_group = member.get("group", member.get("groupName", None)) - member_tooltip = member.get("tooltip", None) - member_tooltip_ext = member.get( - "tooltip_ext", member.get("longDescription", None) - ) - member_tooltip_image = member.get( - "tooltip_image", member.get("tooltipImage", None) - ) - elif isinstance(member, str): - member_id = member_text = member - else: - mlogger.warning( - "Skipping invalid member format: %s (type: %s)", - member, - type(member), - ) - continue + # Set tooltip media (image/video) if available + tooltip_media = getattr(combobox, "media_file", None) + if tooltip_media: + try: + combobox_ui.set_tooltip_media(tooltip_media) + except Exception as tooltip_media_err: + mlogger.debug("Error setting tooltip media: %s", tooltip_media_err) - if not member_id or not member_text: - mlogger.warning("Skipping member with missing id or text") - continue + # Set contextual help if available + help_url = getattr(combobox, "help_url", None) + if help_url: + try: + combobox_ui.set_contexthelp(help_url) + except Exception as help_err: + mlogger.debug("Error setting contextual help: %s", help_err) + + # Add members from metadata + _add_combobox_members(combobox_ui, combobox) + + # Set Current to first item + # Note: Setting current will overwrite ItemText with the selected item's text + # This is expected behavior - ComboBox.ItemText shows the current selection + items = combobox_ui.get_items() + if items and len(items) > 0: + try: + combobox_ui.current = items[0] + except Exception as current_err: + mlogger.debug("Error setting current item: %s", current_err) - # Create member data (minimal - just id and text) - member_data = UI.ComboBoxMemberData(member_id, member_text) + # Call __selfinit__ on script (SmartButton pattern) + try: + combobox_script_file = getattr(combobox, "script_file", None) + combobox_unique_name = getattr(combobox, "unique_name", None) - # Add member to ComboBox first (returns ComboBoxMember object) - try: - member_obj = combobox_ui.add_item(member_data) - if not member_obj: - mlogger.warning("AddItem returned None for: %s", member_text) - continue - - # Now set properties on the actual ComboBoxMember object (not the data) - - # Set member icon if available - if member_icon: - try: - # Resolve icon path (relative to bundle directory or absolute) - if combobox.directory and not op.isabs(member_icon): - icon_path = op.join(combobox.directory, member_icon) - else: - icon_path = member_icon - - if op.exists(icon_path): - button_icon = ribbon.ButtonIcons(icon_path) - member_obj.Image = button_icon.small_bitmap - else: - mlogger.warning("Icon file not found: %s", icon_path) - except Exception as member_icon_err: - mlogger.debug( - "Error setting member icon: %s", member_icon_err - ) - - # Set member group if available - if member_group and hasattr(member_obj, "GroupName"): - try: - member_obj.GroupName = member_group - except Exception as group_err: - mlogger.debug("Error setting member group: %s", group_err) - - # Set member tooltip if available - if member_tooltip and hasattr(member_obj, "ToolTip"): - try: - member_obj.ToolTip = member_tooltip - except Exception as tooltip_err: - mlogger.debug( - "Error setting member tooltip: %s", tooltip_err - ) - - # Set member extended tooltip if available - if member_tooltip_ext and hasattr(member_obj, "LongDescription"): - try: - member_obj.LongDescription = member_tooltip_ext - except Exception as tooltip_ext_err: - mlogger.debug( - "Error setting member extended tooltip: %s", - tooltip_ext_err, - ) - - # Set member tooltip image if available - if member_tooltip_image and hasattr(member_obj, "ToolTipImage"): - try: - # Resolve tooltip image path (relative to bundle directory or absolute) - if combobox.directory and not op.isabs( - member_tooltip_image - ): - tooltip_image_path = op.join( - combobox.directory, member_tooltip_image - ) - else: - tooltip_image_path = member_tooltip_image - - if op.exists(tooltip_image_path): - from pyrevit.coreutils.ribbon import load_bitmapimage - - tooltip_bitmap = load_bitmapimage(tooltip_image_path) - member_obj.ToolTipImage = tooltip_bitmap - except Exception as tooltip_image_err: - mlogger.debug( - "Error setting member tooltip image: %s", - tooltip_image_err, - ) - - except Exception as add_err: - mlogger.warning("Error adding member: %s", add_err) - - # Set Current to first item - # Note: Setting current will overwrite ItemText with the selected item's text - # This is expected behavior - ComboBox.ItemText shows the current selection - items = combobox_ui.get_items() - if items and len(items) > 0: - try: - combobox_ui.current = items[0] - except Exception as current_err: - mlogger.debug("Error setting current item: %s", current_err) + if ( + not combobox_script_file + and hasattr(combobox, "directory") + and combobox.directory + ): + script_path = op.join(combobox.directory, "script.py") + if op.exists(script_path): + combobox_script_file = script_path + + if combobox_script_file and combobox_unique_name: + current_paths = list(sys.path) + combobox_module_paths = getattr(combobox, "module_paths", []) + for search_path in combobox_module_paths: + if search_path not in current_paths: + sys.path.append(search_path) + + imported_script = imp.load_source( + combobox_unique_name, combobox_script_file + ) + sys.path = current_paths - # Call __selfinit__ on script (SmartButton pattern) - try: - combobox_script_file = getattr(combobox, "script_file", None) - combobox_unique_name = getattr(combobox, "unique_name", None) - - if ( - not combobox_script_file - and hasattr(combobox, "directory") - and combobox.directory - ): - script_path = op.join(combobox.directory, "script.py") - if op.exists(script_path): - combobox_script_file = script_path - - if combobox_script_file and combobox_unique_name: - current_paths = list(sys.path) - combobox_module_paths = getattr(combobox, "module_paths", []) - for search_path in combobox_module_paths: - if search_path not in current_paths: - sys.path.append(search_path) - - imported_script = imp.load_source( - combobox_unique_name, combobox_script_file + if hasattr(imported_script, "__selfinit__"): + res = imported_script.__selfinit__( + combobox, combobox_ui, HOST_APP.uiapp ) - sys.path = current_paths - - if hasattr(imported_script, "__selfinit__"): - res = imported_script.__selfinit__( - combobox, combobox_ui, HOST_APP.uiapp - ) - if res is False: - combobox_ui.deactivate() - except Exception as init_err: - mlogger.exception("Error in __selfinit__: %s", init_err) + if res is False: + combobox_ui.deactivate() + except Exception as init_err: + mlogger.exception("Error in __selfinit__: %s", init_err) - # Ensure visible & enabled - try: - if hasattr(combobox_obj, "Visible"): - combobox_obj.Visible = True - if hasattr(combobox_obj, "Enabled"): - combobox_obj.Enabled = True - except Exception as vis_err: - mlogger.debug("Could not set visibility: %s", vis_err) - - # Activate UI item - try: - combobox_ui.activate() - except Exception as activate_err: - mlogger.debug("Could not activate: %s", activate_err) + # Ensure visible & enabled + try: + if hasattr(combobox_obj, "Visible"): + combobox_obj.Visible = True + if hasattr(combobox_obj, "Enabled"): + combobox_obj.Enabled = True + except Exception as vis_err: + mlogger.debug("Could not set visibility: %s", vis_err) + + # Activate UI item + try: + combobox_ui.activate() + except Exception as activate_err: + mlogger.debug("Could not activate: %s", activate_err) - return combobox_ui + return combobox_ui def _produce_ui_split(ui_maker_params): From 1325ebc1bbdfba0d4c7f36de9f7ac29d0b03e410 Mon Sep 17 00:00:00 2001 From: Deyan Nenov Date: Wed, 10 Dec 2025 17:16:53 +0000 Subject: [PATCH 12/12] Add non-ASCII character sanitization for combobox scripts - Add _sanitize_script_file() function to replace common non-ASCII characters with ASCII equivalents - Call sanitization before loading combobox scripts to prevent SyntaxError - Handles em/en dashes, smart quotes, ellipsis, and non-breaking spaces --- pyrevitlib/pyrevit/loader/uimaker.py | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/pyrevitlib/pyrevit/loader/uimaker.py b/pyrevitlib/pyrevit/loader/uimaker.py index 402293197..e6f8d8c23 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -25,6 +25,56 @@ CONFIG_SCRIPT_TITLE_POSTFIX = "\u25CF" +def _sanitize_script_file(script_file_path): + """Sanitize non-ASCII characters in a Python script file. + + Reads the file, replaces common non-ASCII characters with ASCII equivalents, + and writes it back. This prevents SyntaxError when loading scripts without + encoding declarations. + + Args: + script_file_path (str): Path to the script file to sanitize. + + Returns: + bool: True if file was sanitized, False if no changes were needed. + """ + try: + # Read file with UTF-8 encoding, fallback to latin-1 if needed + try: + with open(script_file_path, 'r', encoding='utf-8') as f: + content = f.read() + except UnicodeDecodeError: + with open(script_file_path, 'r', encoding='latin-1') as f: + content = f.read() + + original_content = content + + # Replace common non-ASCII characters with ASCII equivalents + # Em dash, en dash -> regular dash + content = content.replace('\u2014', '-') # em dash + content = content.replace('\u2013', '-') # en dash + # Smart quotes -> regular quotes + content = content.replace('\u2018', "'") # left single quotation mark + content = content.replace('\u2019', "'") # right single quotation mark + content = content.replace('\u201C', '"') # left double quotation mark + content = content.replace('\u201D', '"') # right double quotation mark + # Other common non-ASCII characters + content = content.replace('\u2026', '...') # horizontal ellipsis + content = content.replace('\u00A0', ' ') # non-breaking space + + # Only write back if content changed + if content != original_content: + # Write back with UTF-8 encoding + with open(script_file_path, 'w', encoding='utf-8') as f: + f.write(content) + mlogger.debug("Sanitized non-ASCII characters in: %s", script_file_path) + return True + return False + except Exception as sanitize_err: + mlogger.warning("Error sanitizing script file %s: %s", script_file_path, sanitize_err) + return False + + class UIMakerParams: """UI maker parameters. @@ -754,17 +804,36 @@ def _produce_ui_combobox(ui_maker_params): if search_path not in current_paths: sys.path.append(search_path) + # Sanitize non-ASCII characters before loading + _sanitize_script_file(combobox_script_file) + imported_script = imp.load_source( combobox_unique_name, combobox_script_file ) sys.path = current_paths if hasattr(imported_script, "__selfinit__"): + mlogger.warning( + "[ComboBox Script Load] '%s': Calling __selfinit__ function", + combobox_name, + ) res = imported_script.__selfinit__( combobox, combobox_ui, HOST_APP.uiapp ) if res is False: combobox_ui.deactivate() + else: + mlogger.warning( + "[ComboBox Script Load] '%s': Script loaded but __selfinit__ function not found", + combobox_name, + ) + else: + mlogger.warning( + "[ComboBox Script Load] '%s': Skipping script load - script_file=%s, unique_name=%s", + combobox_name, + combobox_script_file, + combobox_unique_name, + ) except Exception as init_err: mlogger.exception("Error in __selfinit__: %s", init_err)