Attach Export Dialog
-
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! -
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. -
@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.
-
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. -
@m_adam Thank you very much! This is what I was looking for.