MoleditPy Plugin Development Manual (Version 3.0)

Welcome to the Version 3.0 of the MoleditPy Plugin API. This version introduces a fully decoupled, namespaced architecture designed for high stability, clean memory management, and long-term maintainability.

[!IMPORTANT] API PHILOSOPHY: In Version 3.0, it is strongly recommended that you avoid accessing the MainWindow directly via monkey-patching or unverified attributes. Instead, you should use the stable methods provided by the PluginContext.

While we aim to maintain internal stability as much as possible, the application’s core structure (e.g., manager names, attribute paths) may change significantly between major versions. By using the PluginContext, your plugin is protected by a stable abstraction layer that ensures long-term compatibility even when the core application is refactored.


1. Getting Started

A MoleditPy plugin is a Python script (or folder package) that defines metadata and an initialize(context) entry point.

1.1 Plugin Metadata

Define these at the top of your script. They are used for the UI and the internal registry.

Variable Description
PLUGIN_NAME (Required) The human-readable name of the plugin.
PLUGIN_VERSION Version string (e.g., "1.0.2" or "2026.03.31").
PLUGIN_AUTHOR Name of the developer.
PLUGIN_DESCRIPTION A short summary shown in the Plugin Manager.
PLUGIN_CATEGORY Optional category (e.g., "Analysis", "Visualization").

1.2 Folder-based Plugins (Packages)

For complex plugins, use a folder structure. MoleditPy will treat the folder as a single plugin if it contains an __init__.py.

Structure:

plugins/
  └── HighValuePlugin/      <-- Folder Name
       ├── __init__.py      <-- Entry point & Metadata (REQUIRED)
       ├── logic.py         <-- Sub-module
       └── assets/          <-- Icons or data

In your __init__.py, use relative imports:

from .logic import process_data

PLUGIN_NAME = "High Value Tool"
PLUGIN_VERSION = "1.0"

def initialize(context):
    context.add_menu_action("Tools/Process", lambda: process_data(context))

2. Core API: The PluginContext Reference

The context object passed to initialize(context) is your safe proxy to the application’s core logic.

PLUGIN_NAME = "System Diagnostic Tool"
PLUGIN_VERSION = "3.0.1"
PLUGIN_AUTHOR = "Research Team"
PLUGIN_DESCRIPTION = "Analyzes molecular symmetry and logs diagnostics."

def initialize(context):
    """
    Entry point called by the PluginManager during application startup.
    :param context: PluginContext - Your interface to the application.
    """
    # Add a menu item
    context.add_menu_action("Tools/Run Diagnostic", lambda: run_diag(context))

    # Add a toolbar button
    context.add_toolbar_action(
        callback=lambda: run_diag(context),
        text="Diag",
        tooltip="Run System Diagnostics"
    )

2.1 UI & Feedback

The following methods allow your plugin to communicate with the user via the main interface.

show_status_message(message, timeout=3000)

Display a temporary message in the application’s bottom status bar.

  • message (str): The text to display.
  • timeout (int): Duration in milliseconds before the message disappears. Default is 3000ms.

add_menu_action(path, callback, text=None, icon=None, shortcut=None)

Register a custom item in the main menu. MoleditPy will automatically create any sub-menus defined in the path.

  • path (str): The full menu path.
    • Use File/My Action to add to existing menus.
    • Use MyPlugin/Action to create a new top-level menu.
  • callback (Callable): Function to execute when the action is triggered.
  • text (str, optional): The label of the menu item. Defaults to the last part of path.
  • icon (str, optional): Path to an image file or a standard icon name.
  • shortcut (str, optional): Keyboard shortcut (e.g., "Ctrl+Shift+X").

register_menu_action(path, text_or_callback, callback=None, icon=None, shortcut=None)

Backward-compatible alias for add_menu_action. Supports two calling styles:

  • New style (preferred): register_menu_action(path, callback) — equivalent to add_menu_action.
  • Old style (V2 compat): register_menu_action(path, text, callback) — text and callback positions are swapped.

[!NOTE] New plugins should use add_menu_action instead of register_menu_action.

add_plugin_menu(path, callback, text=None, icon=None, shortcut=None)

Register an action nested inside the Plugins menu. This is the preferred way to keep the main menu bar clean if your plugin has many tools.

  • path (str): The sub-path within the Plugin menu (e.g., "Utils/My Tool").
  • callback (Callable): Function to execute.
  • text (str, optional): Label for the action.
  • icon (str, optional): Icon path.
  • shortcut (str, optional): Keyboard shortcut.

add_toolbar_action(callback, text, icon=None, tooltip=None)

Add a button to the dedicated Plugin Toolbar.

  • callback (Callable): Function to execute when clicked.
  • text (str): Label for the button.
  • icon (str, optional): Icon path.
  • tooltip (str, optional): Hover text.

add_analysis_tool(label, callback)

Register a tool in the top-level Analysis menu. This is the preferred location for non-modifying data processing tools.

  • label (str): Text to display in the menu.
  • callback (Callable): Function to execute.

add_export_action(label, callback)

Register an action in the Export menu. Use this for custom file formats or data summaries.

  • label (str): Text to display (e.g., "Export as MyFormat...").
  • callback (Callable): Function to execute.

2.2 Files & Interoperability

These methods allow your plugin to handle external files and drag-and-drop events.

register_file_opener(extension, callback, priority=0)

Register a handler for opening a specific file type. Used for the File > Import menu and Command Line startup.

  • extension (str): File extension including the dot (e.g., ".xyz", ".cub").
  • callback (Callable[[str], None]): Function that receives the absolute file path and handles the loading logic.
  • priority (int): Handlers with higher priority are checked first. Use negative values (e.g., -1) to register a fallback handler.

register_drop_handler(callback, priority=0)

Register a handler for files dropped onto the main 2D/3D editor window.

  • callback (Callable[[str], bool]): Function that receives the dropped file path. Must return True if it successfully handled the file, False otherwise.
  • priority (int): Handlers with higher priority are checked first.

2.3 Molecular State & Undo

Methods for interacting with the active molecule and managing the undo stack.

current_molecule (Property)

Get or set the active RDKit molecule object.

  • Getter: Returns the rdkit.Chem.Mol currently loaded in the editor.
  • Setter: Replaces the active molecule. Automatically triggers 2D/3D redrawing of the scene.

[!NOTE] current_mol is an alias for current_molecule — both are available.

get_selected_atom_indices()

Returns the RDKit indices of atoms currently selected by the user in either the 2D canvas or the 3D viewer.

push_undo_checkpoint()

Snapshots the current application state and adds it to the Undo history.

[!IMPORTANT] Usage Timing: You should call this method AFTER you have finished modifying the molecule. The system only pushes a new state if it detects a difference from the previous one.


2.4 Lifecycle & Project Data

Manage how your plugin interacts with the application’s overall lifecycle and the .pmeprj project format.

register_save_handler(callback)

Register a callback to save custom state into the application’s project file.

  • callback (Callable[[], dict]): Must return a dictionary of JSON-serializable data.

register_load_handler(callback)

Register a callback to restore custom state from the project file.

  • callback (Callable[[dict], None]): Receives the dictionary previously saved by your save handler.

register_document_reset_handler(callback)

Register a callback to be invoked when a new document is created (File > New). Use this to reset your plugin’s internal state when the user clears all data.

  • callback (Callable[[], None]): Function with no arguments that resets plugin state.
  • Note: This handler is called AFTER the main application has cleared all molecular data but BEFORE the “Cleared” message.

2.5 3D Visualization & Engine

These methods and properties allow your plugin to extend the core rendering and computational capabilities of MoleditPy.

refresh_3d_view()

Force the 3D window to redraw. Use this after performing minor visual changes (like color overrides) or manual coordinate updates.

draw_molecule_3d(mol)

Directly trigger a full redraw of the 3D scene using a specific RDKit molecule. This is more intensive than refresh_3d_view() as it rebuilds all 3D actors.

  • mol (rdkit.Chem.Mol): The molecule to render.

reset_3d_camera()

Zoom in and re-center the 3D viewport to perfectly fit the current molecule.

get_3d_controller()

Returns a Plugin3DController instance (see Section 3). Use this for high-level visual overrides (atom/bond colors).

register_3d_style(style_name, callback)

Register a fully custom 3D visualization mode. This allows you to completely bypass the standard engine’s rendering for specific research needs.

  • style_name (str): Unique name for the style (e.g., "vdw_surface").
  • callback (Callable[[MainWindow, rdkit.Chem.Mol], None]): Function responsible for the entire drawing process. Access the PyVista plotter via mw.plotter.

register_optimization_method(method_name, callback)

Add a custom geometry optimizer to the Compute > Optimize Geometry menu.

  • method_name (str): Name as it appears in the menu.
  • callback (Callable[[rdkit.Chem.Mol], bool]): Function that receives the RDKit molecule. It should modify the coordinates in-place and return True on success.

plotter (Property)

Direct access to the pyvista.Plotter instance. Use for adding custom actors, text, or shapes to the 3D scene (e.g., context.plotter.add_mesh(...)).

scene (Property)

Direct access to the 2D MoleculeScene.

get_main_window()

Returns the raw MainWindow instance. Use with caution — prefer specific context methods when available.


2.7 Settings & Persistence

Plugins can store persistent settings that are saved in the global application configuration.

get_setting(key, default=None)

Retrieve a plugin-specific setting. The key is automatically namespaced by your plugin name.

  • key (str): The setting name.
  • default (Any): Value to return if the setting is not found.

set_setting(key, value)

Save a plugin-specific setting. These are saved to the user’s disk when the application closes.

  • key (str): The setting name.
  • value (Any): The value to store (must be JSON-serializable).

[!CAUTION] Persistence Limit: Settings saved via set_setting reside in the application’s global settings.json. If the user triggers “Reset All Settings” via the main menu, these settings will be REMOVED.

Isolated Storage (Companion JSON)

If your plugin needs to persist data that must survive an application-wide reset, or if you have complex data structures, use a separate JSON file in your plugin’s directory.

Implementation Pattern:

import os, json

def get_config_path():
    # Use __file__ to locate the plugin's own folder and script name
    script_path = os.path.abspath(__file__)
    plugin_dir = os.path.dirname(script_path)
    base_name = os.path.splitext(os.path.basename(script_path))[0]
    
    # Use script_name.json to avoid overlaps
    return os.path.join(plugin_dir, f"{base_name}.json")

def load_config():
    path = get_config_path()
    if os.path.exists(path):
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"my_prop": 42} # Default

def save_config(data):
    with open(get_config_path(), "w", encoding="utf-8") as f:
        json.dump(data, f, indent=4)

2.6 Window & State Management (Namespaced)

In V2, plugins often attached windows directly to mw. In V3, use the Namespaced Registry. This keeps your windows alive in memory and prevents ID collisions with other plugins.

Method Description
register_window(id, window) Stores a Qt window/dialog. The ID is automatically prefixed with your PLUGIN_NAME.
get_window(id) Retrieves your registered window by ID. Returns None if not found or if the window was deleted.

3. Visual Overrides: Plugin3DController

Obtain a controller via ctrl = context.get_3d_controller(). These overrides are temporary visual changes that do not modify the actual RDKit molecule coordinates or properties.

  • set_atom_color(atom_index: int, color_hex: str): Override the color of a specific atom (e.g., "#FF0000" for red).
  • set_bond_color(bond_index: int, color_hex: str): Override the color of a specific RDKit bond by its index.
  • set_bond_color_by_atoms(idx1: int, idx2: int, color_hex: str): Helper to find and color the bond connecting two atom indices.

4. Modern Workflow Examples

4.1 The “Singleton Dialog” Pattern

Always check if your window is already open before creating a new one.

def toggle_viewer(context):
    # 1. Try to get existing window
    win = context.get_window("main_panel")

    if win:
        win.show()
        win.raise_()
        return

    # 2. Create and Register if it's the first time
    win = MyCustomDialog(context.get_main_window())
    context.register_window("main_panel", win)
    win.show()

4.2 The “Modify-Then-Push” Undo Pattern

Correct ordering is essential for the Undo/Redo system to function.

def center_molecule(context):
    mol = context.current_molecule
    if not mol: return

    # 1. Modify the molecule coordinates
    AllChem.ComputeCanonicalTransform(mol)

    # 2. Update the application state
    context.current_molecule = mol

    # 3. Save to History (Must come after setting the mol)
    context.push_undo_checkpoint()

    # 4. Success feedback
    context.show_status_message("Molecule centered.", 2000)

4.3 Highlighting Selection

Color atoms that the user has selected.

def highlight_selection(context):
    indices = context.get_selected_atom_indices()
    ctrl = context.get_3d_controller()

    for idx in indices:
        ctrl.set_atom_color(idx, "#00FF00")  # Highlight green

    context.refresh_3d_view()

5. Migration Guide (V2 to V3)

Legacy Pattern (Direct Access) Modern Pattern (Best Practice)
mw.statusBar().showMessage(m) context.show_status_message(m)
mw.push_undo_state() context.push_undo_checkpoint()
mw.current_mol context.current_molecule
mw.plotter.reset_camera() context.reset_3d_camera()
mw.my_tool = win context.register_window("my_tool", win)
mw.draw_molecule_3d(...) context.refresh_3d_view() or context.draw_molecule_3d(mol)
context.register_menu_action(path, text, cb) context.add_menu_action(path, cb, text)
mw.settings.get(key) context.get_setting(key)

[!NOTE] mw.trigger_conversion() is still accessible via context.get_main_window() and delegates to the internal compute manager. Prefer context.add_menu_action or context.add_analysis_tool for triggering computation workflows from menus.


6. Advanced: MainWindow Internals (Low-Level)

While V3 encourages using context, sometimes you need direct access to the MainWindow (mw) for specialized Qt or PyVista operations. Obtain it via mw = context.get_main_window().

6.1 Core Proxy Properties (Convenience)

V3 maintains several proxy properties on mw for backward compatibility and convenience.

Attribute / Method Description
mw.plotter Proxy for mw.view_3d_manager.plotter. Direct PyVista plotter access.
mw.scene Proxy for mw.init_manager.scene. Direct 2D graphics scene access.
mw.current_mol Proxy for mw.view_3d_manager.current_mol. The active RDKit molecule.
mw.draw_molecule_3d(mol) Proxy method to trigger a full 3D redraw of the scene.

6.2 The Managed Architecture (V3)

In Version 3.0, most core logic is separated into specialized Managers. If a feature is not available as a proxy on mw, you should look in the corresponding manager.

Attribute Path Type Description
mw.init_manager.settings dict Global application settings (stored in settings.json).
mw.init_manager.splitter QSplitter The UI divider between the 2D and 3D editor panels.
mw.ui_manager UIManager Handles status bar messages, editor modes, and UI state updates.
mw.io_manager IOManager Handles loading/saving molecules and project files.
mw.compute_manager ComputeManager Manages 3D coordinate conversion and background calculations.
mw.view_3d_manager View3DManager Manages the PyVista engine, styles, and 3D visualization.
mw.state_manager StateManager Manages the undo/redo stack and molecular data lifecycle.

Example: Setting the Editor Mode

mw = context.get_main_window()
# Correct V3 way to switch to Carbon draw mode
mw.ui_manager.set_mode("atom_C")

Example: Triggering Coordinate Conversion (2D to 3D)

mw = context.get_main_window()
# Correct V3 way to trigger the internal conversion logic (if molecule is already loaded)
mw.compute_manager.trigger_conversion()

7. Legacy Support (Compatibility Mode)

MoleditPy 3.0 remains compatible with older plugin patterns, though they are considered deprecated.

7.1 The run(mw) Function

If you define a function run(main_window), MoleditPy will add your plugin to the standard “Plugins” menu. This is useful for simple scripts that don’t need the advanced V3 ecosystem.

7.2 The autorun(mw) Function

Will be executed immediately on startup. Use initialize(context) for modern equivalents.


8. Debugging & Advanced Logistics

8.1 File Extension Conflicts

  • Import Menu: If multiple plugins accept the same extension, they will appear in the Import menu as separate entries (e.g., “Import .xyz (Plugin A)” and “Import .xyz (Plugin B)”).
  • Command Line: If the user opens a file via the command line, the plugin with the highest priority wins. If priorities are equal, the first registered plugin is used.

8.2 Hot Reloading

MoleditPy scans for plugins at startup. To see code changes, simply use the Plugins > Reload All Plugins menu item. V3 plugins are designed to survive reloads if their windows are correctly registered via context.register_window().

8.3 Threading & Freezing

All plugin callbacks run in the Main UI Thread. Performance-heavy tasks will freeze the GUI. Use QThread for calculations and context.show_status_message for progress updates.


9. Best Practices & Troubleshooting

9.1 Thread Safety

CRITICAL: All plugin callbacks run in the Main UI Thread.

  • Fast operations (< 100ms) are fine.
  • Slow operations (heavy QM, large loops) will freeze the entire application.
  • Solution: Use QThread or Python’s threading for heavy work and return results via signals.

9.2 Error Handling

Always wrap your logic in try...except blocks. An uncaught exception in a plugin can cause the entire application to hang or crash silently.

def my_callback(context):
    try:
        # unsafe logic
    except Exception as e:
        context.show_status_message(f"Plugin Error: {e}", 5000)

9.3 Memory Safety

Use context.register_window() for all persistent UI elements (dialogs, panels). If you don’t register them, Python’s garbage collector might delete the window while it’s still being used by the user.

9.4 Atom Indices

Remember that MoleditPy uses 0-based RDKit indices. These may differ from “Atom Numbers” in some chemical file formats. Always verify your mappings.


10. Complete Integrated Example

The following example combines multiple V3 features: namespaced windows, undo history, and status bar updates.

from PyQt6.QtWidgets import QMessageBox

PLUGIN_NAME = "Research Assistant"
PLUGIN_VERSION = "3.1.0"

def initialize(context):
    # 1. Add tool to menu
    context.add_menu_action("Research/Analyze Selection", lambda: process(context))

    # 2. Register for specific file types
    context.register_file_opener(".res", lambda path: load_research(path, context))

def process(context):
    indices = context.get_selected_atom_indices()
    if not indices:
        context.show_status_message("Select atoms first!", 2000)
        return

    context.show_status_message(f"Analyzing {len(indices)} atoms...")
    # Logic goes here...

def load_research(path, context):
    try:
        # Load logic...
        context.push_undo_checkpoint()
        context.show_status_message("Loaded research data.")
    except Exception as e:
        QMessageBox.critical(context.get_main_window(), "Error", str(e))

11. Cookbook & Examples

11.1 Analysis Tool: Molecular Weight

A tool that calculates properties and shows them in a popup. Use context.current_molecule and context.show_status_message.

from rdkit.Chem import Descriptors
from PyQt6.QtWidgets import QMessageBox

PLUGIN_NAME = "Weight Calculator"

def initialize(context):
    def run_calc():
        mol = context.current_molecule
        if not mol:
            context.show_status_message("Error: No molecule loaded!", 2000)
            return

        weight = Descriptors.MolWt(mol)
        num_atoms = mol.GetNumAtoms()

        QMessageBox.information(
            context.get_main_window(),
            "Stats",
            f"Molecular Weight: {weight:.2f}\nAtoms: {num_atoms}"
        )

    context.add_analysis_tool("Show Molecular Weight", run_calc)

11.2 Custom 3D Component: Add Sphere

Use the plotter object to add persistent custom 3D geometries.

import pyvista as pv

PLUGIN_NAME = "3D Decorator"

def initialize(context):
    context.add_menu_action("Visuals/Add Red Sphere", lambda: add_sphere(context))

def add_sphere(context):
    mw = context.get_main_window()
    plotter = mw.plotter

    # Create and add a sphere mesh
    sphere = pv.Sphere(radius=2.0)
    plotter.add_mesh(sphere, color="red", opacity=0.3, name="custom_plugin_sphere")

    context.refresh_3d_view()
    context.show_status_message("Sphere added to scene.")

11.3 Persistent Dock Panel (Singleton)

V3 uses register_window to keep panels alive and manage their unique identity across the entire session.

from PyQt6.QtWidgets import QDockWidget, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import Qt

PLUGIN_NAME = "Info Panel"

def initialize(context):
    context.add_menu_action("Tools/Show Info Panel", lambda: toggle_panel(context))

def toggle_panel(context):
    # 1. Check if the panel already exists in the manager
    dock = context.get_window("main_dock")
    if dock:
        dock.show()
        dock.raise_()
        return

    # 2. Create new panel if not found
    mw = context.get_main_window()
    dock = QDockWidget("Plugin Information", mw)
    content = QWidget()
    layout = QVBoxLayout(content)
    layout.addWidget(QLabel("Telemetry Active..."))
    dock.setWidget(content)

    mw.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)

    # 3. Register it so context.get_window() works next time
    context.register_window("main_dock", dock)
    dock.show()

11.4 Custom File Importer

Refactor your loaders to use current_molecule and push_undo_checkpoint() to ensure the user can revert the import.

from rdkit import Chem

PLUGIN_NAME = "Simple SMILES Loader"

def initialize(context):
    context.register_file_opener(".smiles", lambda path: load_smiles(path, context))

def load_smiles(path, context):
    try:
        with open(path, 'r') as f:
            smiles = f.read().strip()

        mol = Chem.MolFromSmiles(smiles)
        if mol:
            mol = Chem.AddHs(mol)

            # 1. Update the app
            context.current_molecule = mol

            # 2. Save to Undo history (MANDATORY for importers)
            context.push_undo_checkpoint()

            # 3. Adjust View
            context.reset_3d_camera()
            context.show_status_message(f"Imported {path} successfully.")
    except Exception as e:
        context.show_status_message(f"Import Failed: {e}")

11.5 Custom 3D Style (Dynamic Overrides)

Custom styles in V3 should combine standard drawing with custom elements.

PLUGIN_NAME = "High-Contrast Style"

def initialize(context):
    context.register_3d_style("High Contrast", draw_hc_style)

def draw_hc_style(mw, mol):
    # 1. Get the plotter from the window passed by the engine
    plotter = mw.plotter

    # 2. Add a background gradient or text
    plotter.set_background("black", top="gray")
    plotter.add_text("HIGH CONTRAST MODE", position='upper_left', color='yellow')

    # 3. Trigger standard draw with a style override
    # Note: Accessing view_3d_manager on mw is allowed in style callbacks
    mw.view_3d_manager.draw_standard_3d_style(mol, style_override='stick')

11.6 Custom Optimization Method

Optimization callbacks must modify the molecule in-place.

from rdkit.Chem import AllChem

PLUGIN_NAME = "Fast UFF Optimizer"

def initialize(context):
    context.register_optimization_method("Quick UFF", run_uff)

def run_uff(mol):
    """
    Called by the 'Optimize' logic.
    Modify 'mol' in-place. Return True on success.
    """
    if mol.GetNumAtoms() == 0:
        return False

    AllChem.UFFOptimizeMolecule(mol, maxIters=500)
    return True

11.7 Persistent User Preferences

Use get_setting and set_setting to remember user choices across sessions.

PLUGIN_NAME = "Smart Labels"

def initialize(context):
    # Load user preference (default to True)
    show_labels = context.get_setting("show_labels", True)
    
    context.add_menu_action("Labels/Toggle", lambda: toggle_labels(context))
    context.show_status_message(f"Labels are {'on' if show_labels else 'off'}")

def toggle_labels(context):
    current = context.get_setting("show_labels", True)
    new_state = not current
    
    # Save the new state
    context.set_setting("show_labels", new_state)
    
    context.show_status_message(f"Labels toggled to: {new_state}")
    # ... trigger re-draw logic ...

12. UI & UX Style Guide

To ensure your plugin feels like a native part of MoleditPy, follow these design principles:

12.1 Color Palette

MoleditPy uses a “Sleek Dark” theme. If you build custom widgets, use these QColor hints:

  • Background: QColor(30, 30, 35)
  • Accent: QColor(0, 120, 215)
  • Secondary Text: QColor(180, 180, 180)

12.2 Icons

Prefer using the system’s built-in icons where possible. If you provide your own, use 24x24 pixel PNGs with transparency for toolbars.


13. Testing Your Plugins

You can test your plugin logic without running the full MoleditPy GUI by mocking the PluginContext.

import unittest
from unittest.mock import MagicMock
from rdkit import Chem

class TestMyPlugin(unittest.TestCase):
    def test_logic(self):
        # 1. Create a mock context
        context = MagicMock()
        context.current_molecule = Chem.MolFromSmiles("CCO")

        # 2. Run your logic
        my_plugin_function(context)

        # 3. Verify side effects
        context.push_undo_checkpoint.assert_called_once()

14. Conclusion & Support

Thank you for contributing to the MoleditPy ecosystem! If you encounter any bugs or need new API features, please reach out via GitHub Issues.

Happy Coding!