-
Notifications
You must be signed in to change notification settings - Fork 0
MacroManager
Relevant source files
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
-
.mmcfile format specification and backward compatibility, see Macro File Format)
Location: src/io/github/samera2022/mouse_macros/manager/MacroManager.java
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.
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"
Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L1-L201
| 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
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.
#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 = nullSources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L22-L86
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.
Method: startRecording() src/io/github/samera2022/mouse_macros/manager/MacroManager.java L28-L33
Behavior:
- Clears the
actionsbuffer withactions.clear() - Sets
recording = trueto enable event capture - Initializes
lastTime = System.currentTimeMillis()for delay calculations - Logs localized message:
Localizer.get("start_recording")
Method: recordAction(MouseAction action) src/io/github/samera2022/mouse_macros/manager/MacroManager.java L88-L91
Behavior:
- Appends the action to
actionslist - 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().
Method: stopRecording() src/io/github/samera2022/mouse_macros/manager/MacroManager.java L35-L38
Behavior:
- Sets
recording = falseto disable event capture - Logs summary:
"Stopped recording. Recorded {N} actions."(localized)
The actions buffer is not cleared on stop, allowing immediate playback or file saving.
| 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
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
Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L40-L69
Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L40-L69
Execution Steps:
-
Validation: Checks if
actions.isEmpty(). If true, logs"no_recorded_actions"and returns. -
Initialization: * Sets
playing = true* Logs"start_playback" -
Thread Spawning: Creates a new
Threadthat: * Iteratesconfig.repeatTimetimes (outer loop) * For each iteration, loops throughactions(inner loop) * ChecksThread.interrupted()before each action to support abort * CallsThread.sleep(action.delay)to respect timing * Callsaction.perform()to execute the action viaRobot -
Completion Handling: * On successful completion: logs
"playback_complete"* On interruption: logs"macro_aborted"(via catch block) * On error: logs"playback_error"+ exception message * Always setsplaying = falseandplayThread = nullin finally block
Thread Safety: The playThread reference allows external interruption via abort().
Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L79-L86
Behavior:
- Sets
playing = false - Checks if
playThread != null && playThread.isAlive() - If true, calls
playThread.interrupt()and logs"macro_aborted" - 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
MacroManager provides file save/load functionality with backward compatibility for older .mmc formats.
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).
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
Implementation: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L107-L131
Key Details:
- Uses
JFileChooserwithFileConsts.MMC_FILTERsrc/io/github/samera2022/mouse_macros/constant/FileConsts.java L6-L9 - Automatically appends
.mmcextension if missing [line 117-118](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 117-118) - Persists last save directory to
FileChooserConfig[line 119-121](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 119-121) - Uses
PrintWriterwithStandardCharsets.UTF_8for Unicode support [line 122](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 122) - Writes all 8 fields per line in current format [line 124](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 124)
Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L107-L131
src/io/github/samera2022/mouse_macros/constant/FileConsts.java L1-L10
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
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:
- Per-line exceptions are caught and logged with line number [lines 191-193](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 191-193)
- File-level exceptions are caught and logged [lines 196-198](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 196-198)
- Parsing continues even if individual lines fail, maximizing data recovery
Post-Load State:
-
actions.clear()is called before parsing [line 149](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 149) -
fc_config.lastLoadDirectoryis persisted [lines 142-144](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 142-144) - Success message includes file path and action count [line 195](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/line 195)
Sources: src/io/github/samera2022/mouse_macros/manager/MacroManager.java L133-L200
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
MacroManager methods are invoked by:
-
MainFrame - UI button handlers: * Record button →
startRecording()/stopRecording()* Play button →play()* Abort button →abort()* Save/Load menu items →saveToFile()/loadFromFile() -
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
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
Workflow:
- On save/load dialog open: Read
fc_config.lastSaveDirectoryorfc_config.lastLoadDirectory[lines 111, 136](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 111, 136) - On file selection: Update directory with
fc_config.setLastSaveDirectory()orsetLastLoadDirectory()[lines 119, 142](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 119, 142) - Persist to disk: Call
saveFileChooserConfig(fc_config)[lines 120, 143](https://github.com/Samera2022/MouseMacros/blob/1eb6620b/lines 120, 143) - 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
| 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
MacroManager is not thread-safe:
- The
actionslist can be modified during iteration if recording/loading occurs during playback - The
playingflag is set without synchronization - Multiple calls to
play()could spawn multipleplayThreadinstances (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 -
actionslist modifications during playback iteration could causeConcurrentModificationException
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