1 /**
2  * Control, screen-scrape, and send input to other
3  * graphical programs.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <vladimir@thecybershadow.net>
13  */
14 
15 module ae.sys.sendinput;
16 
17 import core.thread;
18 import core.time;
19 
20 import std.exception;
21 import std.random;
22 import std.string;
23 
24 import ae.utils.graphics.color;
25 import ae.utils.graphics.image;
26 import ae.utils.geometry : Rect;
27 import ae.utils.math;
28 
29 version (linux)
30 {
31 	enum haveX11 = is(typeof({ import deimos.X11.X; }));
32 	//version = HAVE_X11;
33 
34 	static if (haveX11)
35 	{
36 		pragma(lib, "X11");
37 
38 		import deimos.X11.X;
39 		import deimos.X11.Xlib;
40 	}
41 
42 	import std.conv;
43 	import std.process;
44 
45 	static if (haveX11)
46 	private Display* getDisplay()
47 	{
48 		static Display* dpy;
49 		if (!dpy)
50 			dpy = XOpenDisplay(null);
51 		enforce(dpy, "Can't open display!");
52 		return dpy;
53 	}
54 
55 	void setMousePos(int x, int y)
56 	{
57 		static if (haveX11)
58 		{
59 			auto dpy = getDisplay();
60 			auto rootWindow = XRootWindow(dpy, 0);
61 			XSelectInput(dpy, rootWindow, KeyReleaseMask);
62 			XWarpPointer(dpy, None, rootWindow, 0, 0, 0, 0, x, y);
63 			XFlush(dpy);
64 		}
65 		else
66 			enforce(spawnProcess(["xdotool", "mousemove", text(x), text(y)]).wait() == 0, "xdotool failed");
67 	}
68 
69 	static if (haveX11)
70 		alias Window = deimos.X11.X.Window;
71 	else
72 		alias Window = uint;
73 
74 	auto captureWindow(Window window)
75 	{
76 		static if (haveX11)
77 		{
78 			auto g = getWindowGeometry(window);
79 			return captureWindowRect(window, Rect!int(0, 0, g.w, g.h));
80 		}
81 		else
82 		{
83 			auto result = execute(["import", "-window", text(window), "bmp:-"]);
84 			enforce(result.status == 0, "ImageMagick import failed");
85 			return result.output.parseBMP!BGR();
86 		}
87 	}
88 
89 	auto captureWindowRect(Window window, Rect!int r)
90 	{
91 		static if (haveX11)
92 		{
93 			auto dpy = getDisplay();
94 			auto ximage = XGetImage(dpy, window, r.x0, r.y0, r.w, r.h, AllPlanes, ZPixmap).xEnforce("XGetImage");
95 			scope(exit) XFree(ximage);
96 
97 			enforce(ximage.format == ZPixmap, "Wrong image format (expected ZPixmap)");
98 			enforce(ximage.bits_per_pixel == 32, "Wrong image bits_per_pixel (expected 32)");
99 
100 			alias COLOR = BGRA;
101 			return ImageRef!COLOR(ximage.width, ximage.height, ximage.chars_per_line, cast(COLOR*) ximage.data).copy();
102 		}
103 		else
104 			assert(false, "TODO");
105 	}
106 
107 	auto captureRect(Rect!int r)
108 	{
109 		static if (haveX11)
110 		{
111 			auto dpy = getDisplay();
112 			return captureWindowRect(RootWindow(dpy, DefaultScreen(dpy)), r);
113 		}
114 		else
115 			assert(false, "TODO");
116 	}
117 
118 	Window findWindowByName(string name)
119 	{
120 		// TODO haveX11
121 		auto result = execute(["xdotool", "search", "--name", name]);
122 		enforce(result.status == 0, "xdotool failed");
123 		return result.output.chomp.to!Window;
124 	}
125 
126 	static if (haveX11)
127 	Rect!int getWindowGeometry(Window window)
128 	{
129 		auto dpy = getDisplay();
130 		Window child;
131 		XWindowAttributes xwa;
132 		XGetWindowAttributes(dpy, window, &xwa).xEnforce("XGetWindowAttributes");
133 		XTranslateCoordinates(dpy, window, XRootWindow(dpy, 0), xwa.x, xwa.y, &xwa.x, &xwa.y, &child).xEnforce("XTranslateCoordinates");
134 		return Rect!int(xwa.x, xwa.y, xwa.x + xwa.width, xwa.y + xwa.height);
135 	}
136 
137 	float ease(float t, float speed)
138 	{
139 		import std.math : pow, abs;
140 		speed = 0.3f + speed * 0.4f;
141 		t = t * 2 - 1;
142 		t = (1-pow(1-abs(t), 1/speed)) * sign(t);
143 		t = (t + 1) / 2;
144 		return t;
145 	}
146 
147 	static if (haveX11)
148 	T xEnforce(T)(T cond, string msg)
149 	{
150 		return enforce(cond, msg);
151 	}
152 
153 	void easeMousePos(int x0, int y0, int x1, int y1, Duration duration)
154 	{
155 		auto xSpeed = uniform01!float;
156 		auto ySpeed = uniform01!float;
157 
158 		auto start = MonoTime.currTime();
159 		auto end = start + duration;
160 		while (true)
161 		{
162 			auto now = MonoTime.currTime();
163 			if (now >= end)
164 				break;
165 			float t = 1f * (now - start).total!"hnsecs" / duration.total!"hnsecs";
166 			setMousePos(
167 				x0 + cast(int)(ease(t, xSpeed) * (x1 - x0)),
168 				y0 + cast(int)(ease(t, ySpeed) * (y1 - y0)),
169 			);
170 			Thread.sleep(1.msecs);
171 		}
172 		x0 = x1;
173 		y0 = y1;
174 
175 		setMousePos(x1, y1);
176 	}
177 
178 	void mouseButton(int button, bool down)
179 	{
180 		// TODO haveX11
181 		enforce(spawnProcess(["xdotool", down ? "mousedown" : "mouseup", text(button)]).wait() == 0, "xdotool failed");
182 	}
183 }
184 
185 version (Windows)
186 {
187 	// TODO
188 }