mindm.mindmanager_mac_as module

MacOS-specific implementation of the Mindmanager interface. This class implementation uses the native AppleScript approach.

This module provides MacOS platform-specific implementation for interacting with MindManager application, including functionality for manipulating topics, properties, relationships, and document structure.

class mindm.mindmanager_mac_as.Mindmanager(charttype)

Bases: object

MACOS_LIBRARY_FOLDER = 'C:\\Users\\runneradmin\\Library\\Application Support\\Mindjet\\MindManager\\XX\\English\\Library'
MACOS_MERGE_ALL_WINDOWS = False
__init__(charttype)
add_document(max_topic_level)

Opens the correct template based on charttype and subtopic counts.

add_relationship(guid1, guid2, label='')

Create a new relationship from topic guid1 to topic guid2.

add_subtopic_to_topic(topic_id, topic_text)

Create a new subtopic under topic_id with topic_text. Return the new subtopic’s ID or None on failure.

add_tag_to_topic(topic_id, tag_text, topic_guid)

Unimplemented in original snippet. Just a placeholder returning nothing.

Unimplemented in snippet. Placeholder.

create_map_icons(map_icons)

Unimplemented in snippet. Return nothing or store as needed.

create_tags(tags: list[str], DUPLICATED_TAG: str)

Unimplemented in snippet. Return nothing or store as needed.

document_exists()

Returns True if there’s at least one open document in MindManager.

finalize(max_topic_level)

Balance the map, activate MindManager, optionally merge windows, and clean up.

get_active_document_object()

Return ‘document 1’ if a document is open, else None.

get_central_topic() MindmapTopic

Return the central topic’s ID or None if not found.

get_guid_from_topic(topic_id) str
get_icons_from_topic(topic_id) list[MindmapIcon]
get_image_from_topic(topic_id) MindmapImage
get_level_from_topic(topic_id)
get_library_folder()

Return a list of MindmapLink objects or an empty list if not implemented/found.

get_mindmanager_object()

In pure AppleScript usage, there’s no persistent object to return.

get_mindmaptopic_from_topic(topic_id) MindmapTopic

Returns a MindmapTopic with guid, text, rtf and level, all retrieved via a single AppleScript call.

get_mindmaptopic_from_topic_content(topic_id) MindmapTopic

Returns a MindmapTopic with guid, text, rtf, level, and notes, all retrieved via a single AppleScript call.

get_mindmaptopic_from_topic_full(topic_id) MindmapTopic

Returns a MindmapTopic with guid, text, rtf, level, notes, and references, all via one AppleScript call. (links/icons/tags/image remain unimplemented.)

get_notes_from_topic(topic_id) MindmapNotes

Return MindmapNotes or None.

get_parent_from_topic(topic_id)

Return the parent’s ID or None if there is no parent or the topic doesn’t exist.

get_references_from_topic(topic_id) list[MindmapReference]

Return a list of MindmapReference objects for the given topic.

get_selection()

Return a list of topic IDs in the current selection.

get_subtopics_from_topic(topic_id)

Return a list of subtopic IDs.

get_tags_from_topic(topic_id) list[MindmapTag]
get_text_from_topic(topic_id)
get_title_from_topic(topic_id)
get_topic_by_id(topic_id)

Return topic_id if it exists in the document; else None.

get_version()
get_whole_mindmap_tree() MindmapTopic

Gathers the entire MindManager map (starting from the central topic) in a single AppleScript call, returning a single MindmapTopic object whose children recursively contain the entire tree.

set_document_background_image(path)
set_text_to_topic(topic_id, topic_text)

Set the topic’s text (equivalent to topic.name.set).

set_title_to_topic(topic_id, topic_rtf)

Set the topic’s title (equivalent to topic.title.set).

set_topic_from_mindmap_topic(topic_id, mindmap_topic, map_icons)

Updates the topic’s text, RTF title, and notes from mindmap_topic via a single AppleScript call. Returns (refreshed_topic_id, original_topic_id).

mindm.mindmanager_mac_as.run_applescript(script: str) str

Runs AppleScript code via osascript -e and returns stdout as a string. Raises CalledProcessError if the script fails.

Source code for mindmanager_mac_as.py
  1"""
  2MacOS-specific implementation of the Mindmanager interface.
  3This class implementation uses the native AppleScript approach.
  4
  5This module provides MacOS platform-specific implementation for interacting
  6with MindManager application, including functionality for manipulating topics,
  7properties, relationships, and document structure.
  8"""
  9
 10import os
 11import json
 12import subprocess
 13
 14from mindmap.mindmap import (
 15    MindmapLink,
 16    MindmapImage,
 17    MindmapNotes,
 18    MindmapIcon,
 19    MindmapTag,
 20    MindmapReference,
 21    MindmapTopic,
 22)
 23
 24def run_applescript(script: str) -> str:
 25    """
 26    Runs AppleScript code via `osascript -e` and returns stdout as a string.
 27    Raises CalledProcessError if the script fails.
 28    """
 29    try:
 30        result = subprocess.run(
 31            ["osascript", "-e", script],
 32            capture_output=True,
 33            text=True,
 34            check=True
 35        )
 36        return result.stdout.strip()
 37    except subprocess.CalledProcessError as e:
 38        print(f"AppleScript error: {e.stderr}")
 39        return ""
 40
 41class Mindmanager:
 42
 43    MACOS_MERGE_ALL_WINDOWS = False
 44    MACOS_LIBRARY_FOLDER = os.path.join(
 45        os.path.expanduser("~"), 
 46        "Library", 
 47        "Application Support", 
 48        "Mindjet", 
 49        "MindManager", 
 50        "XX", 
 51        "English", 
 52        "Library"
 53    )
 54
 55    def __init__(self, charttype):
 56        self._charttype = charttype
 57
 58        # Get version from MindManager
 59        script = 'tell application "MindManager" to return version'
 60        version_str = run_applescript(script)
 61        if version_str:
 62            self._version = version_str.split('.')[0]
 63        else:
 64            self._version = "0"
 65
 66        # Prepare library folder
 67        self._library_folder = self.MACOS_LIBRARY_FOLDER.replace("XX", self._version)
 68
 69        # Build template paths (as normal POSIX file paths)
 70        self._orgchart_template = os.path.join(
 71            self._library_folder, 
 72            "Templates", 
 73            "Blank Templates", 
 74            "Org-Chart Map.mmat"
 75        )
 76        self._radial_template = os.path.join(
 77            self._library_folder, 
 78            "Templates", 
 79            "Blank Templates", 
 80            "Radial Map.mmat"
 81        )
 82
 83    def get_whole_mindmap_tree(self) -> 'MindmapTopic':
 84        """
 85        Gathers the entire MindManager map (starting from the central topic)
 86        in a single AppleScript call, returning a single MindmapTopic object
 87        whose children recursively contain the entire tree.
 88        """
 89        if not self.document_exists():
 90            return None
 91
 92        script = r'''
 93tell application "MindManager"
 94    if (count of documents) is 0 then
 95        return ""
 96    end if
 97    
 98    set rootTopic to central topic of document 1
 99    return my buildTree(rootTopic)
100end tell
101
102on buildTree(aTopic)
103    set tID to id of aTopic
104    set tName to name of aTopic
105
106    tell application "MindManager"
107        tell aTopic
108            set tLevel to level
109        end tell
110    end tell
111    
112	set tNotes to ""
113	tell application "MindManager"
114		try
115			set tNotesObj to notes of aTopic
116			set tNotes to tNotesObj as rich text
117		on error
118			set tNotes to ""
119		end try
120	end tell
121    
122    set tID to my escapeJSON(tID)
123    set tName to my escapeJSON(tName)
124    set tNotes to my escapeJSON(tNotes)
125    
126    set childJSONs to {}
127    
128    try
129        tell application "MindManager"
130            tell aTopic
131                set childList to {}
132                try
133                    set childList to every topic of aTopic
134                on error
135                    try
136                        set childList to every subtopic of aTopic
137                    on error
138                        try
139                            set childList to topics of aTopic
140                        on error
141                            set childList to {}
142                        end try
143                    end try
144                end try
145                
146                repeat with childTopic in childList
147                    set end of childJSONs to my buildTree(childTopic)
148                end repeat
149            end tell
150        end tell
151    on error errMsg
152        -- log "Error getting subtopics: " & errMsg
153    end try
154    
155    set jsonText to "{\"guid\":\"" & tID & "\", \"text\":\"" & tName & "\", \"level\":" & tLevel & ", \"notes\":\"" & tNotes & "\", \"children\":["
156    set jsonText to jsonText & my joinList(childJSONs, ",") & "]}"
157    return jsonText
158end buildTree
159
160on joinList(aList, delim)
161    if length of aList is 0 then
162        return ""
163    end if
164    
165    set {oldTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delim}
166    set outText to aList as text
167    set AppleScript's text item delimiters to oldTID
168    return outText
169end joinList
170
171on escapeJSON(txt)
172    set txt to my replace_chars(txt, "\\", "\\\\")
173    set txt to my replace_chars(txt, "\"", "\\\"")
174    set txt to my replace_chars(txt, return, "\\n")
175    set txt to my replace_chars(txt, linefeed, "\\n")
176    set txt to my replace_chars(txt, tab, "\\t")
177    return txt
178end escapeJSON
179
180on replace_chars(theText, searchString, replacementString)
181    set AppleScript's text item delimiters to searchString
182    set theList to text items of theText
183    set AppleScript's text item delimiters to replacementString
184    set theText to theList as text
185    set AppleScript's text item delimiters to ""
186    return theText
187end replace_chars
188        '''
189
190        json_string = run_applescript(script)
191        if not json_string:
192            return None  # no data returned
193
194        try:
195            tree_dict = json.loads(json_string)
196        except json.JSONDecodeError as e:
197            print(f"Error parsing JSON from AppleScript: {e}")
198            return None
199
200        return self._dict_to_mindmap_topic(tree_dict)
201
202    def _dict_to_mindmap_topic(self, node_dict: dict) -> 'MindmapTopic':
203        """
204        Helper to recursively convert a dict of the form:
205        { "guid": ..., "text": ..., "level": ..., "notes": ..., "children": [ ... ] }
206        into a MindmapTopic object with subtopics.
207        """
208        notes_obj = None
209        if node_dict.get("notes"):
210            notes_obj = MindmapNotes(text=node_dict["notes"])
211
212        topic = MindmapTopic(
213            guid=node_dict.get("guid", ""),
214            text=node_dict.get("text", ""),
215            level=int(node_dict.get("level", 0)),
216            notes=notes_obj,
217        )
218
219        children = node_dict.get("children", [])
220        for child_dict in children:
221            child_topic = self._dict_to_mindmap_topic(child_dict)
222            if child_topic:
223                topic.subtopics.append(child_topic)
224                child_topic.parent = topic
225
226        return topic
227    
228    def get_mindmanager_object(self):
229        """
230        In pure AppleScript usage, there's no persistent object to return.
231        """
232        return None
233
234    def get_active_document_object(self):
235        """
236        Return 'document 1' if a document is open, else None.
237        """
238        return "document 1" if self.document_exists() else None
239
240    def get_library_folder(self):
241        return self._library_folder
242
243    def get_version(self):
244        return self._version
245
246    def set_document_background_image(self, path):
247        pass
248
249    def document_exists(self):
250        """
251        Returns True if there's at least one open document in MindManager.
252        """
253        script = '''
254            tell application "MindManager"
255                if (count of documents) > 0 then
256                    return "true"
257                else
258                    return "false"
259                end if
260            end tell
261        '''
262        result = run_applescript(script)
263        return (result == "true")
264
265    def get_central_topic(self) -> 'MindmapTopic':
266        """
267        Return the central topic's ID or None if not found.
268        """
269        if not self.document_exists():
270            return None
271
272        result = self.get_whole_mindmap_tree()
273        return result if result else None
274
275    def get_mindmaptopic_from_topic(self, topic_id) -> 'MindmapTopic':
276        """
277        Returns a MindmapTopic with guid, text, rtf and level,
278        all retrieved via a single AppleScript call.
279        """
280        if not topic_id:
281            return None
282
283        # Single AppleScript to grab all basic properties at once:
284        script = f'''
285            tell application "MindManager"
286                try
287                    set theTopic to first topic of document 1 whose id is "{topic_id}"
288                    set theGUID to id of theTopic
289                    set theName to name of theTopic
290                    set theTitle to title of theTopic
291                    set theLevel to level of theTopic
292                    return theGUID & "%%" & theName & "%%" & theTitle & "%%" & (theLevel as text)
293                on error
294                    return ""
295                end try
296            end tell
297        '''
298        result = run_applescript(script)
299        if not result:
300            return None  # topic not found or error
301
302        parts = result.split("%%", 3)  # we expect exactly 4 parts
303        if len(parts) < 4:
304            return None
305
306        theGUID, theName, theTitle, theLevelStr = parts
307
308        # Convert level to integer if possible
309        try:
310            theLevel = int(theLevelStr)
311        except ValueError:
312            theLevel = None
313
314        # Clean up the text property so it mimics your old replacements
315        theName = theName.replace('"', '`').replace("'", "`").replace("\r", "").replace("\n", "")
316
317        # Construct and return the MindmapTopic
318        return MindmapTopic(
319            guid=theGUID,
320            text=theName,
321            rtf=theTitle,
322            level=theLevel,
323        )
324
325    def get_mindmaptopic_from_topic_content(self, topic_id) -> 'MindmapTopic':
326        """
327        Returns a MindmapTopic with guid, text, rtf, level, and notes,
328        all retrieved via a single AppleScript call.
329        """
330        if not topic_id:
331            return None
332
333        # Single AppleScript to grab all basic properties at once:
334        script = f'''
335            tell application "MindManager"
336                try
337                    set theTopic to first topic of document 1 whose id is "{topic_id}"
338                    set theGUID to id of theTopic
339                    set theName to name of theTopic
340                    set theTitle to title of theTopic
341                    set theLevel to level of theTopic
342                    set theNotes to notes of theTopic
343                    return theGUID & "%%" & theName & "%%" & theTitle & "%%" & (theLevel as text) & "%%" & theNotes
344                on error
345                    return ""
346                end try
347            end tell
348        '''
349        result = run_applescript(script)
350        if not result:
351            return None  # topic not found or error
352
353        parts = result.split("%%", 4)  # we expect exactly 5 parts
354        if len(parts) < 5:
355            return None
356
357        theGUID, theName, theTitle, theLevelStr, theNotes = parts
358
359        # Convert level to integer if possible
360        try:
361            theLevel = int(theLevelStr)
362        except ValueError:
363            theLevel = None
364
365        # Clean up the text property so it mimics your old replacements
366        theName = theName.replace('"', '`').replace("'", "`").replace("\r", "").replace("\n", "")
367
368        # Build the MindmapNotes object if notes are non-empty
369        notes_obj = MindmapNotes(text=theNotes) if theNotes else None
370
371        # Construct and return the MindmapTopic
372        return MindmapTopic(
373            guid=theGUID,
374            text=theName,
375            rtf=theTitle,
376            level=theLevel,
377            notes=notes_obj,
378        )
379
380    def get_mindmaptopic_from_topic_full(self, topic_id) -> 'MindmapTopic':
381        """
382        Returns a MindmapTopic with guid, text, rtf, level, notes, and references,
383        all via one AppleScript call. (links/icons/tags/image remain unimplemented.)
384        """
385        if not topic_id:
386            return None
387
388        # Single AppleScript to grab all properties + references
389        script = f'''
390            tell application "MindManager"
391                try
392                    set theTopic to first topic of document 1 whose id is "{topic_id}"
393                    set theGUID to id of theTopic
394                    set theName to name of theTopic
395                    set theTitle to title of theTopic
396                    set theLevel to level of theTopic
397                    set theNotes to notes of theTopic
398                    set rels to relationships of theTopic
399                    set referencesString to ""
400                    repeat with r in rels
401                        set sLoc to id of (starting location of r)
402                        set eLoc to id of (ending location of r)
403                        set referencesString to referencesString & sLoc & "||" & eLoc & "||--||"
404                    end repeat
405                    return theGUID & "%%" & theName & "%%" & theTitle & "%%" & (theLevel as text) & "%%" & theNotes & "%%" & referencesString
406                on error
407                    return ""
408                end try
409            end tell
410        '''
411        result = run_applescript(script)
412        if not result:
413            return None
414
415        # We expect 6 parts: guid, name, title, level, notes, referencesString
416        parts = result.split("%%", 5)
417        if len(parts) < 6:
418            return None
419
420        theGUID, theName, theTitle, theLevelStr, theNotes, referencesRaw = parts
421
422        # Convert level to integer if possible
423        try:
424            theLevel = int(theLevelStr)
425        except ValueError:
426            theLevel = None
427
428        # Clean up the text property
429        theName = theName.replace('"', '`').replace("'", "`").replace("\r", "").replace("\n", "")
430
431        # Build the MindmapNotes object if notes are non-empty
432        notes_obj = MindmapNotes(text=theNotes) if theNotes else None
433
434        # Parse references:
435        # referencesRaw might look like "GUID1||GUID2||--||GUID3||GUID4||--||"
436        references = []
437        if referencesRaw:
438            rel_chunks = referencesRaw.split("||--||")
439            for chunk in rel_chunks:
440                chunk = chunk.strip()
441                if not chunk:
442                    continue
443                pair = chunk.split("||")
444                if len(pair) == 2:
445                    sLoc, eLoc = pair
446                    if sLoc == theGUID:  # If it matches the old pattern
447                        references.append(
448                            MindmapReference(direction=1, guid_1=sLoc, guid_2=eLoc)
449                        )
450                    else:
451                        # Or handle direction=2 or other logic if needed
452                        pass
453
454        # For now, links, icons, tags, image remain unimplemented => empty
455        links = []
456        icons = []
457        tags = []
458        image = None
459
460        return MindmapTopic(
461            guid=theGUID,
462            text=theName,
463            rtf=theTitle,
464            level=theLevel,
465            notes=notes_obj,
466            links=links,
467            image=image,
468            icons=icons,
469            tags=tags,
470            references=references,
471        )
472    
473    def get_topic_by_id(self, topic_id):
474        """
475        Return `topic_id` if it exists in the document; else None.
476        """
477        return topic_id
478
479    def get_selection(self):
480        """
481        Return a list of topic IDs in the current selection.
482        """
483        if not self.document_exists():
484            return []
485        script = '''
486            tell application "MindManager"
487                try
488                    set selItems to selection of document 1
489                    set output to ""
490                    repeat with oneItem in selItems
491                        if (class of oneItem as text) is "topic" then
492                            set output to output & (id of oneItem) & linefeed
493                        end if
494                    end repeat
495                    return output
496                on error
497                    return ""
498                end try
499            end tell
500        '''
501        raw = run_applescript(script)
502        return [ln.strip() for ln in raw.splitlines() if ln.strip()]
503
504    def get_level_from_topic(self, topic_id):
505        if not topic_id:
506            return None
507        script = f'''
508            tell application "MindManager"
509                try
510                    set theTopic to first topic of document 1 whose id is "{topic_id}"
511                    return level of theTopic
512                on error
513                    return ""
514                end try
515            end tell
516        '''
517        level_str = run_applescript(script)
518        return int(level_str) if level_str.isdigit() else None
519
520    def get_text_from_topic(self, topic_id):
521        if not topic_id:
522            return ""
523        script = f'''
524            tell application "MindManager"
525                try
526                    set theTopic to first topic of document 1 whose id is "{topic_id}"
527                    return name of theTopic
528                on error
529                    return ""
530                end try
531            end tell
532        '''
533        text = run_applescript(script)
534        # Replace certain characters (as in original code)
535        text = text.replace('"', '`').replace("'", "`").replace("\r", "").replace("\n", "")
536        return text
537
538    def get_title_from_topic(self, topic_id):
539        if not topic_id:
540            return ""
541        script = f'''
542            tell application "MindManager"
543                try
544                    set theTopic to first topic of document 1 whose id is "{topic_id}"
545                    return title of theTopic
546                on error
547                    return ""
548                end try
549            end tell
550        '''
551        return run_applescript(script)
552
553    def get_subtopics_from_topic(self, topic_id):
554        """
555        Return a list of subtopic IDs.
556        """
557        if not topic_id:
558            return []
559        script = f'''
560            tell application "MindManager"
561                try
562                    set theTopic to first topic of document 1 whose id is "{topic_id}"
563                    set subTs to subtopics of theTopic
564                    set output to ""
565                    repeat with t in subTs
566                        set output to output & (id of t) & linefeed
567                    end repeat
568                    return output
569                on error
570                    return ""
571                end try
572            end tell
573        '''
574        raw = run_applescript(script)
575        return [x.strip() for x in raw.splitlines() if x.strip()]
576
577    def get_links_from_topic(self, topic_id) -> list[MindmapLink]:
578        """
579        Return a list of MindmapLink objects or an empty list if not implemented/found.
580        """
581        # Not implemented in the original snippet, so return an empty list.
582        return []
583
584    def get_image_from_topic(self, topic_id) -> MindmapImage:
585        # Return None or implement if needed
586        return None
587
588    def get_icons_from_topic(self, topic_id) -> list[MindmapIcon]:
589        # Return an empty list if no icons or unimplemented
590        return []
591
592    def get_notes_from_topic(self, topic_id) -> MindmapNotes:
593        """
594        Return MindmapNotes or None.
595        """
596        if not topic_id:
597            return None
598        script = f'''
599            tell application "MindManager"
600                try
601                    set theTopic to first topic of document 1 whose id is "{topic_id}"
602                    return notes of theTopic
603                on error
604                    return ""
605                end try
606            end tell
607        '''
608        notes_text = run_applescript(script)
609        if notes_text:
610            return MindmapNotes(text=notes_text)
611        return None
612
613    def get_tags_from_topic(self, topic_id) -> list[MindmapTag]:
614        # Unimplemented. Return an empty list.
615        return []
616
617    def get_references_from_topic(self, topic_id) -> list[MindmapReference]:
618        """
619        Return a list of MindmapReference objects for the given topic.
620        """
621        references = []
622        if not topic_id:
623            return references
624
625        script = f'''
626            tell application "MindManager"
627                try
628                    set theTopic to first topic of document 1 whose id is "{topic_id}"
629                    set rels to relationships of theTopic
630                    if (count of rels) = 0 then
631                        return ""
632                    end if
633                    set outList to ""
634                    repeat with r in rels
635                        set sLoc to id of (starting location of r)
636                        set eLoc to id of (ending location of r)
637                        set outList to outList & sLoc & "||" & eLoc & linefeed
638                    end repeat
639                    return outList
640                on error
641                    return ""
642                end try
643            end tell
644        '''
645        raw = run_applescript(script)
646        for line in raw.splitlines():
647            parts = line.split("||")
648            if len(parts) == 2:
649                sLoc, eLoc = parts
650                if sLoc == topic_id:
651                    references.append(
652                        MindmapReference(
653                            direction=1,
654                            guid_1=sLoc,
655                            guid_2=eLoc
656                        )
657                    )
658        return references
659
660    def get_guid_from_topic(self, topic_id) -> str:
661        return topic_id if topic_id else ""
662
663    def add_subtopic_to_topic(self, topic_id, topic_text):
664        """
665        Create a new subtopic under `topic_id` with `topic_text`.
666        Return the new subtopic's ID or None on failure.
667        """
668        if not topic_id:
669            return None
670        safe_text = topic_text.replace('"', '\\"')
671        script = f'''
672            tell application "MindManager"
673                try
674                    set parentTopic to first topic of document 1 whose id is "{topic_id}"
675                    set newT to make new topic at end of subtopics of parentTopic with properties {{name:"{safe_text}"}}
676                    return id of newT
677                on error
678                    return ""
679                end try
680            end tell
681        '''
682        new_id = run_applescript(script)
683        return new_id if new_id else None
684
685    def get_parent_from_topic(self, topic_id):
686        """
687        Return the parent's ID or None if there is no parent or the topic doesn't exist.
688        """
689        if not topic_id:
690            return None
691        script = f'''
692            tell application "MindManager"
693                try
694                    set theTopic to first topic of document 1 whose id is "{topic_id}"
695                    set p to parent of theTopic
696                    if p is not missing value then
697                        return id of p
698                    else
699                        return ""
700                    end if
701                on error
702                    return ""
703                end try
704            end tell
705        '''
706        result = run_applescript(script)
707        return result if result else None
708
709    def set_text_to_topic(self, topic_id, topic_text):
710        """
711        Set the topic's text (equivalent to topic.name.set).
712        """
713        if not topic_id:
714            return
715        safe_text = topic_text.replace('"', '\\"')
716        script = f'''
717            tell application "MindManager"
718                try
719                    set theTopic to first topic of document 1 whose id is "{topic_id}"
720                    set name of theTopic to "{safe_text}"
721                end try
722            end tell
723        '''
724        run_applescript(script)
725
726    def set_title_to_topic(self, topic_id, topic_rtf):
727        """
728        Set the topic's title (equivalent to topic.title.set).
729        """
730        if not topic_id:
731            return
732        safe_text = topic_rtf.replace('"', '\\"')
733        script = f'''
734            tell application "MindManager"
735                try
736                    set theTopic to first topic of document 1 whose id is "{topic_id}"
737                    set title of theTopic to "{safe_text}"
738                end try
739            end tell
740        '''
741        run_applescript(script)
742
743    def add_tag_to_topic(self, topic_id, tag_text, topic_guid):
744        """
745        Unimplemented in original snippet. Just a placeholder returning nothing.
746        """
747        pass
748
749    def set_topic_from_mindmap_topic(self, topic_id, mindmap_topic, map_icons):
750        """
751        Updates the topic's text, RTF title, and notes from `mindmap_topic` 
752        via a single AppleScript call. 
753        Returns (refreshed_topic_id, original_topic_id).
754        """
755        if not topic_id:
756            return None, None
757        
758        try:
759            script_lines = []
760            script_lines.append('tell application "MindManager"')
761            script_lines.append('    try')
762            script_lines.append(f'        set theTopic to first topic of document 1 whose id is "{topic_id}"')
763            safe_text = (mindmap_topic.text or "").replace('"', '\\"')
764            script_lines.append(f'        set name of theTopic to "{safe_text}"')
765            if mindmap_topic.rtf:
766                safe_rtf = mindmap_topic.rtf.replace('"', '\\"')
767                script_lines.append(f'        set title of theTopic to "{safe_rtf}"')
768            if mindmap_topic.notes:
769                safe_notes = (mindmap_topic.notes.text or "").replace('"', '\\"')
770                script_lines.append(f'        set notes of theTopic to "{safe_notes}"')
771            script_lines.append('        return id of theTopic')
772            script_lines.append('    on error errMsg')
773            script_lines.append('        return ""')
774            script_lines.append('    end try')
775            script_lines.append('end tell')
776            full_script = "\n".join(script_lines)
777            refreshed_id = run_applescript(full_script)
778            if not refreshed_id:
779                return None, None
780            return refreshed_id, topic_id
781
782        except Exception as e:
783            print(f"Error in set_topic_from_mindmap_topic: {e}")
784            return None, None
785
786    def create_map_icons(self, map_icons):
787        """
788        Unimplemented in snippet. Return nothing or store as needed.
789        """
790        pass
791
792    def create_tags(self, tags: list[str], DUPLICATED_TAG: str):
793        """
794        Unimplemented in snippet. Return nothing or store as needed.
795        """
796        pass
797
798    def add_relationship(self, guid1, guid2, label=''):
799        """
800        Create a new relationship from topic guid1 to topic guid2.
801        """
802        if not guid1 or not guid2:
803            print("Error in add_relationship: One or both topic IDs missing.")
804            return
805        script = f'''
806            tell application "MindManager"
807                try
808                    set t1 to first topic of document 1 whose id is "{guid1}"
809                    set t2 to first topic of document 1 whose id is "{guid2}"
810                    if t1 is not missing value and t2 is not missing value then
811                        make new relationship with properties {{starting location:t1, ending location:t2}}
812                    end if
813                on error errMsg
814                    return ""
815                end try
816            end tell
817        '''
818        run_applescript(script)
819
820    def add_topic_link(self, guid1, guid2, label=''):
821        """
822        Unimplemented in snippet. Placeholder.
823        """
824        pass
825
826    def add_document(self, max_topic_level):
827        """
828        Opens the correct template based on charttype and subtopic counts.
829        """
830        if not self.document_exists():
831            cnt_subtopics = 0
832        else:
833            script_count = '''
834                tell application "MindManager"
835                    set c to count of subtopics of central topic of document 1
836                    return c
837                end tell
838            '''
839            res = run_applescript(script_count)
840            try:
841                cnt_subtopics = int(res)
842            except:
843                cnt_subtopics = 0
844
845        if self._charttype == "orgchart":
846            template_alias = self._orgchart_template
847        elif self._charttype == "radial":
848            template_alias = self._radial_template
849        else:
850            # "auto"
851            if max_topic_level > 2 and cnt_subtopics > 4:
852                template_alias = self._orgchart_template
853            else:
854                template_alias = self._radial_template
855
856        safe_path = template_alias.replace('"', '\\"')
857        script_open = f'''
858            tell application "MindManager"
859                open POSIX file "{safe_path}"
860            end tell
861        '''
862        run_applescript(script_open)
863
864    def finalize(self, max_topic_level):
865        """
866        Balance the map, activate MindManager, optionally merge windows, and clean up.
867        """
868        if not self.document_exists():
869            return
870
871        # Balance map
872        script_balance = '''
873            tell application "MindManager"
874                try
875                    balance map of document 1
876                end try
877            end tell
878        '''
879        run_applescript(script_balance)
880
881        # Activate MindManager
882        script_activate = '''
883            tell application "MindManager"
884                activate
885            end tell
886        '''
887        run_applescript(script_activate)
888
889        # Optionally merge all windows
890        if self.MACOS_MERGE_ALL_WINDOWS:
891            self.merge_windows()
892
893        # No persistent object references to clear
894        pass