Source code for flexicon.code.System.AnnotationDefOperations

#
#   AnnotationDefOperations.py
#
#   Class: AnnotationDefOperations
#          Annotation definition management operations for FieldWorks Language Explorer
#          projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#

# Import FLEx LCM types
from SIL.LCModel import (
    ICmAnnotationDefn,
    ICmAnnotationDefnRepository,
    ICmAnnotationDefnFactory,
    ICmPossibility,
    ICmPossibilityList,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
from System import Guid, DateTime

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_match_key


[docs] class AnnotationDefOperations(BaseOperations): """ This class provides operations for managing annotation type definitions in a FieldWorks project. Annotation definitions specify the types and characteristics of annotations (notes, comments) that can be created in FLEx. They define properties such as name, help text, whether users can create instances, and whether multiple annotations of this type are allowed. This class should be accessed via FLExProject.AnnotationDef property. Usage:: from flexicon import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Get all annotation definitions for anno_def in project.AnnotationDef.GetAll(): name = project.AnnotationDef.GetName(anno_def) anno_type = project.AnnotationDef.GetAnnotationType(anno_def) print(f"{name}: {anno_type}") # Find a specific annotation definition todo_def = project.AnnotationDef.Find("To Do") if todo_def: # Get properties help_text = project.AnnotationDef.GetHelpString(todo_def) user_can_create = project.AnnotationDef.GetUserCanCreate(todo_def) print(f"Help: {help_text}") print(f"User creatable: {user_can_create}") # Create a new annotation definition (type passed as int: the # CmAnnotationType enum is not exposed via pythonnet) custom_def = project.AnnotationDef.Create( "Review Required", 0, "en" ) project.AnnotationDef.SetHelpString(custom_def, "Mark entries that need review", "en") project.AnnotationDef.SetUserCanCreate(custom_def, True) # Get annotation definitions by type for note_def in project.AnnotationDef.FindByType(0): name = project.AnnotationDef.GetName(note_def) print(f"Note type: {name}") project.CloseProject() """ def __init__(self, project): """ Initialize AnnotationDefOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self): """ Get all annotation definitions in the project. Returns: EnumerableWrapper[ICmAnnotationDefn]: Each annotation definition in the project. Example: >>> for anno_def in project.AnnotationDef.GetAll(): ... name = project.AnnotationDef.GetName(anno_def) ... anno_type = project.AnnotationDef.GetAnnotationType(anno_def) ... user_create = project.AnnotationDef.GetUserCanCreate(anno_def) ... print(f"{name} ({anno_type}): user_create={user_create}") To Do (katGeneralNote): user_create=True Question (katGeneralNote): user_create=True Resolved (katGeneralNote): user_create=False Notes: - Returns all annotation definitions regardless of type - Includes both system-defined and custom definitions - Definitions may be organized hierarchically (with sub-types) - Use FindByType() to filter by annotation type - Results are in no particular order See Also: Find, FindByType, GetUserCreatableTypes """ repo = self.project.GetService(ICmAnnotationDefnRepository) for anno_def in repo.AllInstances(): yield anno_def @OperationsMethod def Create(self, name, annotation_type, wsHandle=None, parent=None): """ Create a new annotation definition. Args: name (str): The name of the annotation definition. annotation_type (int): The annotation type code (the CmAnnotationType enum is not exposed via pythonnet, so pass its int value; currently recorded opportunistically). wsHandle: Optional writing system handle. Defaults to analysis WS. parent (ICmAnnotationDefn, optional): Parent definition for creating a sub-type. If None, creates a top-level definition. Returns: ICmAnnotationDefn: The newly created annotation definition. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If name or annotation_type is None. FP_ParameterError: If name is empty or already exists. Example: >>> # Create a top-level annotation definition >>> custom_def = project.AnnotationDef.Create( ... "Review Required", ... 0, ... "en" ... ) >>> print(project.AnnotationDef.GetName(custom_def)) Review Required >>> # Create a sub-type under an existing definition >>> todo_def = project.AnnotationDef.Find("To Do") >>> urgent_def = project.AnnotationDef.Create( ... "Urgent", ... 0, ... "en", ... parent=todo_def ... ) >>> # Set properties for new definition >>> project.AnnotationDef.SetUserCanCreate(custom_def, True) >>> project.AnnotationDef.SetHelpString(custom_def, ... "Marks entries requiring review", "en") Notes: - Annotation type determines how the definition is used - Common types: katGeneralNote, katQuestionNote, katResolvedNote - Name should be unique within the project - Parent creates hierarchical organization of types - New definitions default to user_can_create=False - Use SetUserCanCreate() to make user-creatable - GUID is auto-generated - DateCreated is set automatically See Also: Delete, Find, SetUserCanCreate, SetHelpString """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") self._ValidateParam(annotation_type, "annotation_type") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") # Check if annotation definition with this name already exists if self.Find(name): raise FP_ParameterError(f"Annotation definition '{name}' already exists") wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM(f"Create annotation definition '{name}'"): # Get the factory factory = self.project.project.ServiceLocator.GetService(ICmAnnotationDefnFactory) # Create the annotation definition anno_def = factory.Create() # Add to the annotation definitions list (must be done before setting properties) anno_list = self.project.lp.AnnotationDefsOA if anno_list: if parent: # Add as sub-possibility under parent if hasattr(parent, "SubPossibilitiesOS"): parent.SubPossibilitiesOS.Add(anno_def) else: # Add as top-level possibility anno_list.PossibilitiesOS.Add(anno_def) # Set the name mkstr = TsStringUtils.MakeString(name, wsHandle) anno_def.Name.set_String(wsHandle, mkstr) # Set the annotation type if hasattr(anno_def, "AnnotationType"): anno_def.AnnotationType = int(annotation_type) return anno_def @OperationsMethod def Delete(self, anno_def): """ Delete an annotation definition. Args: anno_def: The ICmAnnotationDefn object to delete. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def is None. FP_ParameterError: If trying to delete a system definition. Example: >>> # Delete a custom annotation definition >>> custom = project.AnnotationDef.Find("Old Custom Type") >>> if custom: ... project.AnnotationDef.Delete(custom) Warning: - DO NOT delete system annotation definitions - Deletion is permanent and cannot be undone - Any annotations using this definition will be affected - Deletes all sub-definitions recursively - Only delete custom definitions you created Notes: - Best practice: only delete custom definitions - Deletion cascades to sub-definitions - Existing annotations may become orphaned - Use with extreme caution on shared projects See Also: Create, Find, GetAll """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") with self._TransactionCM("Delete annotation definition"): # Remove from parent's collection owner = anno_def.Owner if hasattr(owner, "PossibilitiesOS"): if anno_def in owner.PossibilitiesOS: owner.PossibilitiesOS.Remove(anno_def) elif hasattr(owner, "SubPossibilitiesOS"): if anno_def in owner.SubPossibilitiesOS: owner.SubPossibilitiesOS.Remove(anno_def) # Delete the object if hasattr(anno_def, "Delete"): anno_def.Delete() @OperationsMethod def Find(self, name, wsHandle=None): """ Find an annotation definition by name. Args: name (str): The name to search for (case-sensitive). wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: ICmAnnotationDefn or None: The annotation definition if found, None otherwise. Raises: FP_NullParameterError: If name is None. Example: >>> # Find by name >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... print(f"Found: {project.AnnotationDef.GetName(todo_def)}") ... else: ... print("Not found") Found: To Do >>> # Use to check if definition exists >>> if not project.AnnotationDef.Find("Custom Type"): ... # Create it ... custom = project.AnnotationDef.Create( ... "Custom Type", ... 0 ... ) Notes: - Search is case-sensitive - Searches in specified writing system - Returns first match only (names should be unique) - Returns None if not found (doesn't raise exception) - Use Exists() for simple existence check See Also: Exists, GetAll, FindByType """ self._ValidateParam(name, "name") if not name or not name.strip(): return None wsHandle = self.__WSHandle(wsHandle) # Search through all annotation definitions target = normalize_match_key(name, casefold=False) for anno_def in self.GetAll(): def_name = ITsString(anno_def.Name.get_String(wsHandle)).Text if normalize_match_key(def_name, casefold=False) == target: return anno_def return None @OperationsMethod def Exists(self, name, wsHandle=None): """ Check if an annotation definition with the given name exists. Args: name (str): The name to check. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: bool: True if the annotation definition exists, False otherwise. Raises: FP_NullParameterError: If name is None. Example: >>> if project.AnnotationDef.Exists("To Do"): ... print("To Do annotation type exists") To Do annotation type exists >>> if not project.AnnotationDef.Exists("Custom"): ... print("Custom type does not exist") Custom type does not exist Notes: - Convenience method equivalent to Find(name) is not None - Case-sensitive check - Checks in specified writing system See Also: Find, GetAll """ self._ValidateParam(name, "name") return self.Find(name, wsHandle) is not None # --- Property Getters and Setters --- @OperationsMethod def GetName(self, anno_def, wsHandle=None): """ Get the name of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The name, or empty string if not set. Raises: FP_NullParameterError: If anno_def is None. Example: >>> for anno_def in project.AnnotationDef.GetAll(): ... name = project.AnnotationDef.GetName(anno_def) ... print(f"Definition: {name}") Definition: To Do Definition: Question Definition: Resolved See Also: SetName, Find """ self._ValidateParam(anno_def, "anno_def") wsHandle = self.__WSHandle(wsHandle) name = ITsString(anno_def.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetName(self, anno_def, name, wsHandle=None): """ Set the name of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. name (str): The new name. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def or name is None. FP_ParameterError: If name is empty. Example: >>> custom = project.AnnotationDef.Find("Custom") >>> if custom: ... project.AnnotationDef.SetName(custom, "Custom Review", "en") ... print(project.AnnotationDef.GetName(custom)) Custom Review Warning: - Renaming system definitions is not recommended - Only rename custom definitions you created - Changes affect display throughout FLEx See Also: GetName, Create """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") self._ValidateParam(name, "name") if not name or not name.strip(): raise FP_ParameterError("Name cannot be empty") wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM(f"Set annotation definition name '{name}'"): mkstr = TsStringUtils.MakeString(name, wsHandle) anno_def.Name.set_String(wsHandle, mkstr) @OperationsMethod def GetHelpString(self, anno_def, wsHandle=None): """ Get the help string (description) of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The help string, or empty string if not set. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... help_text = project.AnnotationDef.GetHelpString(todo_def) ... print(f"Help: {help_text}") Help: Marks items that need to be done Notes: - Help strings provide user guidance - Displayed as tooltips or help text in FLEx UI - Can be set in multiple writing systems - Stored in the inherited Description field: ICmAnnotationDefn has no HelpString member (live-proven, issue #352) See Also: SetHelpString, GetName """ self._ValidateParam(anno_def, "anno_def") wsHandle = self.__WSHandle(wsHandle) help_str = ITsString(anno_def.Description.get_String(wsHandle)).Text return help_str or "" @OperationsMethod def SetHelpString(self, anno_def, help_string, wsHandle=None): """ Set the help string (description) of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. help_string (str): The help text to set. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def or help_string is None. Example: >>> custom_def = project.AnnotationDef.Create( ... "Review Required", ... 0 ... ) >>> project.AnnotationDef.SetHelpString( ... custom_def, ... "Marks entries that require linguistic review", ... "en" ... ) >>> print(project.AnnotationDef.GetHelpString(custom_def)) Marks entries that require linguistic review Notes: - Empty string is allowed (clears the help string) - Help strings guide users on when to use this type - Can provide different help in different languages - Stored in the inherited Description field (issue #352) See Also: GetHelpString, SetName """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") self._ValidateParam(help_string, "help_string") wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM("Set annotation definition help string"): mkstr = TsStringUtils.MakeString(help_string, wsHandle) anno_def.Description.set_String(wsHandle, mkstr) # --- Type and Instance Information --- @OperationsMethod def GetAnnotationType(self, anno_def): """ Get the annotation type of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. Returns: int: The annotation type code (0 when unset). Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... anno_type = project.AnnotationDef.GetAnnotationType(todo_def) ... print(f"Type code: {anno_type}") Type code: 0 Notes: - Returns the stored type code (0 when unset / unsupported) - The CmAnnotationType enum itself is not exposed via pythonnet, so callers pass and compare plain ints - Annotation type determines usage context See Also: FindByType, Create """ self._ValidateParam(anno_def, "anno_def") if hasattr(anno_def, "AnnotationType"): return anno_def.AnnotationType return 0 @OperationsMethod def GetInstanceOf(self, anno_def): """ Get the class that this annotation definition is an instance of. Args: anno_def: The ICmAnnotationDefn object. Returns: int: The class ID that this definition instantiates. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... class_id = project.AnnotationDef.GetInstanceOf(todo_def) ... print(f"Instance of class: {class_id}") Notes: - Returns the class ID that annotations of this type will be - Used internally by FLEx to create annotation instances - Most annotation definitions are instances of ICmBaseAnnotation See Also: GetAnnotationType, Create """ self._ValidateParam(anno_def, "anno_def") # ICmAnnotationDefn has no InstanceOf member; the class ID lives in # InstanceOfSignature (issue #361). return int(self.__AsDefn(anno_def).InstanceOfSignature) # --- User Control Fields --- @OperationsMethod def GetUserCanCreate(self, anno_def): """ Check if users can create annotations of this type. Args: anno_def: The ICmAnnotationDefn object. Returns: bool: True if users can create this type, False otherwise. Raises: FP_NullParameterError: If anno_def is None. Example: >>> for anno_def in project.AnnotationDef.GetAll(): ... name = project.AnnotationDef.GetName(anno_def) ... can_create = project.AnnotationDef.GetUserCanCreate(anno_def) ... print(f"{name}: user_can_create={can_create}") To Do: user_can_create=True Question: user_can_create=True System Only: user_can_create=False >>> # Filter to user-creatable types only >>> user_types = [d for d in project.AnnotationDef.GetAll() ... if project.AnnotationDef.GetUserCanCreate(d)] Notes: - Controls whether this type appears in user UI - False = system use only, not shown to users - True = users can select this type when creating annotations - Useful for hiding internal or deprecated types See Also: SetUserCanCreate, GetUserCreatableTypes """ self._ValidateParam(anno_def, "anno_def") if hasattr(anno_def, "UserCanCreate"): return bool(anno_def.UserCanCreate) return False @OperationsMethod def SetUserCanCreate(self, anno_def, can_create): """ Set whether users can create annotations of this type. Args: anno_def: The ICmAnnotationDefn object. can_create (bool): True to allow user creation, False to restrict. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def or can_create is None. Example: >>> # Make a custom type user-creatable >>> custom = project.AnnotationDef.Find("Custom Review") >>> if custom: ... project.AnnotationDef.SetUserCanCreate(custom, True) ... print(f"Can create: {project.AnnotationDef.GetUserCanCreate(custom)}") Can create: True >>> # Hide a deprecated type from users >>> old_type = project.AnnotationDef.Find("Old Type") >>> if old_type: ... project.AnnotationDef.SetUserCanCreate(old_type, False) Notes: - True: Type appears in user's annotation type menu - False: Type hidden from users (system use only) - Useful for phasing out old types without deleting - Does not affect existing annotations of this type See Also: GetUserCanCreate, GetUserCreatableTypes """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") self._ValidateParam(can_create, "can_create") # hasattr guard outside the bracket -- its absence is a true no-op. if hasattr(anno_def, "UserCanCreate"): with self._TransactionCM("Set annotation user-can-create flag"): anno_def.UserCanCreate = bool(can_create) @OperationsMethod def GetMultiple(self, anno_def): """ Check if multiple annotations of this type are allowed. Args: anno_def: The ICmAnnotationDefn object. Returns: bool: True if multiple annotations allowed, False if only one. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... allows_multiple = project.AnnotationDef.GetMultiple(todo_def) ... if allows_multiple: ... print("Can have multiple To Do notes per item") ... else: ... print("Only one To Do note allowed per item") Notes: - True: Multiple annotations of this type can exist per object - False: Only one annotation of this type allowed per object - Enforced by FLEx UI when creating annotations - Useful for ensuring unique status markers See Also: SetMultiple, GetUserCanCreate """ self._ValidateParam(anno_def, "anno_def") # The LCM field is Multi; there is no AllowsMultiple member, so a # hasattr probe on that name always fell through to True. return bool(self.__AsDefn(anno_def).Multi) @OperationsMethod def SetMultiple(self, anno_def, allow_multiple): """ Set whether multiple annotations of this type are allowed. Args: anno_def: The ICmAnnotationDefn object. allow_multiple (bool): True to allow multiple, False for one only. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def or allow_multiple is None. Example: >>> # Allow multiple review notes >>> review_def = project.AnnotationDef.Find("Review") >>> if review_def: ... project.AnnotationDef.SetMultiple(review_def, True) >>> # Allow only one status annotation >>> status_def = project.AnnotationDef.Find("Status") >>> if status_def: ... project.AnnotationDef.SetMultiple(status_def, False) Notes: - True: Users can add multiple annotations of this type - False: Only one annotation of this type per object - Useful for status markers that should be unique - Setting affects future annotation creation only See Also: GetMultiple, SetUserCanCreate """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") self._ValidateParam(allow_multiple, "allow_multiple") anno_def = self.__AsDefn(anno_def) with self._TransactionCM("Set annotation allows-multiple flag"): anno_def.Multi = bool(allow_multiple) # --- Prompt and Copy/Paste Settings --- @OperationsMethod def GetPrompt(self, anno_def, wsHandle=None): """ Get the prompt text for an annotation definition. Args: anno_def: The ICmAnnotationDefn object. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The prompt text, or empty string if not set. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... prompt = project.AnnotationDef.GetPrompt(todo_def) ... print(f"Prompt: {prompt}") Prompt: Enter what needs to be done Notes: - Prompt appears when user creates annotation of this type - Guides user on what information to enter - Can be localized in multiple writing systems - Stored in the inherited Description field: ICmAnnotationDefn has no Prompt member (live-proven, issue #352) See Also: SetPrompt, GetHelpString """ self._ValidateParam(anno_def, "anno_def") wsHandle = self.__WSHandle(wsHandle) prompt = ITsString(anno_def.Description.get_String(wsHandle)).Text return prompt or "" @OperationsMethod def SetPrompt(self, anno_def, prompt_text, wsHandle=None): """ Set the prompt text for an annotation definition. Args: anno_def: The ICmAnnotationDefn object. prompt_text (str): The prompt text to set. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If project is not opened with write enabled. FP_NullParameterError: If anno_def or prompt_text is None. Example: >>> custom_def = project.AnnotationDef.Create( ... "Review", ... 0 ... ) >>> project.AnnotationDef.SetPrompt( ... custom_def, ... "Describe what needs to be reviewed", ... "en" ... ) Notes: - Empty string is allowed (clears the prompt) - Prompt helps users understand what to enter - Can provide different prompts in different languages - Stored in the inherited Description field (issue #352) See Also: GetPrompt, SetHelpString """ self._EnsureWriteEnabled() self._ValidateParam(anno_def, "anno_def") self._ValidateParam(prompt_text, "prompt_text") wsHandle = self.__WSHandle(wsHandle) with self._TransactionCM("Set annotation definition prompt"): mkstr = TsStringUtils.MakeString(prompt_text, wsHandle) anno_def.Description.set_String(wsHandle, mkstr) @OperationsMethod def GetCopyCutPasteAllowed(self, anno_def): """ Check if copy/cut/paste operations are allowed for this type. Args: anno_def: The ICmAnnotationDefn object. Returns: bool: True if copy/cut/paste allowed, False otherwise. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... can_copy = project.AnnotationDef.GetCopyCutPasteAllowed(todo_def) ... if can_copy: ... print("Can copy/paste To Do annotations") Notes: - Controls whether users can copy/cut/paste annotations - True: Standard clipboard operations enabled - False: Copy/cut/paste disabled for this type - Useful for restricting certain annotation types See Also: GetUserCanCreate, GetMultiple """ self._ValidateParam(anno_def, "anno_def") # The LCM field is CopyCutPastable; there is no CopyCutPasteAllowed # member, so a hasattr probe on that name always returned True. return bool(self.__AsDefn(anno_def).CopyCutPastable) # --- Query Methods --- @OperationsMethod def FindByType(self, annotation_type): """ Find all annotation definitions of a specific type. Args: annotation_type (int): The annotation type code to search for. Yields: ICmAnnotationDefn: Each annotation definition matching the type. Raises: FP_NullParameterError: If annotation_type is None. Example: >>> # Get all general note types (type code 0) >>> for note_def in project.AnnotationDef.FindByType(0): ... name = project.AnnotationDef.GetName(note_def) ... print(f"Note type: {name}") Note type: To Do Note type: Question Note type: Review Required Notes: - Filters all definitions by annotation type code - Returns empty generator if no matches - Use to find related annotation definition groups - The CmAnnotationType enum is not exposed via pythonnet; pass its int value directly See Also: GetAnnotationType, GetUserCreatableTypes """ self._ValidateParam(annotation_type, "annotation_type") type_value = int(annotation_type) for anno_def in self.GetAll(): if self.GetAnnotationType(anno_def) == type_value: yield anno_def @wrap_enumerable @OperationsMethod def GetUserCreatableTypes(self): """ Get all annotation definitions that users can create. Yields: ICmAnnotationDefn: Each user-creatable annotation definition. Example: >>> # Get types available to users >>> for anno_def in project.AnnotationDef.GetUserCreatableTypes(): ... name = project.AnnotationDef.GetName(anno_def) ... anno_type = project.AnnotationDef.GetAnnotationType(anno_def) ... print(f"Available: {name} (type={anno_type})") Available: To Do (type=0) Available: Question (type=1) Available: Custom Review (type=0) >>> # Count user-creatable types >>> count = len(list(project.AnnotationDef.GetUserCreatableTypes())) >>> print(f"Users can create {count} annotation types") Notes: - Filters to definitions where UserCanCreate is True - These are the types shown in FLEx UI annotation menus - System-only types are excluded - Useful for UI generation and validation See Also: GetUserCanCreate, SetUserCanCreate, FindByType """ for anno_def in self.GetAll(): if self.GetUserCanCreate(anno_def): yield anno_def # --- Metadata Methods --- @OperationsMethod def GetGuid(self, anno_def): """ Get the GUID of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. Returns: System.Guid: The GUID of the annotation definition. Raises: FP_NullParameterError: If anno_def is None. Example: >>> todo_def = project.AnnotationDef.Find("To Do") >>> if todo_def: ... guid = project.AnnotationDef.GetGuid(todo_def) ... print(f"GUID: {guid}") GUID: 12345678-1234-1234-1234-123456789abc Notes: - GUIDs are globally unique identifiers - Persistent across project versions - Use for external references and tracking - Same GUID across different copies of the project See Also: Create, Find """ self._ValidateParam(anno_def, "anno_def") return anno_def.Guid @OperationsMethod def GetDateCreated(self, anno_def): """ Get the creation date of an annotation definition. Args: anno_def: The ICmAnnotationDefn object. Returns: System.DateTime: The creation date/time, or None if not set. Raises: FP_NullParameterError: If anno_def is None. Example: >>> custom = project.AnnotationDef.Find("Custom Review") >>> if custom: ... date = project.AnnotationDef.GetDateCreated(custom) ... if date: ... print(f"Created: {date}") Created: 11/23/2025 10:30:45 AM Notes: - DateCreated is set automatically when definition is created - Returns System.DateTime object - May be None for older definitions - Use for tracking when custom types were added See Also: Create, GetGuid """ self._ValidateParam(anno_def, "anno_def") if hasattr(anno_def, "DateCreated"): return anno_def.DateCreated return None @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True, deep=True): """ Duplicate an annotation definition, creating a new copy with a new GUID. Args: item_or_hvo: The ICmAnnotationDefn object or HVO to duplicate. insert_after (bool): If True (default), insert after the source definition. If False, insert at end of parent's list. deep (bool): If True (default), also duplicate sub-possibilities. If False, only copy simple properties. Returns: ICmAnnotationDefn: The newly created duplicate with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo is None. Example: >>> todo = project.AnnotationDef.Find("To Do") >>> if todo: ... dup = project.AnnotationDef.Duplicate(todo) ... print(f"Duplicate GUID: {project.AnnotationDef.GetGuid(dup)}") Notes: - Factory.Create() automatically generates a new GUID - MultiString properties: Name, Description (surfaced as HelpString and Prompt, which have no backing field) - Simple properties: InstanceOfSignature, AllowsInstanceOf, UserCanCreate, Multi, CopyCutPastable - Sub-possibilities duplicated only if deep=True See Also: Create, Delete, GetGuid """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Get source object source = ( ICmAnnotationDefn(item_or_hvo) if not isinstance(item_or_hvo, int) else ICmAnnotationDefn(self.project.Object(item_or_hvo)) ) # PossibilitiesOS lives on ICmPossibilityList (top-level list owner) and # SubPossibilitiesOS lives on ICmAnnotationDefn (sub-item owner). # Disambiguate by ClassName so pythonnet surfaces the typed accessor. raw_parent = source.Owner if raw_parent.ClassName == "CmPossibilityList": parent = ICmPossibilityList(raw_parent) else: parent = ICmAnnotationDefn(raw_parent) with self._TransactionCM("Duplicate annotation definition"): # Create new annotation definition using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(ICmAnnotationDefnFactory) duplicate = factory.Create() # ADD TO PARENT FIRST if insert_after: # Insert after source if hasattr(parent, "PossibilitiesOS"): source_index = parent.PossibilitiesOS.IndexOf(source) parent.PossibilitiesOS.Insert(source_index + 1, duplicate) elif hasattr(parent, "SubPossibilitiesOS"): source_index = parent.SubPossibilitiesOS.IndexOf(source) parent.SubPossibilitiesOS.Insert(source_index + 1, duplicate) else: # Insert at end if hasattr(parent, "PossibilitiesOS"): parent.PossibilitiesOS.Add(duplicate) elif hasattr(parent, "SubPossibilitiesOS"): parent.SubPossibilitiesOS.Add(duplicate) # Copy MultiString properties (AFTER adding to parent). # HelpString/Prompt have no backing field; both live in the # inherited Description (issue #352). duplicate.Name.CopyAlternatives(source.Name) duplicate.Description.CopyAlternatives(source.Description) self._CopySimpleFlags(source, duplicate) # Deep copy: duplicate sub-possibilities into the NEW duplicate if deep and hasattr(source, "SubPossibilitiesOS") and source.SubPossibilitiesOS.Count > 0: for sub in source.SubPossibilitiesOS: self._DuplicateSubDefInto(sub, duplicate, deep=True) return duplicate def _CopySimpleFlags(self, source, dup): """ Copy the scalar ICmAnnotationDefn fields from source to dup. These are the real LCM names (live reflection, 2026-09-23). The earlier copy blocks probed AnnotationType / InstanceOf / AllowsMultiple, none of which exist, so those flags were never duplicated. """ # Called inside Duplicate's bracket; re-entering joins it. with self._TransactionCM("Copy annotation definition flags"): dup.InstanceOfSignature = source.InstanceOfSignature dup.AllowsInstanceOf = source.AllowsInstanceOf dup.UserCanCreate = source.UserCanCreate dup.Multi = source.Multi dup.CopyCutPastable = source.CopyCutPastable def _DuplicateSubDefInto(self, source_def, parent_dup, deep=True): """Duplicate an annotation sub-definition into the specified parent.""" with self._TransactionCM("Duplicate annotation definition"): factory = self.project.project.ServiceLocator.GetService(ICmAnnotationDefnFactory) dup_def = factory.Create() parent_dup.SubPossibilitiesOS.Add(dup_def) # Copy properties (HelpString/Prompt live in Description; # see Duplicate above). dup_def.Name.CopyAlternatives(source_def.Name) dup_def.Description.CopyAlternatives(source_def.Description) self._CopySimpleFlags(ICmAnnotationDefn(source_def), dup_def) # Recurse into nested sub-definitions. The recursive call re-enters # _TransactionCM, which joins this block under Phase 2 rather than # opening a second undo task, so the whole subtree lands as one # named entry. if deep and hasattr(source_def, "SubPossibilitiesOS") and source_def.SubPossibilitiesOS.Count > 0: for nested_def in source_def.SubPossibilitiesOS: self._DuplicateSubDefInto(nested_def, dup_def, deep=True) # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """Get syncable properties for cross-project synchronization.""" self._ValidateParam(item, "item") anno_def = item if not isinstance(item, int) else self.project.Object(item) wsHandle = self.__WSHandle(None) props = {} props["Name"] = ITsString(anno_def.Name.get_String(wsHandle)).Text or "" # HelpString/Prompt have no backing field; both read the # inherited Description (issue #352). props["HelpString"] = ITsString(anno_def.Description.get_String(wsHandle)).Text or "" props["Prompt"] = ITsString(anno_def.Description.get_String(wsHandle)).Text or "" # ICmAnnotationDefn has InstanceOfSignature / AllowsInstanceOf / Multi, # not InstanceOf / AllowsMultiple; no AnnotationType member (issue #361). props["InstanceOf"] = int(anno_def.InstanceOfSignature) props["AllowsInstanceOf"] = bool(anno_def.AllowsInstanceOf) props["UserCanCreate"] = bool(anno_def.UserCanCreate) props["AllowsMultiple"] = bool(anno_def.Multi) return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """Compare two annotation definitions and return detailed differences.""" if ops1 is None: ops1 = self if ops2 is None: ops2 = self is_different = False differences = {"properties": {}} props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) for key in set(props1.keys()) | set(props2.keys()): val1 = props1.get(key) val2 = props2.get(key) if val1 != val2: is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} return is_different, differences # --- Private Helper Methods --- def __AsDefn(self, anno_def_or_hvo): """ Resolve an HVO or base-typed reference to ICmAnnotationDefn. project.Object() and ICmPossibility-typed views hide the ICmAnnotationDefn fields (Multi, CopyCutPastable, ...), so cast before touching them. """ if isinstance(anno_def_or_hvo, int): anno_def_or_hvo = self.project.Object(anno_def_or_hvo) return ICmAnnotationDefn(anno_def_or_hvo) def __WSHandle(self, wsHandle): """ Get writing system handle, defaulting to analysis WS. Args: wsHandle: Optional writing system handle. Returns: int: The writing system handle. """ if wsHandle is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs)