Skip to content

MacroManager

Samera2022 edited this page Jan 30, 2026 · 1 revision

MacroManager

Relevant source files

Overview

The MacroManager class provides the core orchestration layer for the macro recording and playback system. It manages the application's state machine (recording/playing/idle), maintains an in-memory buffer of recorded actions, coordinates playback execution in a separate thread, and handles serialization/deserialization of macros to .mmc files.

Scope: This page documents the MacroManager class architecture, state management, recording/playback control, and file operations. For details on:

  • OS-level input capture that feeds recorded events to MacroManager, see Global Input Capture
  • Individual action representation and playback execution, see MouseAction
  • .mmc file format specification and backward compatibility, see Macro File Format)

Location: src/io/github/samera2022/mouse_macros/manager/MacroManager.java


Architecture & Design

MacroManager implements a static singleton pattern with no instance creation. All methods and fields are static, making it globally accessible throughout the application as a shared macro state manager.

Class Structure

classDiagram
    class MacroManager {
        -static boolean recording
        -static boolean playing
        -static List<MouseAction> actions
        -static long lastTime
        -static Thread playThread
        +static startRecording() : void
        +static stopRecording() : void
        +static play() : void
        +static abort() : void
        +static recordAction(MouseAction) : void
        +static saveToFile(Component) : void
        +static loadFromFile(Component) : void
        +static isRecording() : boolean
        +static isPlaying() : boolean
        +static getLastTime() : long
        +static setLastTime(long) : void
        +static getActions() : List<MouseAction>
        +static clear() : void
    }
    class MouseAction {
        +int x
        +int y
        +int type
        +int button
        +long delay
        +int wheelAmount
        +int keyCode
        +int awtKeyCode
        +perform() : void
    }
    class ConfigManager {
        +static Config config
        +static FileChooserConfig fc_config
        +static saveFileChooserConfig() : void
        +static reloadFileChooserConfig() : void
    }
    class LogManager {
        +static log(String) : void
    }
    class Localizer {
        +static get(String) : String
    }
    MacroManager --> MouseAction : "stores in actions"
    MacroManager --> ConfigManager : "reads config.repeatTime"
    MacroManager --> LogManager : "outputs status"
    MacroManager --> Localizer : "retrieves messages"
Loading

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L1-L201

Static Fields

Field Type Purpose
recording boolean Indicates whether macro recording is active
playing boolean Indicates whether macro playback is in progress
actions List<MouseAction> In-memory buffer storing recorded actions
lastTime long Timestamp (milliseconds) of the last recorded event
playThread Thread Reference to the active playback thread (null when idle)

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L22-L26


State Machine

MacroManager operates as a finite state machine with three primary states: Idle, Recording, and Playing. State transitions are controlled by user actions (UI buttons or hotkeys) and enforce mutual exclusion between recording and playback.

State Diagram

#mermaid-xm94305j4lk{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-xm94305j4lk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-xm94305j4lk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-xm94305j4lk .error-icon{fill:#dddddd;}#mermaid-xm94305j4lk .error-text{fill:#222222;stroke:#222222;}#mermaid-xm94305j4lk .edge-thickness-normal{stroke-width:1px;}#mermaid-xm94305j4lk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-xm94305j4lk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-xm94305j4lk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-xm94305j4lk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-xm94305j4lk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-xm94305j4lk .marker{fill:#999;stroke:#999;}#mermaid-xm94305j4lk .marker.cross{stroke:#999;}#mermaid-xm94305j4lk svg{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica;font-size:16px;}#mermaid-xm94305j4lk p{margin:0;}#mermaid-xm94305j4lk defs #statediagram-barbEnd{fill:#999;stroke:#999;}#mermaid-xm94305j4lk g.stateGroup text{fill:#dddddd;stroke:none;font-size:10px;}#mermaid-xm94305j4lk g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-xm94305j4lk g.stateGroup .state-title{font-weight:bolder;fill:#333;}#mermaid-xm94305j4lk g.stateGroup rect{fill:#ffffff;stroke:#dddddd;}#mermaid-xm94305j4lk g.stateGroup line{stroke:#999;stroke-width:1;}#mermaid-xm94305j4lk .transition{stroke:#999;stroke-width:1;fill:none;}#mermaid-xm94305j4lk .stateGroup .composit{fill:#f4f4f4;border-bottom:1px;}#mermaid-xm94305j4lk .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-xm94305j4lk .state-note{stroke:#e6d280;fill:#fff5ad;}#mermaid-xm94305j4lk .state-note text{fill:#333;stroke:none;font-size:10px;}#mermaid-xm94305j4lk .stateLabel .box{stroke:none;stroke-width:0;fill:#ffffff;opacity:0.5;}#mermaid-xm94305j4lk .edgeLabel .label rect{fill:#ffffff;opacity:0.5;}#mermaid-xm94305j4lk .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-xm94305j4lk .edgeLabel p{background-color:#ffffff;}#mermaid-xm94305j4lk .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-xm94305j4lk .edgeLabel .label text{fill:#333;}#mermaid-xm94305j4lk .label div .edgeLabel{color:#333;}#mermaid-xm94305j4lk .stateLabel text{fill:#333;font-size:10px;font-weight:bold;}#mermaid-xm94305j4lk .node circle.state-start{fill:#999;stroke:#999;}#mermaid-xm94305j4lk .node .fork-join{fill:#999;stroke:#999;}#mermaid-xm94305j4lk .node circle.state-end{fill:#dddddd;stroke:#f4f4f4;stroke-width:1.5;}#mermaid-xm94305j4lk .end-state-inner{fill:#f4f4f4;stroke-width:1.5;}#mermaid-xm94305j4lk .node rect{fill:#ffffff;stroke:#dddddd;stroke-width:1px;}#mermaid-xm94305j4lk .node polygon{fill:#ffffff;stroke:#dddddd;stroke-width:1px;}#mermaid-xm94305j4lk #statediagram-barbEnd{fill:#999;}#mermaid-xm94305j4lk .statediagram-cluster rect{fill:#ffffff;stroke:#dddddd;stroke-width:1px;}#mermaid-xm94305j4lk .cluster-label,#mermaid-xm94305j4lk .nodeLabel{color:#333;}#mermaid-xm94305j4lk .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-xm94305j4lk .statediagram-state .divider{stroke:#dddddd;}#mermaid-xm94305j4lk .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-xm94305j4lk .statediagram-cluster.statediagram-cluster .inner{fill:#f4f4f4;}#mermaid-xm94305j4lk .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f8f8f8;}#mermaid-xm94305j4lk .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-xm94305j4lk .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-xm94305j4lk .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f8f8f8;}#mermaid-xm94305j4lk .note-edge{stroke-dasharray:5;}#mermaid-xm94305j4lk .statediagram-note rect{fill:#fff5ad;stroke:#e6d280;stroke-width:1px;rx:0;ry:0;}#mermaid-xm94305j4lk .statediagram-note rect{fill:#fff5ad;stroke:#e6d280;stroke-width:1px;rx:0;ry:0;}#mermaid-xm94305j4lk .statediagram-note text{fill:#333;}#mermaid-xm94305j4lk .statediagram-note .nodeLabel{color:#333;}#mermaid-xm94305j4lk .statediagram .edgeLabel{color:red;}#mermaid-xm94305j4lk #dependencyStart,#mermaid-xm94305j4lk #dependencyEnd{fill:#999;stroke:#999;stroke-width:1;}#mermaid-xm94305j4lk .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-xm94305j4lk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}startRecording()stopRecording()play()playback completesabort()IdleRecordingPlayingrecording = trueactions clearedlastTime initializedplaying = trueplayThread spawnediterates repeatTimerecording = falseplaying = falseplayThread = null

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L22-L86

State Query Methods

isRecording() - Returns the current value of the recording flag src/io/github/samera2022/mouse_macros/manager/MacroManager.java L71-L73

isPlaying() - Returns the current value of the playing flag src/io/github/samera2022/mouse_macros/manager/MacroManager.java L75-L77

These methods are called by GlobalMouseListener to filter input events and by MainFrame to manage button states.


Recording System

Starting Recording

Method: startRecording() src/io/github/samera2022/mouse_macros/manager/MacroManager.java L28-L33

Behavior:

  1. Clears the actions buffer with actions.clear()
  2. Sets recording = true to enable event capture
  3. Initializes lastTime = System.currentTimeMillis() for delay calculations
  4. Logs localized message: Localizer.get("start_recording")

Recording Actions

Method: recordAction(MouseAction action) src/io/github/samera2022/mouse_macros/manager/MacroManager.java L88-L91

Behavior:

  1. Appends the action to actions list
  2. Updates lastTime = System.currentTimeMillis()

This method is called by GlobalMouseListener when:

  • Mouse buttons are pressed (nativeMousePressed)
  • Mouse wheel is scrolled (nativeMouseWheelMoved)
  • Keyboard keys are pressed (nativeKeyPressed)

The delay field in each MouseAction is calculated by GlobalMouseListener as currentTime - MacroManager.getLastTime() before calling recordAction().

Stopping Recording

Method: stopRecording() src/io/github/samera2022/mouse_macros/manager/MacroManager.java L35-L38

Behavior:

  1. Sets recording = false to disable event capture
  2. Logs summary: "Stopped recording. Recorded {N} actions." (localized)

The actions buffer is not cleared on stop, allowing immediate playback or file saving.

Timestamp Management

Method Purpose
getLastTime() Returns lastTime for delay calculation by GlobalMouseListener
setLastTime(long t) Updates lastTime (used by GlobalMouseListener during recording)

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L93-L99


Playback System

Playback Execution Flow

sequenceDiagram
  participant MainFrame / Hotkey
  participant MacroManager
  participant playThread
  participant MouseAction
  participant java.awt.Robot
  participant LogManager

  MainFrame / Hotkey->>MacroManager: play()
  loop [Thread.interrupted()]
    MacroManager->>LogManager: log("no_recorded_actions")
    MacroManager-->>MainFrame / Hotkey: return early
    MacroManager->>LogManager: log("start_playback")
    MacroManager->>MacroManager: playing = true
    MacroManager->>playThread: new Thread(playback logic)
    playThread->>playThread: start()
    playThread-->>playThread: return (abort)
    playThread->>playThread: sleep(action.delay)
    playThread->>MouseAction: perform()
    MouseAction->>java.awt.Robot: mouseMove/mousePress/mouseRelease/etc
    playThread->>LogManager: log("playback_complete")
    playThread->>MacroManager: playing = false
    playThread->>MacroManager: playThread = null
  end
Loading

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L40-L69

play() Method

Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L40-L69

Execution Steps:

  1. Validation: Checks if actions.isEmpty(). If true, logs "no_recorded_actions" and returns.
  2. Initialization: * Sets playing = true * Logs "start_playback"
  3. Thread Spawning: Creates a new Thread that: * Iterates config.repeatTime times (outer loop) * For each iteration, loops through actions (inner loop) * Checks Thread.interrupted() before each action to support abort * Calls Thread.sleep(action.delay) to respect timing * Calls action.perform() to execute the action via Robot
  4. Completion Handling: * On successful completion: logs "playback_complete" * On interruption: logs "macro_aborted" (via catch block) * On error: logs "playback_error" + exception message * Always sets playing = false and playThread = null in finally block

Thread Safety: The playThread reference allows external interruption via abort().

abort() Method

Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L79-L86

Behavior:

  1. Sets playing = false
  2. Checks if playThread != null && playThread.isAlive()
  3. If true, calls playThread.interrupt() and logs "macro_aborted"
  4. Otherwise, logs "macro_not_running"

The interrupt is detected by the playback loop at line 51-52, causing immediate termination.

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L40-L86


File I/O Operations

MacroManager provides file save/load functionality with backward compatibility for older .mmc formats.

File Format Summary

The .mmc file format is CSV-based with 5-8 fields per line:

Field Count Fields Version
8 x,y,type,button,delay,wheelAmount,keyCode,awtKeyCode Current (v1.0.0+)
7 x,y,type,button,delay,wheelAmount,keyCode v0.1.0
6 x,y,type,button,delay,wheelAmount v0.0.2
5 x,y,type,button,delay v0.0.1

For detailed format specification, see Macro File Format).

Save Operation

flowchart TD

Start["saveToFile(parent)"]
CreateChooser["Create JFileChooser"]
SetInitialDir["Set initial directory from fc_config.lastSaveDirectory"]
SetFilter["Set file filter: MMC_FILTER"]
ShowDialog["showSaveDialog(parent)"]
CheckResult["User approved?"]
End1["Return"]
GetFile["selectedFile = chooser.getSelectedFile()"]
CheckExt["File ends with .mmc?"]
AddExt["Append .mmc extension"]
SaveDir["fc_config.setLastSaveDirectory()"]
SaveConfig["saveFileChooserConfig(fc_config)"]
Reload["reloadFileChooserConfig()"]
OpenWriter["Open PrintWriter with UTF-8"]
WriteLoop["For each action in actions"]
WriteLine["Write CSV line: x,y,type,button,delay,wheelAmount,keyCode,awtKeyCode"]
CheckMore["More actions?"]
LogSuccess["Log: macro_saved + path"]
LogError["Log: macro_saving_failed + message"]
End2["Return"]

Start --> CreateChooser
CreateChooser --> SetInitialDir
SetInitialDir --> SetFilter
SetFilter --> ShowDialog
ShowDialog --> CheckResult
CheckResult --> End1
CheckResult --> GetFile
GetFile --> CheckExt
CheckExt --> AddExt
CheckExt --> SaveDir
AddExt --> SaveDir
SaveDir --> SaveConfig
SaveConfig --> Reload
Reload --> OpenWriter
OpenWriter --> WriteLoop
WriteLoop --> WriteLine
WriteLine --> CheckMore
CheckMore --> WriteLoop
CheckMore --> LogSuccess
OpenWriter --> LogError
LogSuccess --> End2
LogError --> End2
Loading

Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L107-L131

Key Details:

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L107-L131

src/io/github/samera2022/mouse_macros/constant/FileConsts.java L1-L10

Load Operation

Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L133-L200

Backward Compatibility Parser:

The load operation supports four different formats by parsing line length:

flowchart TD

ReadLine["Read CSV line"]
Split["Split by comma"]
CheckLength["arr.length?"]
Parse8["Parse all 8 fields"]
Parse7["Parse 7 fields, awtKeyCode=0"]
Parse6["Parse 6 fields, keyCode=0, awtKeyCode=0"]
Parse5["Parse 5 fields (legacy), wheelAmount=0, keyCode=0, awtKeyCode=0"]
LogError["Log: macro_loading_line_error"]
CreateAction["new MouseAction(x,y,type,button,delay,wheelAmount,keyCode,awtKeyCode)"]
AddAction["actions.add(action)"]
Continue["Continue to next line"]

ReadLine --> Split
Split --> CheckLength
CheckLength --> Parse8
CheckLength --> Parse7
CheckLength --> Parse6
CheckLength --> Parse5
CheckLength --> LogError
Parse8 --> CreateAction
Parse7 --> CreateAction
Parse6 --> CreateAction
Parse5 --> CreateAction
CreateAction --> AddAction
AddAction --> Continue
LogError --> Continue
Loading

Parsing Logic:

Line Format Code Reference Default Values
8 fields [lines 156-165](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 156-165) None (all fields present)
7 fields [lines 166-174](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 166-174) awtKeyCode = 0
6 fields [lines 175-182](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 175-182) keyCode = 0, awtKeyCode = 0
5 fields [lines 183-189](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 183-189) wheelAmount = 0, keyCode = 0, awtKeyCode = 0

Error Handling:

Post-Load State:

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L133-L200


Integration Points

Dependencies

MacroManager integrates with multiple subsystems:

Subsystem Usage Code Reference
ConfigManager Reads config.repeatTime for playback loops [line 49](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 49)
Reads/writes fc_config for file chooser state [lines 111-121, 136-144](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 111-121, 136-144)
LogManager Outputs status messages to UI log area [lines 18, 32, 37, 42, etc.](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 18, 32, 37, 42, etc.)
Localizer Retrieves localized message strings [lines 4, 32, 37, 42, etc.](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 4, 32, 37, 42, etc.)
MouseAction Stores recorded events in actions list [line 24, 89](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 24, 89)
Calls action.perform() during playback [line 55](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 55)
FileConsts Uses MMC_FILTER for file chooser filter [lines 5, 113, 138](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 5, 113, 138)
OtherConsts (Imported but unused in current version) [line 6](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 6)

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L1-L20

External Callers

MacroManager methods are invoked by:

  1. MainFrame - UI button handlers: * Record button → startRecording() / stopRecording() * Play button → play() * Abort button → abort() * Save/Load menu items → saveToFile() / loadFromFile()
  2. GlobalMouseListener - Hotkey handlers and event recording: * F2 hotkey → startRecording() * F3 hotkey → stopRecording() * F4 hotkey → play() * F5 hotkey → abort() * Event capture → recordAction(MouseAction) * Timing queries → getLastTime(), setLastTime()

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L1-L201

FileChooserConfig Integration

MacroManager persists file dialog state through FileChooserConfig:

flowchart TD

MM["MacroManager"]
FCC["fc_config"]
LastLoad["lastLoadDirectory"]
LastSave["lastSaveDirectory"]
SaveConfig["ConfigManager.saveFileChooserConfig()"]
CacheFile["cache.json"]
ReloadConfig["ConfigManager.reloadFileChooserConfig()"]

MM --> FCC
FCC --> LastLoad
FCC --> LastSave
MM --> SaveConfig
SaveConfig --> CacheFile
MM --> ReloadConfig
ReloadConfig --> CacheFile
Loading

Workflow:

  1. On save/load dialog open: Read fc_config.lastSaveDirectory or fc_config.lastLoadDirectory [lines 111, 136](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 111, 136)
  2. On file selection: Update directory with fc_config.setLastSaveDirectory() or setLastLoadDirectory() [lines 119, 142](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 119, 142)
  3. Persist to disk: Call saveFileChooserConfig(fc_config) [lines 120, 143](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 120, 143)
  4. Reload from disk: Call reloadFileChooserConfig() [lines 121, 144](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 121, 144)

This creates a persistent "memory" of the last directories used, improving UX for repeated save/load operations.

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L107-L144

src/io/github/samera2022/mouse_macros/manager/config/FileChooserConfig.java L1-L28


Utility Methods

Actions Buffer Access

Method Return Type Purpose
getActions() List<MouseAction> Returns direct reference to actions list
clear() void Calls actions.clear() to empty buffer

Usage: getActions() is used by MacroSettingsDialog to display action details for custom macro editing. The clear() method is primarily called internally by startRecording().

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L101-L105


Thread Safety Considerations

MacroManager is not thread-safe:

  • The actions list can be modified during iteration if recording/loading occurs during playback
  • The playing flag is set without synchronization
  • Multiple calls to play() could spawn multiple playThread instances (though UI should prevent this)

Current Mitigation:

  • UI buttons are disabled based on state queries (isRecording(), isPlaying())
  • GlobalMouseListener checks state before calling recordAction()
  • Playback thread checks Thread.interrupted() for safe abort

Potential Issues:

  • Concurrent play() calls from different threads would create race conditions
  • actions list modifications during playback iteration could cause ConcurrentModificationException

In practice, these issues are avoided by the UI control flow and hotkey handling in GlobalMouseListener, which enforces sequential operations.

Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L22-L69

Clone this wiki locally