Popup Blocker

25 views
Skip to first unread message

Ruben K

unread,
Jun 18, 2025, 3:28:15 PMJun 18
to RevitPythonShell
I made this popup blocker with ai. It handles the white popups in the bottom right corner and puts the info from them into a separate log window. Speeds up workflow with less interruptions.

import clr
import os
import System
import time
import subprocess

# Add Revit API references
clr.AddReference('RevitAPI')
clr.AddReference('RevitAPIUI')
clr.AddReference('RevitServices')
clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")

# Import necessary classes from Revit API
from Autodesk.Revit.DB.Events import FailuresProcessingEventArgs, DocumentChangedEventArgs
from Autodesk.Revit.UI.Events import DialogBoxShowingEventArgs
from Autodesk.Revit.DB import (
    FailureProcessingResult, FailuresAccessor, FailureSeverity,
    BuiltInFailures, FailureResolutionType, FailureDefinitionId
)
from Autodesk.Revit.UI import UIApplication, TaskDialog, TaskDialogResult
from System.Collections.Generic import List
from System import Guid
from System.Windows.Forms import Form, TextBox, Button, DockStyle, ScrollBars, FormBorderStyle, BorderStyle, Panel
from System.Drawing import Point, Size, Color, Font, FontStyle, ContentAlignment, FontFamily

# Get the current Revit application and document context
uiapp = __revit__
app = uiapp.Application
doc = __revit__.ActiveUIDocument.Document

# DELETE INSTANCE FAILURE GUIDS
DELETE_INSTANCE_GUIDS = [
    Guid("c4c5d448-87fb-4622-acff-fdb957172dda"),  # "Instance(s) of ... not cutting anything"
    Guid("62f9582a-2459-495d-ba79-23ae8d0753e1"),  # "Can't cut instance(s) ... out of its host"
    Guid("6650b20b-592d-41a0-9f52-51f5f26e3fc3"),
    Guid("36113e20-98f1-4c51-9ace-39cbee1e6c8f"),  # "Can't cut instance(s) ... out of its host"
    Guid("74441dd6-e6dd-41ea-a57c-04f4a4957f19"),
    Guid("16f579ea-6f84-4989-95d1-007c3bdb2f73"),
    Guid("6650b20b-592d-41a0-9f52-51f5f26e3fc3"),
    Guid("c31ef50f-fc52-49f1-8b3a-aff5fab42f1a"),
    Guid("b44c8ba0-7a86-44c1-bbf1-2de8e2017266"),
    Guid("8a9ff20d-fdc2-4f98-87e6-2aa8b71b0c83"),
    Guid("b00a7be7-d0b4-47e6-88c4-eeada3021131"),
    Guid("34a653ae-4577-4bc4-ae55-e2df7f5cfa9c"),
    Guid("76297875-79c8-451b-8f9e-3eed349ac3ba"),
    Guid("88500be4-b871-448c-b406-afd488c3580d"),
    Guid("8e5a1b64-2345-40cf-aa3b-523e5e42700e"),
    Guid("4749d31a-cb87-4536-9637-b53dd96f3722"),
    Guid("c3e30bdc-bfc1-4ff4-a8cf-2aa55c020350"),
     
    # Add more GUIDs as needed from your log
]

# Save prompt dialog IDs - DO NOT SUPPRESS THESE
save_prompt_ids = [
    "TaskDialog_Close_Project",         # Save prompt when closing project
    "TaskDialog_Changes_Not_Saved",     # Generic unsaved changes prompt
    "TaskDialog_Exit_Without_Saving",   # Exit without saving prompt
    "TaskDialog_Save_File",             # Save prompt when closing file
    "TaskDialog_Model_Upgrade"          # Prompt to upgrade model.
    "TaskDialog_Stop_Operation"         # Interrupt/Cancel
    "TaskDialog_Family_Already_Exists"
]

class LogWindow(Form):
    """Dedicated scrolling log window that stays on top and auto-updates"""
    def __init__(self, title):
        self.Text = title
        self.Size = Size(1000, 600)  # Increased size for better readability
        self.StartPosition = System.Windows.Forms.FormStartPosition.Manual
        self.Location = Point(50, 50)  # Better starting position
        self.TopMost = True
        self.FormBorderStyle = FormBorderStyle.SizableToolWindow
       
        # Create panel for buttons at the top
        self.panel = Panel()
        self.panel.Dock = DockStyle.Top
        self.panel.Height = 40
        self.Controls.Add(self.panel)
       
        # Create clear button
        self.clear_btn = Button()
        self.clear_btn.Text = "Clear Log"
        self.clear_btn.Size = Size(100, 30)
        self.clear_btn.Location = Point(10, 5)
        self.clear_btn.Click += self.clear_log
        self.panel.Controls.Add(self.clear_btn)
       
        # Create close button
        self.close_btn = Button()
        self.close_btn.Text = "Close"
        self.close_btn.Size = Size(100, 30)
        self.close_btn.Location = Point(120, 5)
        self.close_btn.Click += self.close_window
        self.panel.Controls.Add(self.close_btn)
       
        # Create open file button
        self.file_btn = Button()
        self.file_btn.Text = "Open File"
        self.file_btn.Size = Size(100, 30)
        self.file_btn.Location = Point(230, 5)
        self.file_btn.Click += self.open_log_file
        self.panel.Controls.Add(self.file_btn)
       
        # Create text box below the button panel
        self.log_box = TextBox()
        self.log_box.Multiline = True
        self.log_box.ScrollBars = ScrollBars.Both  # Enable both scrollbars
        self.log_box.Dock = DockStyle.Fill
        self.log_box.ReadOnly = True
        self.log_box.BackColor = Color.FromArgb(30, 30, 30)
        self.log_box.ForeColor = Color.LightGray
        self.log_box.Font = Font("Consolas", 10)  # Monospaced font for better alignment
        self.log_box.BorderStyle = BorderStyle.Fixed3D
        self.log_box.WordWrap = False  # Disable word wrapping to prevent messy formatting
        self.Controls.Add(self.log_box)
       
        # Bring text box to front to ensure it's visible
        self.log_box.BringToFront()
       
        self.log_path = None
   
    def append_log(self, message):
        """Append message to log window and scroll to end"""
        # Convert to Windows-style newlines
        message = message.replace("\n", "\r\n")
        self.log_box.AppendText(message + "\r\n")
        self.log_box.SelectionStart = self.log_box.TextLength
        self.log_box.ScrollToCaret()
   
    def clear_log(self, sender, args):
        """Clear the log window"""
        self.log_box.Clear()
   
    def close_window(self, sender, args):
        """Close the log window"""
        self.Close()
   
    def open_log_file(self, sender, args):
        """Open the log file in default editor"""
        if self.log_path and os.path.exists(self.log_path):
            subprocess.Popen(['notepad.exe', self.log_path])

class NotificationLogger:
    """
    A singleton class to manage logging Revit notifications and command outcomes to a file and window.
    Ensures only one instance of the logger exists throughout the script's execution.
    """
    _instance = None
    log_file_path = None
    processed_failures_cache = set()
    last_command_outcome = {}
    encountered_guids = set()
    log_window = None
   
    def __init__(self):
        if NotificationLogger._instance is not None:
            raise Exception("This class is a singleton! Use get_logger() method.")

        # Define and create the log directory in the user's home folder
        log_dir = os.path.join(os.path.expanduser('~'), "RevitLogs")
        if not os.path.exists(log_dir):
            os.makedirs(log_dir)

        # Generate a timestamp for the log file name
        timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss")
        self.log_file_path = os.path.join(log_dir, f"RevitNotifications_{timestamp}.txt")

        # Write initial header messages to the log file
        self.log("=" * 80)
        self.log("REVIT NOTIFICATION AND COMMAND LOGGER")
        self.log(f"Started at {System.DateTime.Now.ToString('HH:mm:ss')}")
        self.log(f"Tracking {len(DELETE_INSTANCE_GUIDS)} GUIDs for auto-deletion")
        self.log("=" * 80)
        self.log("\nPredefined GUIDs:")
        for guid in DELETE_INSTANCE_GUIDS:
            self.log(f"  {guid}")
        self.log("-" * 80)
        self.log("Ready to capture Revit notifications and command outcomes.\n")

        # Create and show log window
        self.create_log_window()

    def create_log_window(self):
        """Create the dedicated log window"""
        try:
            self.log_window = LogWindow("Revit Log Viewer")
            self.log_window.log_path = self.log_file_path
            self.log_window.Show()
            # Add a slight delay to ensure window is fully initialized before logging
            System.Threading.Thread.Sleep(100)
            self.log("Created dedicated log window")
        except Exception as e:
            self.log(f"Could not create log window: {str(e)}")
            # Fallback to Notepad
            self.open_log()

    def open_log(self):
        """Open log in Notepad as fallback"""
        try:
            # Create file if it doesn't exist
            if not os.path.exists(self.log_file_path):
                with open(self.log_file_path, 'w') as f:
                    f.write("Revit Notification Log\n")
           
            # Open log in Notepad
            subprocess.Popen(['notepad.exe', self.log_file_path])
            self.log("Opened log in Notepad")
           
        except Exception as e:
            print(f"Could not open log: {str(e)}")

    @staticmethod
    def get_logger():
        if NotificationLogger._instance is None:
            NotificationLogger._instance = NotificationLogger()
        return NotificationLogger._instance

    def log(self, message):
        try:
            # Write to file
            with open(self.log_file_path, 'a') as f:
                f.write(f"{message}\n")
           
            # Write to window if available
            if self.log_window and not self.log_window.IsDisposed:
                self.log_window.append_log(message)
        except Exception as e:
            print(f"Logging error: {str(e)} - Message: {message}")

    def clear_failure_cache(self):
        self.processed_failures_cache.clear()

# Global references to prevent garbage collection
_failure_handler = None
_dialog_box_handler = None
_document_changed_handler = None
_application_closing_handler = None

def on_failures_processing(sender, args):
    logger = NotificationLogger.get_logger()
    timestamp = System.DateTime.Now.ToString("HH:mm:ss")
    failures_accessor = args.GetFailuresAccessor()
    current_failure_messages = failures_accessor.GetFailureMessages()

    if not current_failure_messages or current_failure_messages.Count == 0:
        args.SetProcessingResult(FailureProcessingResult.Continue)
        return

    logger.log(f"\n[FAILURES] {timestamp}")
    logger.log("-" * 60)

    force_silent_rollback = False
    resolved_all = True

    for failure in current_failure_messages:
        description = failure.GetDescriptionText()
        severity = failure.GetSeverity()
        failure_id = failure.GetFailureDefinitionId()
        guid = failure_id.Guid
        logger.encountered_guids.add(guid)

        current_failure_key = f"{description}-{severity}-{guid}"
        if current_failure_key in logger.processed_failures_cache:
            continue
        logger.processed_failures_cache.add(current_failure_key)

        elements = []
        element_ids = []
        failing_ids = failure.GetFailingElementIds()
        if failing_ids and failing_ids.Count > 0:
            current_doc = uiapp.ActiveUIDocument.Document
            for id_val in failing_ids:
                elem = current_doc.GetElement(id_val)
                if elem:
                    try:
                        elem_name = getattr(elem, 'Name', '')
                        if not elem_name:
                            elem_name = f"Element {id_val.IntegerValue}"
                        elements.append(elem_name)
                        element_ids.append(id_val)
                    except Exception:
                        elements.append(f"Element {id_val.IntegerValue}")
                        element_ids.append(id_val)
       
        # Format element list to prevent long lines
        element_list = ""
        if elements:
            element_list = ", ".join(elements)  # Show all elements for deletion context
        else:
            element_list = "No specific elements"

        # Log details in a clean, structured format
        logger.log(f"GUID: {guid}")
        logger.log(f"Type: {severity}")
        logger.log(f"Description: {description}")
        logger.log(f"Elements: {element_list}")
       
        if guid in DELETE_INSTANCE_GUIDS:
            logger.log("GUID Status: IN SUPPRESSION LIST")
        else:
            logger.log("GUID Status: NOT IN SUPPRESSION LIST")

        try:
            # =============================================================
            # HANDLE DELETE INSTANCE FAILURES
            # =============================================================
            if guid in DELETE_INSTANCE_GUIDS:
                # Attempt resolution with DeleteElements if available
                if failure.HasResolutionOfType(FailureResolutionType.DeleteElements):
                    failing_ids = failure.GetFailingElementIds()
                    if failures_accessor.IsElementsDeletionPermitted(failing_ids):
                        resolution_type = FailureResolutionType.DeleteElements
                        failure.SetCurrentResolutionType(resolution_type)
                        failures_accessor.ResolveFailure(failure)
                       
                        # ENHANCED: Log specific deleted elements
                        del_list = ", ".join(elements) if elements else "No specific elements"
                        logger.log(f"Resolution: DELETED ELEMENTS: {del_list}")
                        continue
               
                # Try Default resolution as fallback
                if failure.HasResolutionOfType(FailureResolutionType.Default):
                    resolution_type = FailureResolutionType.Default
                    failure.SetCurrentResolutionType(resolution_type)
                    failures_accessor.ResolveFailure(failure)
                    logger.log(f"Resolution: Applied default action using '{resolution_type}'")
                    continue
                   
                # If we get here, no resolution was available
                logger.log("Resolution: No resolution available")
                resolved_all = False
                force_silent_rollback = True
               
            # =============================================================
            # HANDLE WARNINGS (ALWAYS DELETE)
            # =============================================================
            elif severity == FailureSeverity.Warning:
                failures_accessor.DeleteWarning(failure)
                logger.log("Resolution: Warning deleted")
               
            # =============================================================
            # HANDLE ERRORS (GENERIC)
            # =============================================================
            elif severity == FailureSeverity.Error:
                # Try to resolve with DeleteElements if available
                if failure.HasResolutionOfType(FailureResolutionType.DeleteElements):
                    failing_ids = failure.GetFailingElementIds()
                    if failures_accessor.IsElementsDeletionPermitted(failing_ids):
                        resolution_type = FailureResolutionType.DeleteElements
                        failure.SetCurrentResolutionType(resolution_type)
                        failures_accessor.ResolveFailure(failure)
                       
                        # ENHANCED: Log specific deleted elements
                        del_list = ", ".join(elements) if elements else "No specific elements"
                        logger.log(f"Resolution: DELETED ELEMENTS: {del_list}")
                        continue
                       
                # Try UnjoinElements resolution
                if failure.HasResolutionOfType(FailureResolutionType.UnjoinElements):
                    resolution_type = FailureResolutionType.UnjoinElements
                    failure.SetCurrentResolutionType(resolution_type)
                    failures_accessor.ResolveFailure(failure)
                    logger.log(f"Resolution: Unjoined elements using '{resolution_type}'")
                    continue
                   
                # Try DetachElements resolution
                if failure.HasResolutionOfType(FailureResolutionType.DetachElements):
                    resolution_type = FailureResolutionType.DetachElements
                    failure.SetCurrentResolutionType(resolution_type)
                    failures_accessor.ResolveFailure(failure)
                    logger.log(f"Resolution: Detached elements using '{resolution_type}'")
                    continue
                   
                # Try Default resolution as last resort
                if failure.HasResolutionOfType(FailureResolutionType.Default):
                    resolution_type = FailureResolutionType.Default
                    failure.SetCurrentResolutionType(resolution_type)
                    failures_accessor.ResolveFailure(failure)
                    logger.log(f"Resolution: Applied default action using '{resolution_type}'")
                    continue
                   
                # If no resolution worked, mark for rollback
                logger.log("Resolution: No resolution available")
                resolved_all = False
                force_silent_rollback = True
               
            # =============================================================
            # HANDLE FATAL ERRORS (FORCE ROLLBACK)
            # =============================================================
            elif severity == FailureSeverity.FatalError:
                logger.log("Resolution: Fatal error - cannot resolve")
                resolved_all = False
                force_silent_rollback = True
               
        except Exception as ex:
            logger.log(f"Resolution Error: {str(ex)}")
            resolved_all = False
            force_silent_rollback = True
           
        logger.log("-" * 40)

    if force_silent_rollback or not resolved_all:
        failures_accessor.SetClearAfterRollback(True)
        args.SetProcessingResult(FailureProcessingResult.ProceedWithRollBack)
        logger.log("[RESULT] Dialog suppressed with rollback")
        logger.last_command_outcome['status'] = 'Canceled'
        logger.last_command_outcome['timestamp'] = timestamp
    else:
        args.SetProcessingResult(FailureProcessingResult.ProceedWithCommit)
        logger.log("[RESULT] All resolved - committed")
        logger.last_command_outcome['status'] = 'Completed'
        logger.last_command_outcome['timestamp'] = timestamp

    logger.clear_failure_cache()
    logger.log("=" * 60)

def on_dialog_box_showing(sender, args):
    logger = NotificationLogger.get_logger()
    timestamp = System.DateTime.Now.ToString("HH:mm:ss")
    dialog_id = args.DialogId
    dialog_message = args.Message

    logger.log(f"\n[DIALOG] {timestamp}")
    logger.log(f"ID: {dialog_id}")
    logger.log(f"Message: {dialog_message}")

    # RESTORED ORIGINAL ID-BASED METHOD: Save prompt detection
    # Check if this is a save prompt dialog by ID
    if dialog_id in save_prompt_ids:
        logger.log("Action: Allowed (save prompt) - NOT suppressing")
        return  # Don't override this dialog

    try:
        # For all other dialogs, attempt to dismiss automatically
        args.OverrideResult(1)  # Usually corresponds to OK/default action
        logger.log("Action: Dismissed programmatically")
    except Exception as e:
        logger.log(f"Action Error: {str(e)}")

def on_document_changed(sender, args):
    logger = NotificationLogger.get_logger()
    timestamp = System.DateTime.Now.ToString("HH:mm:ss")

    logger.log(f"\n[DOC CHANGE] {timestamp}")
    logger.log(f"Operation: {args.GetOperation()}")

    if logger.last_command_outcome:
        status = logger.last_command_outcome.get('status', 'Unknown')
        logger.log(f"Correlated with last command: {status}")
        logger.last_command_outcome.clear()

def application_closing(sender, args):
    logger = NotificationLogger.get_logger()
    try:
        logger.log("\n" + "=" * 80)
        logger.log("SESSION SUMMARY")
        logger.log(f"Ended at {System.DateTime.Now.ToString('HH:mm:ss')}")
       
        logger.log(f"\nEncountered {len(logger.encountered_guids)} unique GUIDs:")
        for guid in logger.encountered_guids:
            status = "IN SUPPRESSION LIST" if guid in DELETE_INSTANCE_GUIDS else "NEW - CONSIDER ADDING"
            logger.log(f"  {guid} - {status}")
           
        logger.log("\nPredefined GUIDs:")
        for guid in DELETE_INSTANCE_GUIDS:
            logger.log(f"  {guid}")
           
        logger.log("=" * 80)

        # Unsubscribe from all events
        if _failure_handler:
            app.FailuresProcessing -= _failure_handler
        if _dialog_box_handler:
            uiapp.DialogBoxShowing -= _dialog_box_handler
        if _document_changed_handler:
            app.DocumentChanged -= _document_changed_handler

        logger.log("Event handlers unsubscribed")

    except Exception as e:
        logger.log(f"Shutdown error: {str(e)}")
    finally:
        NotificationLogger._instance = None

# --- Script Execution Start ---

logger = NotificationLogger.get_logger()

_failure_handler = on_failures_processing
_dialog_box_handler = on_dialog_box_showing
_document_changed_handler = on_document_changed
_application_closing_handler = application_closing

app.FailuresProcessing += _failure_handler
uiapp.DialogBoxShowing += _dialog_box_handler
app.DocumentChanged += _document_changed_handler

import System.AppDomain
System.AppDomain.CurrentDomain.ProcessExit += _application_closing_handler
System.AppDomain.CurrentDomain.DomainUnload += _application_closing_handler

logger.log("Event handlers attached successfully")


Reply all
Reply to author
Forward
0 new messages