/*---------------------------------------------------------------------------- ** wclickat.c ** Utilty to send clicks to Wine Windows ** ** See usage() for usage instructions. ** **--------------------------------------------------------------------------- ** Copyright 2004 Jozef Stefanka for CodeWeavers, Inc. ** Copyright 2005 Dmitry Timoshkov for CodeWeavers, Inc. ** Copyright 2005 Francois Gouget for CodeWeavers, Inc. ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ** **--------------------------------------------------------------------------*/ #include #include #include #include #define APP_NAME "wclickat" #define DEFAULT_DELAY 500 #define DEFAULT_REPEAT 1000 #define ARRAY_LENGTH(array) (sizeof(array)/sizeof((array)[0])) static const WCHAR STATIC_CLASS[]={'s','t','a','t','i','c','\0'}; /*---------------------------------------------------------------------------- ** Global variables **--------------------------------------------------------------------------*/ #define RC_RUNNING -1 #define RC_SUCCESS 0 #define RC_INVALID_ARGUMENTS 1 #define RC_NODISPLAY 2 #define RC_TIMEOUT 3 static int status; typedef enum { ACTION_INVALID, ACTION_FIND, ACTION_LCLICK, ACTION_MCLICK, ACTION_RCLICK } action_type; static action_type g_action = ACTION_INVALID; static WCHAR* g_window_class = NULL; static WCHAR* g_window_title = NULL; static long g_control_id = 0; static WCHAR* g_control_class = NULL; static WCHAR* g_control_caption = NULL; static long g_x = -1; static long g_y = -1; static long g_dragto_x = -1; static long g_dragto_y = -1; static long g_disabled = 0; static long g_delay = DEFAULT_DELAY; static long g_timeout = 0; static long g_repeat = 0; static long g_untildeath = 0; static UINT timer_id; /* * Provide some basic debugging support. */ #ifdef __GNUC__ #define __PRINTF_ATTR(fmt,args) __attribute__((format (printf,fmt,args))) #else #define __PRINTF_ATTR(fmt,args) #endif static int debug_on=0; static int init_debug() { char* str=getenv("CXTEST_DEBUG"); if (str && strstr(str, "+wclickat")) debug_on=1; return debug_on; } static void cxlog(const char* format, ...) __PRINTF_ATTR(1,2); static void cxlog(const char* format, ...) { va_list valist; if (debug_on) { va_start(valist, format); vfprintf(stderr, format, valist); va_end(valist); } } /*---------------------------------------------------------------------------- ** usage **--------------------------------------------------------------------------*/ static void usage(void) { fprintf(stderr, "%s - Utility to send clicks to Wine Windows.\n", APP_NAME); fprintf(stderr, "----------------------------------------------\n"); fprintf(stderr, "Usage:\n"); fprintf(stderr, " %s action --winclass class --wintitle title [--timeout ms]\n",APP_NAME); fprintf(stderr, " %*.*s [--ctrlclas class] [--ctrlcaption caption] [--ctrlid id]\n", strlen(APP_NAME) + 3, strlen(APP_NAME) + 3, ""); fprintf(stderr, " %*.*s [--position XxY] [--delay ms] [--untildeath] [--repeat ms]\n", strlen(APP_NAME) + 3, strlen(APP_NAME) + 3, ""); fprintf(stderr, "Where action can be one of:\n"); fprintf(stderr, " find Find the specified window or control\n"); fprintf(stderr, " button Send a click with the given X button number\n"); fprintf(stderr, " click|lclick Synonym for button1 (left click)\n"); fprintf(stderr, " mclick Synonym for button2 (middle click)\n"); fprintf(stderr, " rclick Synonym for button3 (right click)\n"); fprintf(stderr, "\n"); fprintf(stderr, "The options are as follows:\n"); fprintf(stderr, " --timeout ms How long to wait before failing with a code of %d\n", RC_TIMEOUT); fprintf(stderr, " --winclass class Class name of the top-level window of interest\n"); fprintf(stderr, " --wintitle title Title of the top-level window of interest\n"); fprintf(stderr, " --ctrlclass name Class name of the control of interest, if any\n"); fprintf(stderr, " --ctrlcaption cap A substring of the control's caption\n"); fprintf(stderr, " --ctrlid id Id of the control\n"); fprintf(stderr, " --position XxY Coordinates for the click, relative to the window / control\n"); fprintf(stderr, " --dragto If given, then position specifies start click, and\n"); fprintf(stderr, " dragto specifies release coords.\n"); fprintf(stderr, " --allow-disabled Match the window or control even hidden or disabled\n"); fprintf(stderr, " --delay ms Wait ms milliseconds before clicking. The default is %d\n", DEFAULT_DELAY); fprintf(stderr, " --untildeath Wait until the window disappears\n"); fprintf(stderr, " --repeat ms Click every ms milliseconds. The default is %d\n", DEFAULT_REPEAT); fprintf(stderr, "\n"); fprintf(stderr, "%s returns %d on success\n", APP_NAME, RC_SUCCESS); fprintf(stderr, "\n"); fprintf(stderr, "Environment variable overrides:\n"); fprintf(stderr, " CXTEST_TIME_MULTIPLE Specifies a floating multiplier applied to any\n"); fprintf(stderr, " delay and timeout parameters.\n"); } static const WCHAR* my_strstriW(const WCHAR* haystack, const WCHAR* needle) { const WCHAR *h,*n; WCHAR first; if (!*needle) return haystack; /* Special case the first character because * we will be doing a lot of comparisons with it. */ first=towlower(*needle); needle++; while (*haystack) { while (towlower(*haystack)!=first && *haystack) haystack++; h=haystack+1; n=needle; while (towlower(*h)==towlower(*n) && *h) { h++; n++; } if (!*n) return haystack; haystack++; } return NULL; } static BOOL CALLBACK find_control(HWND hwnd, LPARAM lParam) { WCHAR str[1024]; HWND* pcontrol; if (!GetClassNameW(hwnd, str, ARRAY_LENGTH(str)) || lstrcmpiW(str, g_control_class)) return TRUE; if (g_control_caption) { if (!GetWindowTextW(hwnd, str, ARRAY_LENGTH(str)) || !my_strstriW(str, g_control_caption)) return TRUE; } if (g_control_id && g_control_id != GetWindowLong(hwnd, GWL_ID)) return TRUE; /* Check that the control is visible and active */ if (!g_disabled) { DWORD style = GetWindowStyle(hwnd); if (!(style & WS_VISIBLE) || (style & WS_DISABLED)) return TRUE; } pcontrol = (HWND*)lParam; *pcontrol = hwnd; return FALSE; } static BOOL CALLBACK find_top_window(HWND hwnd, LPARAM lParam) { WCHAR str[1024]; HWND* pwindow; if (!GetClassNameW(hwnd, str, ARRAY_LENGTH(str)) || lstrcmpiW(str, g_window_class)) return TRUE; if (!GetWindowTextW(hwnd, str, ARRAY_LENGTH(str)) || lstrcmpiW(str, g_window_title)) return TRUE; /* Check that the window is visible and active */ if (!g_disabled) { DWORD style = GetWindowStyle(hwnd); if (!(style & WS_VISIBLE) || (style & WS_DISABLED)) return TRUE; } /* See if we find the control we want */ if (g_control_class) { HWND control = NULL; EnumChildWindows(hwnd, find_control, (LPARAM)&control); if (!control) return TRUE; hwnd=control; } pwindow = (HWND*)lParam; *pwindow = hwnd; return FALSE; } static HWND find_window() { HWND hwnd; hwnd=NULL; EnumWindows(find_top_window, (LPARAM)&hwnd); return hwnd; } static void do_click(HWND window, DWORD down, DWORD up) { WINDOWINFO window_info; long x, y; SetForegroundWindow(GetParent(window)); window_info.cbSize=sizeof(window_info); GetWindowInfo(window, &window_info); /* The calculations below convert the coordinates so they are absolute * screen coordinates in 'Mickeys' as required by mouse_event. * In mickeys the screen size is always 65535x65535. */ x=window_info.rcWindow.left+g_x; if (x=window_info.rcWindow.right) x=(window_info.rcWindow.right+window_info.rcWindow.left)/2; x=(x << 16)/GetSystemMetrics(SM_CXSCREEN); y=window_info.rcWindow.top+g_y; if (y=window_info.rcWindow.bottom) y=(window_info.rcWindow.bottom+window_info.rcWindow.top)/2; y=(y << 16)/GetSystemMetrics(SM_CYSCREEN); mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, x, y, 0, 0); if (down) { mouse_event(MOUSEEVENTF_ABSOLUTE | down, x, y, 0, 0); if ((g_dragto_x > 0) && (g_dragto_y > 0)) { int i; long dx, dy; long step_per_x, step_per_y; long dragto_x, dragto_y; dragto_x=window_info.rcWindow.left+g_dragto_x; if (dragto_x=window_info.rcWindow.right) dragto_x=(window_info.rcWindow.right+window_info.rcWindow.left)/2; dragto_x=(dragto_x << 16)/GetSystemMetrics(SM_CXSCREEN); dragto_y=window_info.rcWindow.top+g_dragto_y; if (dragto_y=window_info.rcWindow.bottom) dragto_y=(window_info.rcWindow.bottom+window_info.rcWindow.top)/2; dragto_y=(dragto_y << 16)/GetSystemMetrics(SM_CYSCREEN); dx = g_dragto_x - g_x; dy = g_dragto_y - g_y; step_per_x = dx / 4; step_per_y = dy / 4; for (i = 0; i < 4; i++) { mouse_event(MOUSEEVENTF_MOVE, step_per_x, step_per_y, 0, 0); } x=dragto_x; y=dragto_y; } } if (up) mouse_event(MOUSEEVENTF_ABSOLUTE | up, x, y, 0, 0); } static void CALLBACK ClickProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { HWND window = find_window(); if (!window) { if (g_untildeath) { /* FIXME: The window / control might just be disabled and if * that's the case we should not exit yet. But I don't expect * --untildeath to be used at all anyway so fixing this can * wait until it becomes necessary. */ status=RC_SUCCESS; } else cxlog("The window has disappeared!\n"); return; } switch (g_action) { case ACTION_FIND: /* Nothing to do */ break; case ACTION_LCLICK: cxlog("Sending left click\n"); do_click(window, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP); break; case ACTION_MCLICK: cxlog("Sending middle click\n"); do_click(window, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP); break; case ACTION_RCLICK: cxlog("Sending right click\n"); do_click(window, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP); default: fprintf(stderr, "error: unknown action %d\n", g_action); break; } if (!g_repeat) status=RC_SUCCESS; } static void CALLBACK DelayProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { KillTimer(NULL, timer_id); timer_id=0; if (g_repeat) { cxlog("Setting up a timer for --repeat\n"); timer_id=SetTimer(NULL, 0, g_repeat, ClickProc); } ClickProc(NULL, 0, 0, 0); } static void CALLBACK FindWindowProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { HWND window = find_window(); if (!window) return; cxlog("Found the window\n"); if (g_delay) { cxlog("Waiting for a bit\n"); KillTimer(NULL, timer_id); timer_id=SetTimer(NULL, 0, g_delay, DelayProc); do_click(window, 0,0); } else { DelayProc(NULL, 0, 0, 0); } } static void CALLBACK TimeoutProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { status = RC_TIMEOUT; } /*---------------------------------------------------------------------------- ** parse_arguments **--------------------------------------------------------------------------*/ static int arg_get_long(const char** *argv, const char* name, long* value) { if (!**argv) { fprintf(stderr, "error: missing argument for '%s'\n", name); return 1; } *value=atol(**argv); if (*value < 0) { fprintf(stderr, "error: invalid argument '%s' for '%s'\n", **argv, name); (*argv)++; return 1; } (*argv)++; return 0; } static int arg_get_utf8(const char** *argv, const char* name, WCHAR* *value) { int len; if (!**argv) { fprintf(stderr, "error: missing argument for '%s'\n", name); return 1; } len = MultiByteToWideChar(CP_UTF8, 0, **argv, -1, NULL, 0); *value = HeapAlloc(GetProcessHeap(), 0, len*sizeof(WCHAR)); if (!*value) { fprintf(stderr, "error: memory allocation error\n"); (*argv)++; return 1; } MultiByteToWideChar(CP_UTF8, 0, **argv, -1, *value, len); (*argv)++; return 0; } static int parse_arguments(int argc, const char** argv) { int rc; const char* arg; char* p; rc=0; argv++; while (*argv) { arg=*argv++; if (*arg!='-') { if (g_action != ACTION_INVALID) { fprintf(stderr, "error: '%s' an action has already been specified\n", arg); rc=1; } else if (strcmp(arg, "click") == 0 || strcmp(arg, "lclick") == 0) { g_action = ACTION_LCLICK; } else if (strcmp(arg, "mclick") == 0) { g_action = ACTION_MCLICK; } else if (strcmp(arg, "rclick") == 0) { g_action = ACTION_RCLICK; } else if (strncmp(arg, "button", 6) == 0) { int button; char extra='\0'; int r=sscanf(arg, "button%d%c", &button, &extra); /* We should always get r==1 but due to a bug in Wine's * msvcrt.dll implementation (at least up to 20050127) * we may also get r==2 and extra=='\0'. */ if (r!=1 && (r!=2 || extra!='\0')) { fprintf(stderr, "error: invalid argument '%s' for '%s'\n", *argv, arg); rc=1; } else if (button<1 || button>3) { fprintf(stderr, "error: unknown button '%s'\n", arg); rc=1; } else { /* Just to remain compatible with the enum */ g_action=button+ACTION_LCLICK-1; } } else if (strcmp(arg, "find") == 0) { g_action = ACTION_FIND; } else { fprintf(stderr, "error: unknown action '%s'\n", arg); rc=1; } } else if (strcmp(arg, "--winclass") == 0) { rc|=arg_get_utf8(&argv, arg, &g_window_class); } else if (strcmp(arg, "--wintitle") == 0) { rc|=arg_get_utf8(&argv,arg, &g_window_title); } else if (strcmp(arg, "--ctrlclass") == 0) { rc|=arg_get_utf8(&argv, arg, &g_control_class); } else if (strcmp(arg, "--ctrlid") == 0) { rc|=arg_get_long(&argv, arg, &g_control_id); } else if (strcmp(arg, "--ctrlcaption") == 0) { rc|=arg_get_utf8(&argv, arg, &g_control_caption); } else if (strcmp(arg, "--position") == 0) { if (!*argv) { fprintf(stderr, "error: missing argument for '%s'\n", arg); rc=1; } else { char extra='\0'; int r=sscanf(*argv, "%ldx%ld%c", &g_x, &g_y, &extra); /* We should always get r==2 but due to a bug in Wine's * msvcrt.dll implementation (at least up to 20050127) * we may also get r==3 and extra=='\0'. */ if (r!=2 && (r!=3 || extra!='\0')) { fprintf(stderr, "error: invalid argument '%s' for '%s'\n", *argv, arg); rc=1; } argv++; } } else if (strcmp(arg, "--dragto") == 0) { if (!*argv) { fprintf(stderr, "error: missing argument for '%s'\n", arg); rc=1; } else { char extra='\0'; int r=sscanf(*argv, "%ldx%ld%c", &g_dragto_x, &g_dragto_y, &extra); /* We should always get r==2 but due to a bug in Wine's * * msvcrt.dll implementation (at least up to 20050127) * * we may also get r==3 and extra=='\0'. * */ if (r!=2 && (r!=3 || extra!='\0')) { fprintf(stderr, "error: invalid argument '%s' for '%s'\n", *argv, arg); rc=1; } argv++; } } else if (strcmp(arg, "--allow-disabled") == 0) { g_disabled = 1; } else if (strcmp(arg, "--delay") == 0) { rc|=arg_get_long(&argv, arg, &g_delay); } else if (strcmp(arg, "--timeout") == 0) { rc|=arg_get_long(&argv, arg, &g_timeout); } else if (strcmp(arg, "--repeat") == 0) { rc|=arg_get_long(&argv, arg, &g_repeat); } else if (strcmp(arg, "--untildeath") == 0) { g_untildeath=1; } else if (strcmp(arg, "--help") == 0) { rc=2; } } if (g_action == ACTION_INVALID) { fprintf(stderr, "error: you must specify an action type\n"); rc=1; } else { /* Adjust the default delay and repeat parameters depending on * the operating mode so less needs to be specified on the command * line, and so we can assume them to be set right. */ if (g_action == ACTION_FIND) g_delay=0; if (!g_untildeath) g_repeat=0; else if (!g_repeat) g_repeat=DEFAULT_REPEAT; } if (!g_window_class) { fprintf(stderr, "error: you must specify a --winclass parameter\n"); rc=1; } if (!g_window_title) { fprintf(stderr, "error: you must specify a --wintitle parameter\n"); rc=1; } if (g_control_class) { if (!g_control_id && !g_control_caption) { fprintf(stderr, "error: you must specify either the control id or its caption\n"); rc=1; } } /*------------------------------------------------------------------------ ** Process environment variables **----------------------------------------------------------------------*/ p = getenv("CXTEST_TIME_MULTIPLE"); if (p) { float g_multiple = atof(p); g_delay = (long) (((float) g_delay) * g_multiple); g_timeout = (long) (((float) g_timeout) * g_multiple); } return rc; } int main(int argc, const char** argv) { MSG msg; init_debug(); status = parse_arguments(argc, argv); if (status) { if (status == 2) usage(); else fprintf(stderr, "Issue %s --help for usage.\n", *argv); return RC_INVALID_ARGUMENTS; } cxlog("Entering message loop. action=%d\n", g_action); if (g_timeout>0) SetTimer(NULL, 0, g_timeout, TimeoutProc); timer_id=SetTimer(NULL, 0, 100, FindWindowProc); status=RC_RUNNING; while (status==RC_RUNNING && GetMessage(&msg, NULL, 0, 0)!=0) { TranslateMessage(&msg); DispatchMessage(&msg); } return status; }