/*
* Copyright (c) 2010-2016, Sikuli.org, sikulix.com
* Released under the MIT License.
*
*/
package org.sikuli.script;
import java.awt.*;
import java.util.Date;
import org.sikuli.basics.Debug;
import org.sikuli.basics.Settings;
import org.sikuli.util.EventObserver;
import org.sikuli.util.OverlayCapturePrompt;
import org.sikuli.util.ScreenHighlighter;
/**
* A screen represents a physical monitor with its coordinates and size according to the global
* point system: the screen areas are grouped around a point (0,0) like in a cartesian system (the
* top left corner and the points contained in the screen area might have negative x and/or y values)
* <br >The screens are arranged in an array (index = id) and each screen is always the same object
* (not possible to create new objects).
* <br>A screen inherits from class Region, so it can be used as such in all aspects. If you need
* the region of the screen more than once, you have to create new ones based on the screen.
* <br>The so called primary screen is the one with top left (0,0) and has id 0.
*/
public class Screen extends Region implements IScreen {
static RunTime runTime = RunTime.get();
private static String me = "Screen: ";
private static int lvl = 3;
private static Region fakeRegion;
private static void log(int level, String message, Object... args) {
Debug.logx(level, me + message, args);
}
private static IRobot globalRobot = null;
protected static Screen[] screens = null;
protected static int primaryScreen = -1;
private static int waitForScreenshot = 300;
protected IRobot robot = null;
protected int curID = -1;
protected int oldID = 0;
protected int monitor = -1;
protected boolean waitPrompt;
protected OverlayCapturePrompt prompt;
private final static String promptMsg = "Select a region on the screen";
public static boolean ignorePrimaryAtCapture = false;
private ScreenImage lastScreenImage = null;
private static boolean isActiveCapturePrompt = false;
private static EventObserver captureObserver = null;
private static synchronized boolean setActiveCapturePrompt() {
if (isActiveCapturePrompt) {
return false;
}
Debug.log(3, "TRACE: Screen: setActiveCapturePrompt");
isActiveCapturePrompt = true;
return true;
}
private static synchronized void resetActiveCapturePrompt() {
Debug.log(3, "TRACE: Screen: resetActiveCapturePrompt");
isActiveCapturePrompt = false;
captureObserver = null;
}
//<editor-fold defaultstate="collapsed" desc="Initialization">
static {
RunTime.loadLibrary("VisionProxy");
initScreens(false);
}
private long lastCaptureTime = -1;
// private static void initScreens() {
// initScreens(false);
// }
public int getcurrentID() {
return curID;
}
private static void initScreens(boolean reset) {
if (screens != null && !reset) {
return;
}
log(lvl+1, "initScreens: entry");
primaryScreen = 0;
setMouseRobot();
if (null == globalRobot) {
screens = new Screen[1];
screens[0] = null;
} else {
screens = new Screen[runTime.nMonitors];
screens[0] = new Screen(0, runTime.mainMonitor);
screens[0].initScreen();
int nMonitor = 0;
for (int i = 1; i < screens.length; i++) {
if (nMonitor == runTime.mainMonitor) {
nMonitor++;
}
screens[i] = new Screen(i, nMonitor);
screens[i].initScreen();
nMonitor++;
}
Mouse.init();
if (getNumberScreens() > 1) {
log(lvl, "initScreens: multi monitor mouse check");
Location lnow = Mouse.at();
float mmd = Settings.MoveMouseDelay;
Settings.MoveMouseDelay = 0f;
Location lc = null, lcn = null;
for (Screen s : screens) {
lc = s.getCenter();
Mouse.move(lc);
lcn = Mouse.at();
if (!lc.equals(lcn)) {
log(lvl, "*** multimonitor click check: %s center: (%d, %d) --- NOT OK: (%d, %d)",
s.toStringShort(), lc.x, lc.y, lcn.x, lcn.y);
} else {
log(lvl, "*** checking: %s center: (%d, %d) --- OK", s.toStringShort(), lc.x, lc.y);
}
}
Mouse.move(lnow);
Settings.MoveMouseDelay = mmd;
}
}
}
public static IRobot getGlobalRobot() {
return globalRobot;
}
private static void setMouseRobot() {
try {
if (globalRobot == null && !GraphicsEnvironment.isHeadless()) {
globalRobot = new RobotDesktop();
}
} catch (AWTException e) {
Debug.error("Can't initialize global Robot for Mouse: " + e.getMessage());
}
}
private IRobot getMouseRobot() {
setMouseRobot();
if (null == globalRobot && !GraphicsEnvironment.isHeadless()) {
log(-1, "problem getting a java.awt.Robot");
Sikulix.endError(999);
}
return globalRobot;
}
protected static Region getFakeRegion() {
if (fakeRegion == null) {
fakeRegion = new Region(0,0,5,5);
}
return fakeRegion;
}
/**
* create a Screen (ScreenUnion) object as a united region of all available monitors
* @return ScreenUnion
*/
public static ScreenUnion all() {
return new ScreenUnion();
}
// hack to get an additional internal constructor for the initialization
private Screen(int id, boolean init) {
super();
curID = id;
}
// hack to get an additional internal constructor for the initialization
private Screen(int id, int monitor) {
super();
curID = id;
this.monitor = monitor;
}
public static Screen as(int id) {
if (id < 0 || id >= runTime.nMonitors) {
Debug.error("Screen(%d) not in valid range 0 to %d - using primary %d",
id, runTime.nMonitors - 1, primaryScreen);
return screens[0];
} else {
return screens[id];
}
}
/**
* The screen object with the given id
*
* @param id valid screen number
*/
public Screen(int id) {
super();
if (id < 0 || id >= runTime.nMonitors) {
Debug.error("Screen(%d) not in valid range 0 to %d - using primary %d",
id, runTime.nMonitors - 1, primaryScreen);
curID = primaryScreen;
} else {
curID = id;
}
monitor = screens[curID].monitor;
initScreen();
}
/**
* INTERNAL USE
* collect all physical screens to one big region<br>
* TODO: under evaluation, wether it really makes sense
* @param isScreenUnion true/false
*/
public Screen(boolean isScreenUnion) {
super(isScreenUnion);
}
/**
* INTERNAL USE
* collect all physical screens to one big region<br>
* This is under evaluation, wether it really makes sense
*/
public void setAsScreenUnion() {
oldID = curID;
curID = -1;
}
/**
* INTERNAL USE
* reset from being a screen union to the screen used before
*/
public void setAsScreen() {
curID = oldID;
}
/**
* Is the screen object having the top left corner as (0,0). If such a screen does not exist it is
* the screen with id 0.
*/
public Screen() {
super();
curID = primaryScreen;
initScreen();
}
//TODO: remove this method if it is not needed
public void initScreen(Screen scr) {
updateSelf();
}
private void initScreen() {
Rectangle bounds = getBounds();
x = (int) bounds.getX();
y = (int) bounds.getY();
w = (int) bounds.getWidth();
h = (int) bounds.getHeight();
// try {
// robot = new RobotDesktop(this);
// robot.setAutoDelay(10);
// } catch (AWTException e) {
// Debug.error("Can't initialize Java Robot on Screen " + curID + ": " + e.getMessage());
// robot = null;
// }
robot = globalRobot;
}
/**
* {@inheritDoc}
* @return Screen
*/
@Override
public Screen getScreen() {
return this;
}
/**
* Should not be used - throws UnsupportedOperationException
* @param s Screen
* @return should not return
*/
@Override
protected Region setScreen(IScreen s) {
throw new UnsupportedOperationException("The setScreen() method cannot be called from a Screen object.");
}
/**
* show the current monitor setup
*/
public static void showMonitors() {
// initScreens();
Debug.logp("*** monitor configuration [ %s Screen(s)] ***", Screen.getNumberScreens());
Debug.logp("*** Primary is Screen %d", primaryScreen);
for (int i = 0; i < runTime.nMonitors; i++) {
Debug.logp("Screen %d: %s", i, Screen.getScreen(i).toStringShort());
}
Debug.logp("*** end monitor configuration ***");
}
/**
* re-initialize the monitor setup (e.g. when it was changed while running)
*/
public static void resetMonitors() {
Debug.error("*** BE AWARE: experimental - might not work ***");
Debug.error("Re-evaluation of the monitor setup has been requested");
Debug.error("... Current Region/Screen objects might not be valid any longer");
Debug.error("... Use existing Region/Screen objects only if you know what you are doing!");
initScreens(true);
Debug.logp("*** new monitor configuration [ %s Screen(s)] ***", Screen.getNumberScreens());
Debug.logp("*** Primary is Screen %d", primaryScreen);
for (int i = 0; i < runTime.nMonitors; i++) {
Debug.logp("Screen %d: %s", i, Screen.getScreen(i).toStringShort());
}
Debug.error("*** end new monitor configuration ***");
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="getters setters">
protected boolean useFullscreen() {
return false;
}
private static int getValidID(int id) {
if (id < 0 || id >= runTime.nMonitors) {
Debug.error("Screen: invalid screen id %d - using primary screen", id);
return primaryScreen;
}
return id;
}
private static int getValidMonitor(int id) {
if (id < 0 || id >= runTime.nMonitors) {
Debug.error("Screen: invalid screen id %d - using primary screen", id);
return runTime.mainMonitor;
}
return screens[id].monitor;
}
/**
*
* @return number of available screens
*/
public static int getNumberScreens() {
return runTime.nMonitors;
}
/**
*
* @return the id of the screen at (0,0), if not exists 0
*/
public static int getPrimaryId() {
return primaryScreen;
}
/**
*
* @return the screen at (0,0), if not exists the one with id 0
*/
public static Screen getPrimaryScreen() {
return screens[primaryScreen];
}
/**
*
* @param id of the screen
* @return the screen with given id, the primary screen if id is invalid
*/
public static Screen getScreen(int id) {
return screens[getValidID(id)];
}
/**
*
* @return the screen's rectangle
*/
@Override
public Rectangle getBounds() {
return new Rectangle(runTime.getMonitor(monitor));
}
/**
*
* @param id of the screen
* @return the physical coordinate/size <br>as AWT.Rectangle to avoid mix up with getROI
*/
public static Rectangle getBounds(int id) {
return new Rectangle(runTime.getMonitor(getValidMonitor(id)));
}
/**
* each screen has exactly one robot (internally used for screen capturing)
* <br>available as a convenience for those who know what they are doing. Should not be needed
* normally.
*
* @param id of the screen
* @return the AWT.Robot of the given screen, if id invalid the primary screen
*/
public static IRobot getRobot(int id) {
return getScreen(id).getRobot();
}
/**
*
* @return the id
*/
@Override
public int getID() {
return curID;
}
/**
* INTERNAL USE: to be compatible with ScreenUnion
* @param x value
* @param y value
* @return id of the screen
*/
@Override
public int getIdFromPoint(int x, int y) {
return curID;
}
/**
* Gets the Robot of this Screen.
*
* @return The Robot for this Screen
*/
@Override
public IRobot getRobot() {
return getMouseRobot();
}
protected static IRobot getRobot(Region reg) {
if (reg == null || null == reg.getScreen()) {
return getPrimaryScreen().getMouseRobot();
} else {
return reg.getScreen().getRobot();
}
}
/**
* creates a region on the current screen with the given coordinate/size. The coordinate is
* translated to the current screen from its relative position on the screen it would have been
* created normally.
*
* @param loc Location
* @param width value
* @param height value
* @return the new region
*/
public Region newRegion(Location loc, int width, int height) {
return Region.create(loc.copyTo(this), width, height);
}
@Override
public ScreenImage getLastScreenImageFromScreen() {
return lastScreenImage;
}
/**
* creates a location on the current screen with the given point. The coordinate is translated to
* the current screen from its relative position on the screen it would have been created
* normally.
*
* @param loc Location
* @return the new location
*/
public Location newLocation(Location loc) {
return (new Location(loc)).copyTo(this);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Capture - SelectRegion">
public ScreenImage cmdCapture(Object... args) {
if (args.length == 0) {
return userCapture("capture an image");
}
if (args.length == 1) {
Object arg0 = args[0];
if (arg0 instanceof String) {
return userCapture((String) arg0);
} else if (arg0 instanceof Region) {
return capture((Region) arg0);
} else if (arg0 instanceof Rectangle) {
return capture((Rectangle) arg0);
}
}
if (args.length == 4) {
Integer argInt = null;
for (Object arg : args){
argInt = null;
try {
argInt = (Integer) arg;
} catch (Exception ex) {
break;
}
}
if (argInt != null) {
return capture((int) args[0], (int) args[1], (int) args[2], (int) args[3]);
}
}
return userCapture("Invalid parameter for capture");
}
/**
* create a ScreenImage with the physical bounds of this screen
*
* @return the image
*/
@Override
public ScreenImage capture() {
return capture(getRect());
}
/**
* create a ScreenImage with given coordinates on this screen.
*
* @param x x-coordinate of the region to be captured
* @param y y-coordinate of the region to be captured
* @param w width of the region to be captured
* @param h height of the region to be captured
* @return the image of the region
*/
@Override
public ScreenImage capture(int x, int y, int w, int h) {
Rectangle rect = newRegion(new Location(x, y), w, h).getRect();
return capture(rect);
}
public ScreenImage captureforHighlight(int x, int y, int w, int h) {
return robot.captureScreen(new Rectangle(x, y, w, h));
}
/**
* create a ScreenImage with given rectangle on this screen.
*
* @param rect The Rectangle to be captured
* @return the image of the region
*/
@Override
public ScreenImage capture(Rectangle rect) {
lastCaptureTime = new Date().getTime();
ScreenImage simg = robot.captureScreen(rect);
if (Settings.FindProfiling) {
Debug.logp("[FindProfiling] Screen.capture [%d x %d]: %d msec",
rect.width, rect.height, new Date().getTime() - lastCaptureTime);
}
lastScreenImage = simg;
if (Debug.getDebugLevel() > lvl) {
simg.saveLastScreenImage(runTime.fSikulixStore);
}
return simg;
}
/**
* create a ScreenImage with given region on this screen
*
* @param reg The Region to be captured
* @return the image of the region
*/
@Override
public ScreenImage capture(Region reg) {
return capture(reg.getRect());
}
public static void doPrompt(String message, EventObserver obs) {
captureObserver = obs;
Screen.getPrimaryScreen().userCapture(message);
}
public static void closePrompt() {
for (int is = 0; is < Screen.getNumberScreens(); is++) {
if (!Screen.getScreen(is).hasPrompt()) {
continue;
}
Screen.getScreen(is).prompt.close();
}
}
public static void closePrompt(Screen scr) {
for (int is = 0; is < Screen.getNumberScreens(); is++) {
if (Screen.getScreen(is).getID() == scr.getID() ||
!Screen.getScreen(is).hasPrompt()) {
continue;
}
Screen.getScreen(is).prompt.close();
Screen.getScreen(is).prompt = null;
}
}
public static void resetPrompt(OverlayCapturePrompt ocp) {
int scrID = ocp.getScrID();
if (scrID > -1) {
Screen.getScreen(scrID).prompt = null;
}
resetActiveCapturePrompt();
}
public boolean hasPrompt() {
return prompt != null;
}
/**
* interactive capture with predefined message: lets the user capture a screen image using the
* mouse to draw the rectangle
*
* @return the image
*/
public ScreenImage userCapture() {
return userCapture("");
}
/**
* interactive capture with given message: lets the user capture a screen image using the mouse to
* draw the rectangle
*
* @param message text
* @return the image
*/
@Override
public ScreenImage userCapture(final String message) {
if (!setActiveCapturePrompt()) {
return null;
}
Debug.log(3, "TRACE: Screen: userCapture");
waitPrompt = true;
Thread th = new Thread() {
@Override
public void run() {
String msg = message.isEmpty() ? promptMsg : message;
for (int is = 0; is < Screen.getNumberScreens(); is++) {
if (ignorePrimaryAtCapture && is == 0) {
continue;
}
Screen.getScreen(is).prompt = new OverlayCapturePrompt(Screen.getScreen(is));
Screen.getScreen(is).prompt.addObserver(captureObserver);
Screen.getScreen(is).prompt.prompt(msg);
}
}
};
th.start();
if (captureObserver != null) {
return null;
}
boolean isComplete = false;
ScreenImage simg = null;
int count = 0;
while (!isComplete) {
this.wait(0.1f);
if (count++ > waitForScreenshot) {
break;
}
for (int is = 0; is < Screen.getNumberScreens(); is++) {
OverlayCapturePrompt ocp = Screen.getScreen(is).prompt;
if (ocp == null) {
continue;
}
if (ocp.isComplete()) {
closePrompt(Screen.getScreen(is));
simg = ocp.getSelection();
if (simg != null) {
Screen.getScreen(is).lastScreenImage = simg;
}
ocp.close();
Screen.getScreen(is).prompt = null;
isComplete = true;
}
}
}
resetActiveCapturePrompt();
return simg;
}
public String saveCapture(String name) {
return saveCapture(name, null);
}
public String saveCapture(String name, Region reg) {
ScreenImage simg;
if (reg == null) {
simg = userCapture("Capture for image " + name);
} else {
simg = capture(reg);
}
if (simg == null) {
return null;
} else {
return simg.saveInBundle(name);
}
}
/**
* interactive region create with predefined message: lets the user draw the rectangle using the
* mouse
*
* @return the region
*/
public Region selectRegion() {
return selectRegion("Select a region on the screen");
}
/**
* interactive region create with given message: lets the user draw the rectangle using the mouse
*
* @param message text
* @return the region
*/
public Region selectRegion(final String message) {
Debug.log(3, "TRACE: Screen: selectRegion");
ScreenImage sim = userCapture(message);
if (sim == null) {
return null;
}
Rectangle r = sim.getROI();
return Region.create((int) r.getX(), (int) r.getY(),
(int) r.getWidth(), (int) r.getHeight());
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Visual effects">
@Override
public void showTarget(Location loc) {
showTarget(loc, Settings.SlowMotionDelay);
}
protected void showTarget(Location loc, double secs) {
if (Settings.isShowActions()) {
ScreenHighlighter overlay = new ScreenHighlighter(this, null);
overlay.showTarget(loc, (float) secs);
}
}
//</editor-fold>
@Override
public String toString() {
Rectangle r = getBounds();
String scrText = curID == -1 ? "Union" : "" + curID;
return String.format("S(%s)[%d,%d %dx%d] E:%s, T:%.1f",
scrText, (int) r.getX(), (int) r.getY(),
(int) r.getWidth(), (int) r.getHeight(),
getThrowException() ? "Y" : "N", getAutoWaitTimeout());
}
/**
* only a short version of toString()
*
* @return like S(0) [0,0, 1440x900]
*/
@Override
public String toStringShort() {
Rectangle r = getBounds();
String scrText = curID == -1 ? "Union" : "" + curID;
return String.format("S(%s)[%d,%d %dx%d]",
scrText, (int) r.getX(), (int) r.getY(),
(int) r.getWidth(), (int) r.getHeight());
}
}