Skip to content

Commit 2a22aff

Browse files
committed
Update script and instructions
1 parent 2833f73 commit 2a22aff

File tree

3 files changed

+154
-124
lines changed

3 files changed

+154
-124
lines changed

docs/_scripts/generate_screenshots/README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@ and videos from pre-recorded interactions with napari. It uses `pyautogui` and
55
`pynput` to record the screen and mouse interactions, exports the results to a
66
json file, and then generates screenshots and videos based on this data.
77

8+
NOTE: Make sure the Qt version on your system is compatible with the PyQt version
9+
you are using.
10+
811
## Usage
912

1013
To use these scripts, follow these steps:
1114
1. **Install Dependencies**: Ensure you have the required Python packages installed. You can do this by running:
1215
```bash
1316
pip install pyautogui pynput
1417
```
15-
2. **Launch napari**: Start napari in a separate terminal or environment. Ensure it is running and ready to accept interactions.
16-
3. **Record Interactions**: Use the `record.py` script to record your interactions with napari. This will create a JSON file containing the recorded mouse and keyboard events.
17-
4. **Convert to Screenshots**: Use the `convert.py` script to convert the recorded interactions into screenshots. This will generate a series of PNG files in the `screenshots` directory.
18-
5. The conversion will be saved as `play.py`. Run python play.py to play back the actions
18+
2. **Install napari**: You probably want to have a [development installation of napari](hhttps://napari.org/stable/developers/contributing/dev_install.html).
19+
3. **Record Interactions**: Use the `record_interactions.py` script to record your interactions with napari. This will
20+
a. Open a napari window,
21+
b. Record mouse and keyboard actions,
22+
c. Save the recorded actions to a JSON file named `recording.json`,
23+
d. Convert the recorded actions into a Python script named `play.py`.
24+
You can use `python record_interactions.py --help` to see the available options for naming output files.
25+
4. Run python `play.py` to play back the actions.
1926

2027
## Attribution
2128

docs/_scripts/generate_screenshots/convert.py

Lines changed: 0 additions & 103 deletions
This file was deleted.

docs/_scripts/generate_screenshots/record.py renamed to docs/_scripts/generate_screenshots/record_interactions.py

Lines changed: 143 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import time
1414
import json
1515
import napari
16+
import platform
1617
import argparse
1718
from pynput import mouse, keyboard
1819

@@ -31,7 +32,7 @@ def on_press(self, key):
3132
except AttributeError:
3233
if key == keyboard.Key.esc:
3334
print("Keyboard recording ended.")
34-
self.stop_recording()
35+
self.stop_recording("keyboard")
3536
return False
3637

3738
json_object = {"action": "pressed_key", "key": str(key), "_time": time.time()}
@@ -47,7 +48,7 @@ def on_release(self, key):
4748
self.recording.append(json_object)
4849

4950
def on_move(self, x, y):
50-
if len(self.recording) >= 1:
51+
if len(self.recording) > 1:
5152
if (
5253
self.recording[-1]["action"] == "pressed"
5354
and self.recording[-1]["button"] == "Button.left"
@@ -69,15 +70,15 @@ def on_click(self, x, y, button, pressed):
6970

7071
self.recording.append(json_object)
7172

72-
if len(self.recording) > 1:
73+
if len(self.recording) > 2:
7374
if (
7475
self.recording[-1]["action"] == "unclicked"
7576
and self.recording[-1]["button"] == "Button.right"
7677
and self.recording[-1]["_time"] - self.recording[-2]["_time"] > 2
7778
):
7879
self.save_recording()
7980
print("Mouse recording ended.")
80-
self.stop_recording()
81+
self.stop_recording("mouse")
8182
return False
8283

8384
def on_scroll(self, x, y, dx, dy):
@@ -106,7 +107,14 @@ def start_recording(self):
106107
self.viewer.window._qt_window.setGeometry(0, 0, 800, 600)
107108

108109
# Reset recording for this session
109-
self.recording = []
110+
self.recording = [{
111+
"metadata": {
112+
"start_time": time.time(),
113+
"napari_version": napari.__version__,
114+
"python_version": platform.python_version(),
115+
"os": platform.system()
116+
}
117+
}]
110118

111119
print("Press 'ESC' to end the keyboard recording")
112120
print("Hold right click for 2 seconds then release to end the mouse recording")
@@ -131,38 +139,156 @@ def start_recording(self):
131139
self.keyboard_listener.join()
132140
self.mouse_listener.join()
133141

134-
def stop_recording(self):
135-
"""Stop the recording and save the data."""
136-
if self.keyboard_listener:
142+
def stop_recording(self, mode: str):
143+
"""Stop the recording and save the data.
144+
145+
Args:
146+
mode (str): The mode of recording ("mouse" or "keyboard").
147+
"""
148+
if self.keyboard_listener and mode == "keyboard":
137149
self.keyboard_listener.stop()
138-
if self.mouse_listener:
150+
if self.mouse_listener and mode == "mouse":
139151
self.mouse_listener.stop()
140-
self.save_recording()
141152

142-
if self.viewer:
143-
self.viewer.close()
153+
if not self.keyboard_listener.running and not self.mouse_listener.running:
154+
self.save_recording()
155+
# if self.viewer:
156+
# self.viewer.close()
157+
print("Recording stopped. Please close the napari window.")
158+
159+
160+
def read_json_file(json_input: str) -> tuple:
161+
"""
162+
Takes the JSON output 'recording.json'
163+
164+
Excludes released and scrolling events to
165+
keep things simple.
166+
"""
167+
with open(json_input) as f:
168+
recording = json.load(f)
169+
metadata = recording.pop(0)
170+
171+
def excluded_actions(object):
172+
return "released" not in object["action"] and "scroll" not in object["action"]
173+
174+
recording = list(filter(excluded_actions, recording))
175+
176+
return metadata, recording
177+
178+
179+
def convert_recording(json_input: str, output_file: str = "play.py"):
180+
"""Converts the recorded interactions from JSON to a Python script using pyautogui.
181+
182+
Converts the:
183+
184+
- Mouse clicks
185+
- Keyboard input
186+
- Time between actions calculated
187+
188+
Args:
189+
json_input (str): The path to the JSON file containing the recorded interactions.
190+
output_file (str): The name of the output Python script file.
191+
"""
192+
193+
key_mappings = {
194+
"cmd": "win",
195+
"alt_l": "alt",
196+
"alt_r": "alt",
197+
"ctrl_l": "ctrl",
198+
"ctrl_r": "ctrl",
199+
}
144200

201+
metadata, recording = read_json_file(json_input)
145202

146-
def start_recording(output: str = "recording.json"):
203+
if not recording:
204+
return
205+
206+
output = open(output_file, "w")
207+
output.write(f"# {metadata}\n")
208+
output.write("import time\n")
209+
output.write("import pyautogui\n\n")
210+
output.write("# Open napari with standard window size (800x600)\n")
211+
output.write("self.viewer = napari.Viewer()\n")
212+
output.write("self.viewer.window._qt_window.setGeometry(0, 0, 800, 600)\n")
213+
214+
for i, step in enumerate(recording):
215+
print(step)
216+
217+
not_first_element = (i - 1) > 0
218+
if not_first_element:
219+
## compare time to previous time for the 'sleep' with a 10% buffer
220+
pause_in_seconds = (step["_time"] - recording[i - 1]["_time"]) * 1.1
221+
222+
output.write(f"time.sleep({pause_in_seconds})\n\n")
223+
else:
224+
output.write("time.sleep(1)\n\n")
225+
226+
if step["action"] == "pressed_key":
227+
key = (
228+
step["key"].replace("Key.", "")
229+
if "Key." in step["key"]
230+
else step["key"]
231+
)
232+
233+
if key in key_mappings.keys():
234+
key = key_mappings[key]
235+
236+
output.write(f"pyautogui.press('{key}')\n")
237+
238+
if step["action"] == "clicked":
239+
output.write(f"pyautogui.moveTo({step['x']}, {step['y']})\n")
240+
241+
if step["button"] == "Button.right":
242+
output.write("pyautogui.mouseDown(button='right')\n")
243+
else:
244+
output.write("pyautogui.mouseDown()\n")
245+
246+
if step["action"] == "unclicked":
247+
output.write(f"pyautogui.moveTo({step['x']}, {step['y']})\n")
248+
249+
if step["button"] == "Button.right":
250+
output.write("pyautogui.mouseUp(button='right')\n")
251+
else:
252+
output.write("pyautogui.mouseUp()\n")
253+
254+
print(f"Recording converted. Saved to '{output_file}'")
255+
256+
257+
def start_recording(json_output: str = "recording.json"):
147258
"""Creates a napari window and starts recording mouse and keyboard interactions.
148259
149260
Args:
150261
output (str): The name of the output JSON file to save the recording.
151262
"""
152-
recorder = InteractionRecorder(output)
263+
recorder = InteractionRecorder(json_output)
153264
recorder.start_recording()
154265

155266

156267
if __name__ == "__main__":
157268
parser = argparse.ArgumentParser(
158-
description="Record mouse and keyboard interactions in a napari window."
269+
description="Record mouse and keyboard interactions in a napari window.",
270+
epilog="Use --json-output to specify the temporary json output file name. Use --output to specify the output Python script file name."
159271
)
160272
parser.add_argument(
161-
"output",
273+
"--json-output",
162274
type=str,
163275
nargs='?',
164276
default="recording.json",
165277
help="Output JSON file name"
166278
)
279+
parser.add_argument(
280+
"--output",
281+
type=str,
282+
nargs='?',
283+
default="play.py",
284+
help="Output Python script file name (default: play.py)"
285+
)
286+
parser.add_argument(
287+
"--no-convert",
288+
action='store_true',
289+
help="Do not convert the recording to a Python script after recording"
290+
)
167291
args = parser.parse_args()
168-
start_recording(args.output)
292+
start_recording(args.json_output)
293+
if not args.no_convert:
294+
convert_recording(args.json_output, args.output)

0 commit comments

Comments
 (0)