Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Attach Export Dialog

    Cinema 4D SDK
    python
    2
    5
    767
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • J
      JohnSmith
      last edited by

      Hello everyone. I want to attach in my CommandData plugin interface an export menu like fbx (e.g. using AttachSubDialog). So that it displays up-to-date information with access to export presets. If this is possible then how?
      If I change any settings in my plugin window, will those changes be applied to the global export settings? If so, how can this be avoided?
      Thanks!

      1 Reply Last reply Reply Quote 0
      • M
        m_adam
        last edited by m_adam

        Hi @JohnSmith the easiest solution would be to integrate directly the command from the export menu rather than re-creating everything under the hoods. Of course doing so will means it behave exactly as if the user pressed the FBX entry within the export menu but I guess it's what you are looking for, isn't it? This way you don't have to deal with preset since it's the exporter dialog that already does it but the drawback of that is that if you change a settings, then it will change the global export settings. This is how to do it (including the same logic as we have internally to build the export menu with correct Id and order)

        import c4d
        from typing import Tuple
        
        def HierarchyIterator(obj):
            """A Python Generator yielding all objects recursively of a Cinema 4D BaseList2D tree.
            
            Used to iterate over all registered Plugins (being BaseList2D) to find SceneSaver plugins.
            """
            while obj:
                yield obj
                for opChild in HierarchyIterator(obj.GetDown()):
                    yield opChild
                obj = obj.GetNext()
        
        def IsHidden(plugin: c4d.plugins.BasePlugin) -> bool:
            """Return if a plugin is hidden. """
            return plugin.GetInfo() & c4d.PLUGINFLAG_HIDE != 0
        
        def CreateExportFilterBaseContainer(idOffset: int) -> Tuple[c4d.BaseContainer, int]:
            """Create a BaseContainer with items order as the Export Menu of Cinema 4D.
            
                Args:
                    idOffset (int): The offset apply to the If of the inserted entries in the BaseContainer.
                    
                Return:
                    The feeded BaseContainer
                    The last entryId inserted into the previously returned BaseContainer.
            """
            bc: c4d.BaseContainer = c4d.BaseContainer(1)
            entryId: int = 2 + idOffset
            
            # Iterates all plugins and all SceneSaver to the BaseContainer
            for plugin in HierarchyIterator(c4d.plugins.GetFirstPlugin()):
                if plugin.GetType() == c4d.PLUGINTYPE_SCENESAVER and not IsHidden(plugin):
                    bc.InsData(entryId, plugin.GetName())
                    entryId += 1
            
            # Perform a string sorting over the name of the entries in the BaseContainer
            bc.Sort()
            return bc, entryId
        
        class ExportMenuDialog(c4d.gui.GeDialog):
        
            def __init__(self):
                # Defines an offset where the export menu ID starts
                self.ID_MENU_EXPORT_START = 100000
                # Build ExportFilter BaseContainer and assign the end range of possible ID for the export menu
                self.exportFilterBc, self.ID_MENU_EXPORT_END = CreateExportFilterBaseContainer(self.ID_MENU_EXPORT_START)
        
            def CreateLayout(self):
                self.SetTitle("Export Menu Dialog")
                
                # Flushes all the already existing top bar menu to create our one. 
                # Content will be on the left.
                # Creates a Sub menu begin to inserts new menu entry
                self.MenuFlushAll()
                self.MenuSubBegin("Export")
                
                # Iterate over the BaseContainer to insert entries in the menu
                # The ##entryId will be passed to the Command method when the user click on the entry
                for entryId, entryName in self.exportFilterBc:
                    print(entryId, entryName)
                    self.MenuAddString(entryId, entryName)
                    
                # Finalizes the Sub Menu and the top bar menu
                self.MenuSubEnd()
                self.MenuFinished()
        
                return True
            
            def Command(self, msgId, msgData):
                # When the user click on an entry in the menu it's id is passed as msgId
                # We compare if the received msgId is between the start and the end of our menu ID
                # Then we compute the correct Id (same as used in the Cinema 4D File menu)
                # Finally execute CallCommand with the Id 6000 == the exporter (hardcoded)
                # and the exporterId as the subId 
                if self.ID_MENU_EXPORT_START <= msgId <= self.ID_MENU_EXPORT_END:
                    realExporterId = msgId - self.ID_MENU_EXPORT_START
                    c4d.CallCommand(60000, realExporterId)
                
                return True
        
        def main():
            global myDialog
            myDialog = ExportMenuDialog()
            myDialog.Open(c4d.DLG_TYPE_ASYNC)
        
        
        if __name__ == '__main__':
            main()
        

        The other way around would means recreate all windows manually since it is not possible to call "AttachSubDialog" in this context so this is a lot of work then you would also need to map this windows to the SceneSaver setting, then manually call the SceneSaver for each one (see example in export_alembic_r14). Then you also need to deal with Preset loading, while possible this is again a bit cobblestone, so I would really not advise go this way as it represents a lot of work, that you will need to maintain and therefor go with the first option.

        Cheers,
        Maxime.

        MAXON SDK Specialist

        Development Blog, MAXON Registered Developer

        J 1 Reply Last reply Reply Quote 1
        • J
          JohnSmith @m_adam
          last edited by

          @m_adam Thank you very much for your reply. Unfortunately, this solution does not suit me and I will have to recreate the menu manually. Export is carried out automatically in certain situations and the export settings menu should be in a separate "Settings" window.

          1 Reply Last reply Reply Quote 0
          • M
            m_adam
            last edited by m_adam

            Hi @JohnSmith sorry for the long wait, I was busy with other projects.
            I guess we did not understand each other with the term "menu" as I understood it as a regular Cinema 4D Menu, but you mean more that your tool act as a menu.

            So in order to integrate the setting within your windows you should create a c4d.gui.DescriptionCustomGui and link it to the Exporter Plugin. In cinema 4D what we call a "Description" correspond to the UI of a BaseList2D. A BaseList2D is an object that can be inserted within a tree of item and this is what is used as base for object, tags, material but even plugins like importer and exporter. In a case of exporters there is always only 1 instance of this BaseList2D meaning there is only 1 settings, so any modification done to it is global.

            With that said here an example of a Dialog with a menu to select an exporter from the menu. The exporter settings are then displayed within the dialog and then if you press the export button it export the current document to the selected format.

            import c4d
            
            
            def CreateExportsList(idStart: int) -> list[tuple[int, str, int]]:
                """Create a list with items ordered as the Export Menu of Cinema 4D.
            
                    Args:
                        idStart : The offset applied to the Index of the inserted entries.
            
                    Return:
                        The list fed with tuples constructed like so:
                            int: The entry index in the menu.
                            str: Name of the exporter plugin.
                            int: Plugin Id of the exporter plugin.
                """
                entryId: int = idStart
                outputList = []
            
                # Iterate over all exporters plugins
                for plugin in c4d.plugins.FilterPluginList(c4d.PLUGINTYPE_SCENESAVER, True):
            
                    # If the plugin is Hidden in this case skip it
                    if plugin.GetInfo() & c4d.PLUGINFLAG_HIDE != 0:
                        continue
            
                    outputList.append((entryId, plugin.GetName(), plugin.GetID()))
                    entryId += 1
            
                return outputList
            
            
            class ExportMenuDialog(c4d.gui.GeDialog):
            
                def __init__(self):
                    # Defines an offset where the export menu ID starts
                    self.ID_MENU_EXPORT_START = 100000
                    # Build ExportFilter list
                    self.outputList = CreateExportsList(self.ID_MENU_EXPORT_START)
            
                    # Assign the end range of possible ID for the export menu and other ID for the export Button and the Description CustomGUI
                    self.ID_MENU_EXPORT_END = self.ID_MENU_EXPORT_START + self.outputList[-1][0]
                    self.ID_DESCRIPTION = self.ID_MENU_EXPORT_END + 1
                    self.ID_EXPORT_BUTTON = self.ID_DESCRIPTION + 1
            
                    # Define the default active exporter
                    self._activeExporterMenuId = self.outputList[0][0]
            
                    # Will store the description custom gui, responsible to display a BaseList2D interface.
                    self.descriptionGUI = None
            
                @property
                def activeExporterMenuId(self) -> int:
                    """Return the selected item in the menu."""
                    return self._activeExporterMenuId
            
                @activeExporterMenuId.setter
                def activeExporterMenuId(self, value: int):
                    """ Update the menu when the selected item from the menu is changed.
            
                    Arg:
                        value: The id of the item to be selected in the menu.
                    """
                    if value == self._activeExporterMenuId:
                        return
            
                    self._activeExporterMenuId = value
                    self.UpdateMenuCheckedStatus()
            
                @property
                def activeExporterPluginId(self) -> int:
                    """Return the plugin identifier of the selected item."""
                    plugId: int = 0
                    for entryId, entryName, pluginId in self.outputList:
                        if entryId == self.activeExporterMenuId:
                            plugId = pluginId
                            break
            
                    if plugId == 0:
                        raise ValueError(f"Unable to find plugin for {self.activeExporterMenuId}.")
            
                    return plugId
            
                @property
                def exporterBaseList2D(self) -> c4d.BaseList2D:
                    """
                    Retrieve the plugin instance of the active exporter.
                    """
                    plug = c4d.plugins.FindPlugin(self.activeExporterPluginId, c4d.PLUGINTYPE_SCENESAVER)
                    if plug is None:
                        raise RuntimeError("Failed to retrieve the exporter plugin.")
            
                    data = dict()
                    if not plug.Message(c4d.MSG_RETRIEVEPRIVATEDATA, data):
                        raise RuntimeError("Failed to retrieve private data.")
            
                    # BaseList2D object stored in "imexporter" key hold the settings
                    exportBaseList2D = data.get("imexporter", None)
                    if exportBaseList2D is None:
                        raise RuntimeError("Failed to retrieve BaseList 2D of the exporter.")
            
                    return exportBaseList2D
            
                def UpdateMenuCheckedStatus(self):
                    """ Update the menu to have only one item selected in the menu."""
            
                    # Iterate over the list of exporter to edit their check status
                    for entryId, entryName, pluginId in self.outputList:
                        enableState = entryId == self.activeExporterMenuId
                        self.MenuInitString(entryId, True, enableState)
                        if enableState:
                            self.SetTitle(f"Export Dialog: {entryName}")
            
                def CreateLayout(self) -> bool:
                    # Flushes all the already existing top bar menu to create our one.
                    # Content will be on the left.
                    # Creates a Sub menu begin to insert new menu entry
                    self.MenuFlushAll()
                    self.MenuSubBegin("Export")
            
                    # Iterate over the list of exporters to insert entries in the menu
                    # The ##entryId will be passed to the Command method when the user click on the entry
                    for entryId, entryName, pluginId in self.outputList:
                        self.MenuAddString(entryId, entryName)
            
                    self.UpdateMenuCheckedStatus()
            
                    # Finalizes the Sub Menu and the top bar menu
                    self.MenuSubEnd()
                    self.MenuFinished()
            
                    # Create a CustomGui that will display a Description
                    bc = c4d.BaseContainer()
                    bc[c4d.DESCRIPTION_ALLOWFOLDING] = True
                    bc[c4d.DESCRIPTION_OBJECTSNOTINDOC] = True
                    bc[c4d.DESCRIPTION_NOUNDO] = False
                    bc[c4d.DESCRIPTION_SHOWTITLE] = False
                    bc[c4d.DESCRIPTION_OBJECTSNOTINDOC] = True
                    bc[c4d.DESCRIPTION_NO_TAKE_OVERRIDES] = False
                    self.descriptionGUI = self.AddCustomGui(self.ID_DESCRIPTION,
                                                            c4d.CUSTOMGUI_DESCRIPTION,
                                                            "",
                                                            c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,
                                                            300,
                                                            300,
                                                            bc)
            
                    # Define the BaseList2D to display
                    self.descriptionGUI.SetObject(self.exporterBaseList2D)
            
                    # Add a button to export based on the settings
                    self.AddButton(self.ID_EXPORT_BUTTON, c4d.BFH_CENTER, name="Export")
            
                    return True
            
                def Command(self, msgId: int, msgData: c4d.BaseContainer) -> bool:
                    # When the user click on an entry in the menu it's id is passed as msgId
                    # We compare if the received msgId is between the start and the end of our menu ID
                    # Then we compute the correct Id (same as used in the Cinema 4D File menu)
                    # Finally execute CallCommand with the Id 6000 == the exporter (hardcoded)
                    # and the exporterId as the subId
                    if self.ID_MENU_EXPORT_START <= msgId <= self.ID_MENU_EXPORT_END:
                        self.activeExporterMenuId = msgId
                        self.descriptionGUI.SetObject(self.exporterBaseList2D)
            
                    elif msgId == self.ID_EXPORT_BUTTON:
                        # Retrieves a path to save the exported file
                        filePath = c4d.storage.LoadDialog(title="Save File for the exporter", flags=c4d.FILESELECT_SAVE)
                        if not filePath:
                            return True
            
                        if not c4d.documents.SaveDocument(c4d.documents.GetActiveDocument(),
                                                          filePath,
                                                          c4d.SAVEDOCUMENTFLAGS_DONTADDTORECENTLIST,
                                                          self.activeExporterPluginId):
                            c4d.gui.MessageDialog("Failed to Export the document.")
            
                    return True
            
            
            def main():
                global myDialog
                myDialog = ExportMenuDialog()
                myDialog.Open(c4d.DLG_TYPE_ASYNC)
            
            
            if __name__ == '__main__':
                main()
            

            Cheers,
            Maxime.

            MAXON SDK Specialist

            Development Blog, MAXON Registered Developer

            J 1 Reply Last reply Reply Quote 2
            • J
              JohnSmith @m_adam
              last edited by

              @m_adam Thank you very much! This is what I was looking for.

              1 Reply Last reply Reply Quote 0
              • First post
                Last post