Declarative Region Definition
Brobot introduces a powerful declarative approach to defining search regions for StateImages. This guide explains how to define regions that are dynamically calculated relative to other state objects.
Note: Code examples show the API structure. When implementing, note that
SearchRegionOnObjectandMatchAdjustmentOptionsuse Lombok@Builder(setterPrefix = "set"), so builder methods require thesetprefix (e.g.,.setTargetType(),.setAddX()).
Quick Startโ
Define a search region relative to another object in just three lines:
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetStateName("Menu").setTargetObjectName("Button")
.setAdjustments(MatchAdjustmentOptions.builder().setAddY(50).build())
.build())
That's it! The search region automatically updates whenever the target object moves. No manual region calculations needed.
Overviewโ
The declarative approach allows you to:
- Define search regions relative to other state objects
- Apply adjustments and fixed dimensions
- Eliminate manual region calculations in your action code
- Create more maintainable and reusable state definitions
Visual Example: How It Worksโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Screen โ
โ โ
โ โโโโโโโโโโโโโโโโ โ
โ โ Menu Button โ โ Target object found โ
โ โ (100, 50) โ at location โ
โ โโโโโโโโโโโโโโโโ โ
โ โ
โ โ Dependency: "50 pixels below" โ
โ โ
โ โโโโโโโโโโโโโโโโ โ
โ โ Sub Menu โ โ Dependent region โ
โ โ (100, 100) โ automatically โ
โ โโโโโโโโโโโโโโโโ calculated! โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Flow:
Target Found โ Region Calculated โ Dependent Updated
(Menu) (100, 100) (Sub Menu)
SearchRegionOnObjectโ
The SearchRegionOnObject class enables dynamic region definition:
public class SearchRegionOnObject {
private StateObject.Type targetType; // Type of target object (IMAGE, REGION, etc.)
private String targetStateName; // Name of the state containing the target
private String targetObjectName; // Name of the specific object
private MatchAdjustmentOptions adjustments; // Position and size adjustments (reuses existing class)
}
The adjustments field uses the standard MatchAdjustmentOptions class for consistency with other Brobot operations.
Basic Usageโ
Simple Relative Regionโ
Define a search region relative to another StateImage:
import io.github.jspinak.brobot.model.state.StateImage;
import io.github.jspinak.brobot.model.element.SearchRegionOnObject;
import io.github.jspinak.brobot.model.state.StateObject;
StateImage searchArea = new StateImage.Builder()
.addPatterns("search-icon.png")
.setName("SearchIcon")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("MainMenu")
.setTargetObjectName("MenuButton")
.build())
.build();
With Adjustmentsโ
Apply position and size adjustments to the derived region:
import io.github.jspinak.brobot.model.state.StateImage;
import io.github.jspinak.brobot.model.element.SearchRegionOnObject;
import io.github.jspinak.brobot.model.state.StateObject;
import io.github.jspinak.brobot.action.basic.find.MatchAdjustmentOptions;
StateImage icon = new StateImage.Builder()
.addPatterns("status-icon.png")
.setName("StatusIcon")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Dashboard")
.setTargetObjectName("HeaderBar")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(10) // Move 10 pixels right
.setAddY(-5) // Move 5 pixels up
.setAddW(50) // Expand width by 50 pixels
.setAddH(20) // Expand height by 20 pixels
.build())
.build())
.build();
With Fixed Dimensionsโ
Override the calculated dimensions with fixed values:
StateImage button = new StateImage.Builder()
.addPatterns("submit-button.png")
.setName("SubmitButton")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Form")
.setTargetObjectName("FormTitle")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddY(100) // Move down 100 pixels
.setAbsoluteW(200) // Fixed width of 200 pixels
.setAbsoluteH(50) // Fixed height of 50 pixels
.build())
.build())
.build();
Real-World Example: Claude Automatorโ
The claude-automator project demonstrates this pattern effectively:
import io.github.jspinak.brobot.annotations.State;
import io.github.jspinak.brobot.model.state.StateImage;
import io.github.jspinak.brobot.model.element.SearchRegionOnObject;
import io.github.jspinak.brobot.model.state.StateObject;
import io.github.jspinak.brobot.action.basic.find.MatchAdjustmentOptions;
import lombok.Getter;
@State
@Getter
public class WorkingState {
private final StateImage claudeIcon;
public WorkingState() {
// The search region will be dynamically defined relative to the prompt
claudeIcon = new StateImage.Builder()
.addPatterns("working/claude-icon-1",
"working/claude-icon-2",
"working/claude-icon-3",
"working/claude-icon-4")
.setName("ClaudeIcon")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Prompt")
.setTargetObjectName("ClaudePrompt")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(3) // Slight offset to the right
.setAddY(10) // Below the prompt
.setAddW(30) // Wider search area
.setAddH(55) // Taller search area
.build())
.build())
.build();
}
}
This declarative approach eliminates the need for manual region calculations:
// Before: Manual calculation in action code
private void setupIconRegion(Region promptRegion) {
Region iconRegion = new Region(promptRegion);
iconRegion.adjust(3, 10, 30, 55);
workingState.getClaudeIcon().setSearchRegions(iconRegion);
}
// After: Automatic calculation based on declaration
// No manual setup needed - just use the StateImage directly
ActionResult result = action.perform(findOptions, workingState.getClaudeIcon());
Builder Methodsโ
The SearchRegionOnObject.builder() provides a fluent API for configuration:
Basic Structureโ
SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE) // Required: Type of target
.setTargetStateName("StateName") // Required: State containing target
.setTargetObjectName("ObjectName") // Required: Name of target object
.setAdjustments(...) // Optional: Position/size adjustments using MatchAdjustmentOptions
.build()
MatchAdjustmentOptions Builderโ
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(10) // Add to x position
.setAddY(20) // Add to y position
.setAddW(30) // Add to width
.setAddH(40) // Add to height
.setAbsoluteW(200) // Override with fixed width (optional)
.setAbsoluteH(100) // Override with fixed height (optional)
.setTargetPosition(Position.CENTER) // Target position within region (optional)
.setTargetOffset(new Location(5, 5)) // Additional offset (optional)
.build())
Key Differences from Standard Match Adjustmentsโ
- When used with SearchRegionOnObject, only position and dimension adjustments apply
targetPositionandtargetOffsetare ignored for search region calculation- Use negative values in
addX/addYto move left/up - Use
absoluteW/absoluteHset to -1 (default) to not override dimensions
Cross-State Referencesโ
SearchRegionOnObject supports referencing objects from different states:
// In LoginState
StateImage loginButton = new StateImage.Builder()
.addPatterns("login-button.png")
.setName("LoginButton")
.build();
// In DashboardState - reference login button location
StateImage notification = new StateImage.Builder()
.addPatterns("notification.png")
.setName("Notification")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Login") // Different state
.setTargetObjectName("LoginButton")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddY(-50) // Above the login button
.build())
.build())
.build();
How Cross-State Dependencies Workโ
When you define a cross-state dependency:
-
Registration Phase: When states are loaded, the
SearchRegionDependencyInitializerautomatically registers all dependencies with theDynamicRegionResolver. -
Runtime Resolution: When a FIND operation succeeds:
- The
FindPipelinecallsupdateDependentSearchRegions() - All objects depending on the found object have their search regions updated
- The updates apply the configured adjustments
- The
-
Automatic Updates: Search regions are dynamically updated each time the target object is found in a new location.
Example flow:
// 1. ClaudePrompt is found at location (100, 200)
// 2. ClaudeIcon's search region is automatically updated to (103, 210, width+30, height+55)
// 3. Next search for ClaudeIcon uses this updated region
Optimized Search Region Updatesโ
Search regions are updated efficiently through dependency ordering and post-search updates, ensuring that dependent objects use current region information.
How Region Updates Workโ
When using declarative regions with dependencies:
-
Before Search: StateImages are automatically ordered by dependencies
- Images without dependencies are searched first
- Dependent images are searched after their dependencies
- This ordering ensures dependencies are resolved in the correct sequence
-
After Search Batch: When matches are found:
- All objects depending on found objects have their regions calculated and updated
- Updates happen after the search batch completes via
updateDependentSearchRegions() - Subsequent searches use the updated region information
- This ensures dependent objects search in the correct location on their next execution
-
Example Flow:
// Given this setup:
StateImage prompt = new StateImage.Builder()
.addPatterns("prompt.png")
.setName("ClaudePrompt")
.build();
StateImage icon = new StateImage.Builder()
.addPatterns("icon.png")
.setName("ClaudeIcon")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Prompt")
.setTargetObjectName("ClaudePrompt")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(3).setAddY(10).setAddW(30).setAddH(55)
.build())
.build())
.build();
// When searching for both:
action.perform(findOptions, prompt, icon);
// The execution order is:
// 1. StateImages ordered by dependency: ClaudePrompt first (no dependencies), then ClaudeIcon
// 2. Search for ClaudePrompt
// 3. Search for ClaudeIcon
// 4. After search completes, updateDependentSearchRegions() is called
// 5. If ClaudePrompt was found at (100, 200):
// - ClaudeIcon's search region is updated to (103, 210, w+30, h+55)
// 6. Next search operation will use the updated region
This update mechanism ensures:
- Dependent objects are searched in the correct order
- Search regions are updated based on latest findings
- Subsequent searches use current region information
- Better performance through dependency-aware ordering
Integration with State-Aware Schedulingโ
The declarative approach works seamlessly with StateAwareScheduler:
@Service
public class MonitoringService {
private final StateAwareScheduler scheduler;
private final WorkingState workingState;
public void startMonitoring() {
// Configure state checking
StateCheckConfiguration config = new StateCheckConfiguration.Builder()
.withRequiredStates(List.of("Prompt", "Working"))
.build();
// Schedule monitoring - regions are resolved automatically
scheduler.scheduleWithStateCheck(
executor,
this::checkIcon,
config,
5, 2, TimeUnit.SECONDS
);
}
private void checkIcon() {
// The search region is automatically calculated based on
// the current location of ClaudePrompt in PromptState
ActionResult result = action.perform(
new PatternFindOptions.Builder().build(),
workingState.getClaudeIcon()
);
}
}
Best Practicesโ
-
Use Descriptive Names: Give clear names to both source and target objects
.targetObject("HeaderNavigationBar") // Clear and specific -
Document Adjustments: Comment on why specific adjustments are used
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddY(50) // Below header, same width
.build()) -
Consider State Dependencies: Ensure target states are loaded when needed
.withRequiredStates(List.of("SourceState", "TargetState")) -
Use Fixed Dimensions Sparingly: Prefer relative sizing for responsiveness
// Good: Relative adjustment
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddW(20) // Slightly wider than source
.build())
// Use fixed only when necessary
.setAdjustments(MatchAdjustmentOptions.builder()
.setAbsoluteW(100) // Fixed width for consistent button size
.build())
Common Pitfallsโ
1. Circular Dependenciesโ
โ WRONG: Creating circular dependencies between states
// LoginState
StateImage loginButton = new StateImage.Builder()
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetStateName("Dashboard") // Depends on Dashboard
.setTargetObjectName("BackButton")
.build())
.build();
// DashboardState
StateImage dashboardPanel = new StateImage.Builder()
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetStateName("Login") // Also depends on Login โ CIRCULAR!
.setTargetObjectName("LoginButton")
.build())
.build();
โ CORRECT: One-way dependencies
// LoginState has no SearchRegionOnObject dependencies
StateImage loginButton = new StateImage.Builder()
.addPatterns("login-button.png")
.setName("LoginButton")
.build();
// DashboardState depends on Login (one-way dependency)
StateImage dashboardPanel = new StateImage.Builder()
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetStateName("Login")
.setTargetObjectName("LoginButton")
.build())
.build();
2. Target Object Not Found Firstโ
If the target object hasn't been found yet, the dependent region won't be initialized.
โ WRONG: Searching dependent object before target
// This will fail if ClaudePrompt hasn't been found yet
action.perform(findOptions, workingState.getClaudeIcon());
โ CORRECT: Let dependency ordering handle it automatically
// Search both - dependency ordering ensures ClaudePrompt is found first
action.perform(findOptions,
promptState.getClaudePrompt(),
workingState.getClaudeIcon()
);
Or search target explicitly first:
// Manually ensure target is found first
action.perform(findOptions, promptState.getClaudePrompt());
action.perform(findOptions, workingState.getClaudeIcon());
3. Name Mismatchesโ
StateObject names are case-sensitive and must match exactly.
โ WRONG: Case mismatch or wrong name
// In PromptState
.setName("ClaudePrompt")
// In WorkingState
.setTargetObjectName("claudePrompt") // WRONG: lowercase 'c'
// or
.setTargetObjectName("claude-prompt") // WRONG: hyphenated
// or
.setTargetObjectName("Prompt") // WRONG: incomplete name
โ CORRECT: Exact name match
// In PromptState
.setName("ClaudePrompt")
// In WorkingState
.setTargetObjectName("ClaudePrompt") // CORRECT: exact match
4. Wrong State Nameโ
Remember that @State annotation removes the "State" suffix from class names.
โ WRONG: Including "State" suffix
@State
public class PromptState {
// Class name: PromptState
}
// In another state
.setTargetStateName("PromptState") // WRONG: includes "State" suffix
โ CORRECT: Use name without "State" suffix
@State
public class PromptState {
// State name: "Prompt" (without "State" suffix)
}
// In another state
.setTargetStateName("Prompt") // CORRECT: without "State" suffix
Or use custom name:
@State(name = "MyCustomName")
public class PromptState {
// State name: "MyCustomName"
}
// In another state
.setTargetStateName("MyCustomName") // Use the custom name
5. Forgetting Target Typeโ
Always specify the correct target type.
โ WRONG: Wrong or missing target type
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetStateName("Menu")
.setTargetObjectName("Button")
// Missing: setTargetType()
.build())
โ CORRECT: Always set target type
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE) // Specify type
.setTargetStateName("Menu")
.setTargetObjectName("Button")
.build())
6. Inactive Target Stateโ
If the target state is not active, the dependency won't be resolved.
โ WRONG: Referencing inactive state
// LoginState is not active, but DashboardState references it
.setTargetStateName("Login") // Login state must be active!
โ CORRECT: Ensure target state is active or use state-aware scheduling
// Option 1: Use StateAwareScheduler to ensure states are active
StateCheckConfiguration config = new StateCheckConfiguration.Builder()
.withRequiredStates(List.of("Login", "Dashboard"))
.build();
// Option 2: Only reference states that remain active (e.g., hidden states)
.setTargetStateName("Header") // Header state stays active as overlay
Migration from Manual Approachโ
To migrate existing code:
-
Identify Manual Region Calculations
// Old approach
Region baseRegion = findResult.getRegion();
Region searchRegion = new Region(
baseRegion.x() + 10,
baseRegion.y() + 50,
baseRegion.w() + 20,
baseRegion.h()
);
stateImage.setSearchRegions(searchRegion); -
Convert to Declarative Definition
// New approach
stateImage = new StateImage.Builder()
.addPatterns("pattern.png")
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Base") // @State removes "State" suffix from class name
.setTargetObjectName("BaseImage")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(10)
.setAddY(50)
.setAddW(20)
.setAddH(0)
.build())
.build())
.build(); -
Remove Manual Region Management
- Delete region calculation code
- Remove region storage variables
- Simplify action methods
Implementation Architectureโ
The declarative region system consists of several key components:
Core Componentsโ
-
SearchRegionOnObject: The configuration object that defines the dependency
- Holds target state/object information
- Contains adjustment and dimension settings
- Attached to StateImages during state construction
-
SearchRegionDependencyRegistry: Tracks all dependencies
- Maps source objects to their dependents
- Provides lookup for dependent objects when sources are found
- Thread-safe for concurrent access
-
DynamicRegionResolver: Resolves and updates regions
- Calculates actual regions based on found objects
- Updates dependent object search regions
- Handles both same-state and cross-state dependencies
-
StateInitializationOrchestrator: Initializes the system
- Listens for
StatesRegisteredEvent - Collects all StateObjects with dependencies
- Registers them with the DynamicRegionResolver
- Listens for
-
FindPipeline Integration: Triggers updates
- Calls
updateDependentSearchRegions()after successful finds - Ensures dependent regions are updated before next search
- Calls
Initialization Flowโ
Application Start
โ
States Loaded (@State classes instantiated)
โ
StatesRegisteredEvent Published
โ
StateInitializationOrchestrator Receives Event
โ
Collects All StateObjects with SearchRegionOnObject
โ
Registers Dependencies with DynamicRegionResolver
โ
System Ready for Dynamic Region Updates
Runtime Flow (Updated in 1.1.0+)โ
FIND Operation Starts
โ
FindPipeline Orders StateImages by Dependencies
โ
Search for Non-Dependent Images First
โ
For Each Match Found:
- Immediately Update All Dependent Search Regions
- Clear Fixed Regions if Outside New Region
- Update Remaining Images in Current Batch
โ
Continue Searching Dependent Images
โ
All Images Use Updated Regions
Key changes in 1.1.0+:
- Dependency Ordering: Images are sorted so dependencies are resolved first
- Immediate Updates: Regions update as soon as dependencies are found
- Batch Processing: Remaining images in a search batch are updated before being searched
- Fixed Region Management: Fixed regions are cleared proactively, not reactively
Fixed Regions and Declarative Regionsโ
Interaction with Fixed Regionsโ
Fixed regions are automatically set when a StateImage with fixed=true is found for the first time. This optimization improves performance by limiting future searches to the exact location where the pattern was previously found.
However, fixed regions can conflict with declarative regions when the UI layout changes. Brobot 1.1.0+ includes intelligent fixed region management:
Automatic Fixed Region Clearingโ
When a declarative region is calculated based on SearchRegionOnObject:
- Fixed Region Check: The system checks if there's an existing fixed region
- Containment Test: It verifies if the fixed region is within the new declarative region
- Automatic Clearing: If the fixed region is outside the declarative region, it's automatically cleared
- Region Update: The search region is updated to use the declarative region
Important: Fixed regions are cleared immediately when a declarative region is applied. This happens as soon as the dependency is found, not when the dependent object is searched.
This ensures that declarative regions take precedence when the UI layout changes:
// Example scenario:
// 1. ClaudeIcon is found at (100, 200) and sets a fixed region
// 2. User moves the window, ClaudePrompt is now at (500, 300)
// 3. ClaudeIcon's declarative region is calculated as (503, 310, w+30, h+55)
// 4. Fixed region (100, 200) is outside the new region โ automatically cleared
// 5. Next search uses the declarative region (503, 310, w+30, h+55)
Best Practices for Fixed Regionsโ
-
Don't Set ActionHistory with Declarative Regions:
// โ WRONG: ActionHistory creates a fixed region that conflicts
StateImage icon = new StateImage.Builder()
.addPatterns("icon.png")
.setSearchRegionOnObject(...)
.withActionHistory(MockActionHistoryBuilder.Presets.reliable(region))
.build();
// โ CORRECT: Let declarative regions manage the search area
StateImage icon = new StateImage.Builder()
.addPatterns("icon.png")
.setSearchRegionOnObject(...)
.build(); -
Use Fixed Regions for Static UI Elements:
// Good for elements that never move
StateImage logo = new StateImage.Builder()
.addPatterns("company-logo.png")
.setFixed(true) // Will lock to first found location
.build(); -
Use Declarative Regions for Dynamic UI:
// Good for elements that move relative to others
StateImage button = new StateImage.Builder()
.addPatterns("submit-button.png")
.setSearchRegionOnObject(...) // Adapts to UI changes
.build();
Debugging Fixed Region Conflictsโ
Enable debug logging to see when fixed regions are cleared:
logging.level.io.github.jspinak.brobot.action.internal.region.DynamicRegionResolver=DEBUG
Log output will show:
INFO: Fixed region R[100,200,50,50] for ClaudeIcon is outside new declarative region R[503,310,80,105], clearing fixed region
Troubleshootingโ
Region Not Foundโ
- Verify target state and object names match exactly
- Ensure target state is active when searching
- Check that target object has been found at least once
- Enable logging:
logging.level.io.github.jspinak.brobot.action.internal.region=DEBUG
Incorrect Region Positionโ
- Log the resolved region for debugging:
ActionResult result = action.perform(findOptions, stateImage);
log.info("Search region: {}", result.getSearchedRegion()); - Adjust the adjustment values incrementally
- Consider using visual feedback:
action.perform(new HighlightOptions.Builder().build(), stateImage);
Dependencies Not Workingโ
- Verify SearchRegionDependencyInitializer is being instantiated
- Check logs for "Registered search region dependency" messages
- Ensure Spring component scanning includes brobot packages
- Verify target object names match exactly (case-sensitive)
Fixed Region Overriding Declarative Regionโ
- Check if you're setting ActionHistory on the StateImage (remove it)
- Enable debug logging to see if fixed region is being cleared
- Verify the declarative region is being calculated correctly
- Consider manually resetting the fixed region:
stateImage.getPatterns().forEach(pattern -> {
pattern.getSearchRegions().resetFixedRegion();
pattern.setFixed(false);
});
Performance Considerationsโ
- Region resolution happens on each search
- Dependencies are registered once at startup
- Updates only occur when source objects are found
- Consider using fixed regions for static layouts
Testing Declarative Regionsโ
Testing with Mock Modeโ
Declarative regions work seamlessly with Brobot's mock mode, enabling fast, headless testing without a real GUI:
import io.github.jspinak.brobot.test.BrobotTestBase;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class DeclarativeRegionTest extends BrobotTestBase {
@Autowired
private Action action;
@Autowired
private PromptState promptState;
@Autowired
private WorkingState workingState;
@Test
public void testDeclarativeRegionDependency() {
// In mock mode, dependencies are registered and resolved automatically
// No special setup needed - just use the states as normal
// Search for both target and dependent objects
ActionResult result = action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt(),
workingState.getClaudeIcon()
);
// Verify both were found
assertTrue(result.isSuccess());
assertEquals(2, result.getMatches().size());
}
}
Testing Region Calculationsโ
Verify that search regions are calculated correctly with proper adjustments:
@Test
public void testRegionAdjustments() {
// Find the target object first
ActionResult targetResult = action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt()
);
assertTrue(targetResult.isSuccess());
// Get the dependent object's calculated search region
StateImage dependentImage = workingState.getClaudeIcon();
Region searchRegion = dependentImage.getPatterns().get(0)
.getSearchRegions().getSearchRegion();
// Verify the region was calculated and updated
assertNotNull(searchRegion, "Search region should be calculated");
// If you know the expected position, verify adjustments were applied
Region targetRegion = targetResult.getBestMatch().getRegion();
assertEquals(targetRegion.x() + 3, searchRegion.x(), "X adjustment should be +3");
assertEquals(targetRegion.y() + 10, searchRegion.y(), "Y adjustment should be +10");
}
Testing Cross-State Dependenciesโ
Test that dependencies work correctly across different states:
@Test
public void testCrossStateDependency() {
// Ensure both states are active
stateManager.activateStates("Prompt", "Working");
// Find target in one state
ActionResult targetResult = action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt()
);
assertTrue(targetResult.isSuccess());
// Verify dependent object in different state uses updated region
ActionResult dependentResult = action.perform(
new PatternFindOptions.Builder().build(),
workingState.getClaudeIcon()
);
assertTrue(dependentResult.isSuccess());
// Verify the dependent was found in the expected region
Region dependentRegion = dependentResult.getBestMatch().getRegion();
Region targetRegion = targetResult.getBestMatch().getRegion();
// Dependent should be near target (with adjustments applied)
assertTrue(Math.abs(dependentRegion.x() - (targetRegion.x() + 3)) < 5,
"Dependent should be ~3 pixels right of target");
}
Testing with ActionHistory (Mock Data)โ
For completely deterministic testing, use ActionHistory to provide mock match data:
@Test
public void testWithMockActionHistory() {
// Create mock region for target object
Region mockTargetRegion = new Region(100, 200, 50, 30);
// Set up ActionHistory with mock data
promptState.getClaudePrompt().setActionHistory(
MockActionHistoryBuilder.Presets.reliable(mockTargetRegion)
);
// Find the target (will use mock data)
ActionResult result = action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt()
);
// Verify target was "found" at mock location
assertEquals(mockTargetRegion, result.getBestMatch().getRegion());
// Now verify dependent object has correct calculated region
Region expectedDependentRegion = new Region(
103, // 100 + 3 (addX)
210, // 200 + 10 (addY)
80, // 50 + 30 (addW)
85 // 30 + 55 (addH)
);
StateImage dependentImage = workingState.getClaudeIcon();
Region actualRegion = dependentImage.getPatterns().get(0)
.getSearchRegions().getSearchRegion();
assertEquals(expectedDependentRegion, actualRegion);
}
Testing Dependency Orderingโ
Verify that StateImages are searched in correct dependency order:
@Test
public void testDependencyOrdering() {
// Create a list to track search order
List<String> searchOrder = new ArrayList<>();
// Add listeners to track when each image is searched
// (Implementation depends on your testing framework)
// Search both images
action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt(),
workingState.getClaudeIcon()
);
// Verify ClaudePrompt (no dependencies) was searched before ClaudeIcon (has dependency)
assertEquals("ClaudePrompt", searchOrder.get(0));
assertEquals("ClaudeIcon", searchOrder.get(1));
}
Performance Metricsโ
Overhead Analysisโ
Declarative regions add minimal overhead to Brobot operations:
| Operation | Overhead | Impact |
|---|---|---|
| Dependency Registration | 5-10ms per dependency | One-time cost at application startup |
| Region Calculation | 0.1-0.5ms per update | Per search when target found |
| Dependency Ordering | 1-2ms | Per batch search operation |
| Total Per Search | ~2-3ms | Negligible compared to image matching (50-200ms) |
Performance Benefitsโ
The performance benefit from targeted searching far outweighs the calculation overhead:
Without Declarative Regions (full screen search):
โโ Image Match: ~150ms (searching entire screen)
โโ Total: 150ms
With Declarative Regions (targeted search):
โโ Dependency Calculation: ~0.5ms
โโ Image Match: ~30ms (searching small region)
โโ Total: 30.5ms
Performance Gain: 5x faster (80% reduction in search time)
Real-World Performanceโ
Based on testing with the claude-automator project:
- Startup Time: +15ms (registering 5 dependencies)
- Search Time per Dependent Object: -120ms average (faster due to smaller search regions)
- Memory Overhead: <1KB per dependency (negligible)
- CPU Usage: No measurable increase
Recommendationsโ
โ USE declarative regions when:
- Objects have predictable spatial relationships
- UI elements move together (menus, dialogs, panels)
- Cross-state dependencies exist
- Search regions can be significantly reduced
โ AVOID declarative regions when:
- Objects appear randomly on screen
- Target object location is unpredictable
- Simple full-screen search is fast enough
- No clear spatial relationship exists
Bottom Line: Use declarative regions freely - the performance benefit from targeted searching makes the overhead insignificant in practice.
Complete Example: Real-World Usageโ
Here's a complete example showing all the new features working together:
@State(initial = true)
public class PromptState {
private final StateImage claudePrompt;
public PromptState() {
claudePrompt = new StateImage.Builder()
.addPatterns("prompt/claude-prompt.png")
.setName("ClaudePrompt")
.build();
}
}
@State
public class WorkingState {
private final StateImage claudeIcon;
public WorkingState() {
// Icon depends on prompt location
claudeIcon = new StateImage.Builder()
.addPatterns("working/claude-icon-1.png",
"working/claude-icon-2.png")
.setName("ClaudeIcon")
.setFixed(true) // Will be cleared when prompt moves
.setSearchRegionOnObject(SearchRegionOnObject.builder()
.setTargetType(StateObject.Type.IMAGE)
.setTargetStateName("Prompt")
.setTargetObjectName("ClaudePrompt")
.setAdjustments(MatchAdjustmentOptions.builder()
.setAddX(3).setAddY(10).setAddW(30).setAddH(55)
.build())
.build())
.build();
}
}
// In your automation code:
public class ClaudeAutomator {
@Autowired
private Action action;
@Autowired
private PromptState promptState;
@Autowired
private WorkingState workingState;
public void findElements() {
// Search for both - dependency ordering happens automatically
ActionResult result = action.perform(
new PatternFindOptions.Builder().build(),
promptState.getClaudePrompt(),
workingState.getClaudeIcon()
);
// What happens internally:
// 1. FindPipeline orders images: ClaudePrompt first (no dependencies)
// 2. ClaudePrompt is found at (100, 200)
// 3. ClaudeIcon's search region immediately updated to (103, 210, w+30, h+55)
// 4. ClaudeIcon's fixed region (if any) is cleared
// 5. ClaudeIcon is searched in the updated region
// 6. If found, ClaudeIcon sets a new fixed region at the found location
}
}
Key Benefits of the New Implementationโ
- No Manual Region Management: Dependencies are resolved automatically
- Immediate Updates: Search regions update as soon as dependencies are found
- Intelligent Fixed Region Handling: Fixed regions cleared when they conflict with declarative regions
- Optimized Search Order: Dependencies are searched in the correct order
- Better Performance: Fewer false matches due to targeted searching
Summaryโ
Declarative region definition provides:
- Cleaner, more maintainable code
- Dynamic adaptation to UI changes
- Better separation of concerns
- Seamless integration with state management
- Optimized search region updates for performance
- Intelligent fixed region management
By defining regions declaratively, you create more robust automation that adapts to UI variations while keeping your action code focused on business logic rather than region calculations.
Related Documentationโ
Core Conceptsโ
- States Guide - Understanding states and the @State annotation
- State-Aware Scheduling - Scheduling with automatic state validation
- Search Regions and Fixed Locations - Search region fundamentals
Region Buildingโ
- Screen Adaptive Regions - RegionBuilder and Position integration
- Processes as Objects - Complex UI process modeling
Actions and Configurationโ
- ActionConfig Overview - PatternFindOptions and configuration
- ActionConfig Examples - Practical action examples
Testingโ
- Mock Mode Guide - Testing with ActionHistory and mock data
Tutorialsโ
- Claude Automator Tutorial - Real-world declarative region example