diff --git a/pyrevitlib/pyrevit/coreutils/ribbon.py b/pyrevitlib/pyrevit/coreutils/ribbon.py index cc51a860f..2622c827b 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() @@ -864,13 +897,12 @@ 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() - 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): @@ -885,6 +917,393 @@ def availability_class_name(self): return self._rvtapi_object.AvailabilityClassName +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 + ) + + # 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 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( + 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 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( + 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. + 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: + 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 + + Returns: + None + """ + 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. + + Returns: + None + + Raises: + PyRevitUIError: If adding the separator fails. + """ + 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 _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 = 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.""" + self._set_highlight(AdInternal.Windows.HighlightMode.New) + + def highlight_as_updated(self): + """Highlight as updated item.""" + self._set_highlight(AdInternal.Windows.HighlightMode.Updated) + + class _PyRevitRibbonGroupItem(GenericPyRevitUIContainer): button = GenericPyRevitUIContainer._get_component @@ -900,8 +1319,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 @@ -918,14 +1336,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 @@ -935,8 +1355,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 @@ -958,8 +1379,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: @@ -972,8 +1394,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() @@ -984,32 +1407,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) @@ -1021,25 +1453,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) @@ -1050,9 +1491,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) @@ -1063,22 +1507,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) @@ -1087,20 +1529,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) @@ -1110,15 +1561,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: @@ -1155,13 +1605,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: @@ -1183,18 +1633,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 @@ -1231,8 +1678,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, @@ -1242,8 +1688,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 = [] @@ -1253,32 +1698,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 @@ -1302,14 +1750,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) @@ -1323,8 +1779,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) @@ -1337,31 +1796,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) @@ -1370,16 +1830,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) @@ -1392,11 +1860,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) @@ -1404,87 +1872,145 @@ 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_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)) + 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. + """ + if self.contains(item_name): + 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) + + 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, + 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): @@ -1506,13 +2032,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() @@ -1531,8 +2059,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 @@ -1547,23 +2076,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): @@ -1590,14 +2120,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 @@ -1605,19 +2133,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() @@ -1629,29 +2157,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 @@ -1673,8 +2198,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 @@ -1683,8 +2210,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 @@ -1692,17 +2218,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/__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..774dee9b4 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,16 +309,54 @@ 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 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): """Split push button group.""" + type_id = exts.SPLITPUSH_BUTTON_POSTFIX class SplitButtonGroup(GenericUICommandGroup): """Split button group.""" + type_id = exts.SPLIT_BUTTON_POSTFIX @@ -313,15 +365,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: @@ -335,6 +387,7 @@ def has_commands(self): class StackButtonGroup(GenericStack): """Stack buttons group.""" + type_id = exts.STACK_BUTTON_POSTFIX @@ -343,32 +396,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: @@ -392,13 +449,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] @@ -411,6 +470,7 @@ def has_commands(self): class Extension(GenericUIContainer): """UI Tools extension.""" + type_id = exts.ExtensionTypes.UI_EXTENSION.POSTFIX allowed_sub_cmps = [Tab] @@ -427,35 +487,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.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() @@ -478,12 +542,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())) @@ -497,13 +563,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): @@ -518,8 +586,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 @@ -543,6 +612,7 @@ def get_checks(self): class LibraryExtension(GenericComponent): """Library extension.""" + type_id = exts.ExtensionTypes.LIB_EXTENSION.POSTFIX def __init__(self, cmp_path=None): @@ -554,8 +624,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 6a2a76ce4..e6f8d8c23 100644 --- a/pyrevitlib/pyrevit/loader/uimaker.py +++ b/pyrevitlib/pyrevit/loader/uimaker.py @@ -1,15 +1,19 @@ +# -*- coding: utf-8 -*- """UI maker.""" import sys import imp +import os.path as op from pyrevit import HOST_APP, EXEC_PARAMS, PyRevitException 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 +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 @@ -18,7 +22,57 @@ mlogger = get_logger(__name__) -CONFIG_SCRIPT_TITLE_POSTFIX = u'\u25CF' +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: @@ -31,6 +85,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 @@ -40,49 +95,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 @@ -94,14 +151,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 @@ -143,12 +200,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 @@ -163,11 +220,30 @@ 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.debug( + "SLIDEOUT: Panel has %d items before adding slideout", + len(existing_items), + ) + for idx, item in enumerate(existing_items): + mlogger.debug( + "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.debug("SLIDEOUT: Adding slide out to: %s", parent_ui_item) try: parent_ui_item.add_slideout() + mlogger.debug("SLIDEOUT: Slideout added successfully") except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) + mlogger.error("UI error: %s", err.msg) return None @@ -189,7 +265,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, @@ -197,42 +273,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: @@ -243,15 +327,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 @@ -259,23 +344,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) @@ -300,7 +385,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 @@ -321,29 +406,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 @@ -364,7 +445,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, @@ -372,20 +453,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 @@ -398,20 +479,381 @@ 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 _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 + 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 + + # 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.exception("Error calling create_combobox: %s", create_err) + 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 + + 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, 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 + # 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 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 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 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) + + # 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) + + # 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) + + # 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 + def _produce_ui_split(ui_maker_params): """Produce a split button. @@ -422,18 +864,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 @@ -446,18 +888,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 @@ -478,47 +920,51 @@ 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) for component in stack_cmp: if hasattr(component, 'highlight_type') and component.highlight_type: # Get the UI item for this component ui_item = parent_ui_panel.button(component.name) if ui_item: _set_highlights(component, ui_item) - mlogger.debug('Set highlights on stack: %s', stack_cmp.name) + mlogger.debug("Set highlights on 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): @@ -535,20 +981,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) @@ -556,7 +1004,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 @@ -575,7 +1023,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) @@ -597,7 +1045,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 @@ -610,10 +1058,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) @@ -622,10 +1070,14 @@ def _produce_ui_tab(ui_maker_params): return tab_ui except PyRevitException as err: - mlogger.error('UI error: %s', err.msg) - return None + # 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) + mlogger.debug("Tab does not have any commands. Skipping: %s", tab.name) return None @@ -634,6 +1086,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, @@ -645,7 +1098,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): @@ -653,35 +1106,61 @@ def _recursively_produce_ui_items(ui_maker_params): for sub_cmp in ui_maker_params.component: ui_item = None try: - mlogger.debug('Calling create func %s for: %s', - _component_creation_dict[sub_cmp.type_id], - sub_cmp) + # 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 Exception: + parent_name = "unknown" + mlogger.debug( + "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.debug('Can not find create function for: %s', 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, ) - - mlogger.debug('UI item created by create func is: %s', ui_item) - - if ui_item \ - and not isinstance(ui_item, components.GenericStack) \ - and sub_cmp.is_container: + 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) + # 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, - 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: @@ -701,10 +1180,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): @@ -718,13 +1198,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) @@ -739,10 +1219,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: - 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):