mirror of
https://github.com/go-vgo/robotgo.git
synced 2025-06-01 14:43:55 +00:00
Merge pull request #154 from go-vgo/dev
add GetBounds func and update code style
This commit is contained in:
commit
1ec411fd39
108
key/keycode_c.h
108
key/keycode_c.h
@ -62,74 +62,74 @@ struct XSpecialCharacterMapping XSpecialCharacterTable[] = {
|
||||
|
||||
#endif
|
||||
|
||||
MMKeyCode keyCodeForChar(const char c)
|
||||
{
|
||||
#if defined(IS_MACOSX)
|
||||
/* OS X does not appear to have a built-in function for this, so instead we
|
||||
* have to write our own. */
|
||||
static CFMutableDictionaryRef charToCodeDict = NULL;
|
||||
CGKeyCode code;
|
||||
UniChar character = c;
|
||||
CFStringRef charStr = NULL;
|
||||
MMKeyCode keyCodeForChar(const char c){
|
||||
#if defined(IS_MACOSX)
|
||||
/* OS X does not appear to have a built-in function for this, so instead we
|
||||
* have to write our own. */
|
||||
static CFMutableDictionaryRef charToCodeDict = NULL;
|
||||
CGKeyCode code;
|
||||
UniChar character = c;
|
||||
CFStringRef charStr = NULL;
|
||||
|
||||
/* Generate table of keycodes and characters. */
|
||||
if (charToCodeDict == NULL) {
|
||||
size_t i;
|
||||
charToCodeDict = CFDictionaryCreateMutable(kCFAllocatorDefault,
|
||||
128,
|
||||
&kCFCopyStringDictionaryKeyCallBacks,
|
||||
NULL);
|
||||
if (charToCodeDict == NULL) return UINT16_MAX;
|
||||
/* Generate table of keycodes and characters. */
|
||||
if (charToCodeDict == NULL) {
|
||||
size_t i;
|
||||
charToCodeDict = CFDictionaryCreateMutable(kCFAllocatorDefault,
|
||||
128,
|
||||
&kCFCopyStringDictionaryKeyCallBacks,
|
||||
NULL);
|
||||
if (charToCodeDict == NULL) return UINT16_MAX;
|
||||
|
||||
/* Loop through every keycode (0 - 127) to find its current mapping. */
|
||||
for (i = 0; i < 128; ++i) {
|
||||
CFStringRef string = createStringForKey((CGKeyCode)i);
|
||||
if (string != NULL) {
|
||||
CFDictionaryAddValue(charToCodeDict, string, (const void *)i);
|
||||
CFRelease(string);
|
||||
/* Loop through every keycode (0 - 127) to find its current mapping. */
|
||||
for (i = 0; i < 128; ++i) {
|
||||
CFStringRef string = createStringForKey((CGKeyCode)i);
|
||||
if (string != NULL) {
|
||||
CFDictionaryAddValue(charToCodeDict, string, (const void *)i);
|
||||
CFRelease(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
charStr = CFStringCreateWithCharacters(kCFAllocatorDefault, &character, 1);
|
||||
charStr = CFStringCreateWithCharacters(kCFAllocatorDefault, &character, 1);
|
||||
|
||||
/* Our values may be NULL (0), so we need to use this function. */
|
||||
if (!CFDictionaryGetValueIfPresent(charToCodeDict, charStr,
|
||||
(const void **)&code)) {
|
||||
code = UINT16_MAX; /* Error */
|
||||
}
|
||||
/* Our values may be NULL (0), so we need to use this function. */
|
||||
if (!CFDictionaryGetValueIfPresent(charToCodeDict, charStr,
|
||||
(const void **)&code)) {
|
||||
code = UINT16_MAX; /* Error */
|
||||
}
|
||||
|
||||
CFRelease(charStr);
|
||||
return (MMKeyCode)code;
|
||||
#elif defined(IS_WINDOWS)
|
||||
return VkKeyScan(c);
|
||||
#elif defined(USE_X11)
|
||||
MMKeyCode code;
|
||||
CFRelease(charStr);
|
||||
return (MMKeyCode)code;
|
||||
#elif defined(IS_WINDOWS)
|
||||
return VkKeyScan(c);
|
||||
#elif defined(USE_X11)
|
||||
MMKeyCode code;
|
||||
|
||||
char buf[2];
|
||||
buf[0] = c;
|
||||
buf[1] = '\0';
|
||||
char buf[2];
|
||||
buf[0] = c;
|
||||
buf[1] = '\0';
|
||||
|
||||
code = XStringToKeysym(buf);
|
||||
if (code == NoSymbol) {
|
||||
/* Some special keys are apparently not handled properly by
|
||||
* XStringToKeysym() on some systems, so search for them instead in our
|
||||
* mapping table. */
|
||||
size_t i;
|
||||
const size_t specialCharacterCount =
|
||||
sizeof(XSpecialCharacterTable) / sizeof(XSpecialCharacterTable[0]);
|
||||
for (i = 0; i < specialCharacterCount; ++i) {
|
||||
if (c == XSpecialCharacterTable[i].name) {
|
||||
code = XSpecialCharacterTable[i].code;
|
||||
break;
|
||||
code = XStringToKeysym(buf);
|
||||
if (code == NoSymbol) {
|
||||
/* Some special keys are apparently not handled properly by
|
||||
* XStringToKeysym() on some systems, so search for them instead in our
|
||||
* mapping table. */
|
||||
size_t i;
|
||||
const size_t specialCharacterCount =
|
||||
sizeof(XSpecialCharacterTable) / sizeof(XSpecialCharacterTable[0]);
|
||||
for (i = 0; i < specialCharacterCount; ++i) {
|
||||
if (c == XSpecialCharacterTable[i].name) {
|
||||
code = XSpecialCharacterTable[i].code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
#endif
|
||||
return code;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
|
||||
CFStringRef createStringForKey(CGKeyCode keyCode){
|
||||
|
@ -29,8 +29,7 @@
|
||||
#endif
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
static io_connect_t _getAuxiliaryKeyDriver(void)
|
||||
{
|
||||
static io_connect_t _getAuxiliaryKeyDriver(void){
|
||||
static mach_port_t sEventDrvrRef = 0;
|
||||
mach_port_t masterPort, service, iter;
|
||||
kern_return_t kr;
|
||||
@ -52,13 +51,11 @@ static io_connect_t _getAuxiliaryKeyDriver(void)
|
||||
#endif
|
||||
|
||||
#if defined(IS_WINDOWS)
|
||||
void win32KeyEvent(int key, MMKeyFlags flags)
|
||||
{
|
||||
void win32KeyEvent(int key, MMKeyFlags flags){
|
||||
int scan = MapVirtualKey(key & 0xff, MAPVK_VK_TO_VSC);
|
||||
|
||||
/* Set the scan code for extended keys */
|
||||
switch (key)
|
||||
{
|
||||
switch (key){
|
||||
case VK_RCONTROL:
|
||||
case VK_SNAPSHOT: /* Print Screen */
|
||||
case VK_RMENU: /* Right Alt / Alt Gr */
|
||||
@ -106,8 +103,7 @@ void win32KeyEvent(int key, MMKeyFlags flags)
|
||||
}
|
||||
#endif
|
||||
|
||||
void toggleKeyCode(MMKeyCode code, const bool down, MMKeyFlags flags)
|
||||
{
|
||||
void toggleKeyCode(MMKeyCode code, const bool down, MMKeyFlags flags){
|
||||
#if defined(IS_MACOSX)
|
||||
/* The media keys all have 1000 added to them to help us detect them. */
|
||||
if (code >= 1000) {
|
||||
@ -156,14 +152,12 @@ void toggleKeyCode(MMKeyCode code, const bool down, MMKeyFlags flags)
|
||||
#endif
|
||||
}
|
||||
|
||||
void tapKeyCode(MMKeyCode code, MMKeyFlags flags)
|
||||
{
|
||||
void tapKeyCode(MMKeyCode code, MMKeyFlags flags){
|
||||
toggleKeyCode(code, true, flags);
|
||||
toggleKeyCode(code, false, flags);
|
||||
}
|
||||
|
||||
void toggleKey(char c, const bool down, MMKeyFlags flags)
|
||||
{
|
||||
void toggleKey(char c, const bool down, MMKeyFlags flags){
|
||||
MMKeyCode keyCode = keyCodeForChar(c);
|
||||
|
||||
//Prevent unused variable warning for Mac and Linux.
|
||||
@ -185,15 +179,13 @@ void toggleKey(char c, const bool down, MMKeyFlags flags)
|
||||
toggleKeyCode(keyCode, down, flags);
|
||||
}
|
||||
|
||||
void tapKey(char c, MMKeyFlags flags)
|
||||
{
|
||||
void tapKey(char c, MMKeyFlags flags){
|
||||
toggleKey(c, true, flags);
|
||||
toggleKey(c, false, flags);
|
||||
}
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
void toggleUnicode(UniChar ch, const bool down)
|
||||
{
|
||||
void toggleUnicode(UniChar ch, const bool down){
|
||||
/* This function relies on the convenient
|
||||
* CGEventKeyboardSetUnicodeString(), which allows us to not have to
|
||||
* convert characters to a keycode, but does not support adding modifier
|
||||
|
@ -1398,6 +1398,12 @@ func GetPID() int32 {
|
||||
return int32(pid)
|
||||
}
|
||||
|
||||
// internalGetBounds get the window bounds
|
||||
func internalGetBounds(pid int32, hwnd int) (int, int, int, int) {
|
||||
bounds := C.get_bounds(C.uintptr(pid), C.uintptr(hwnd))
|
||||
return int(bounds.X), int(bounds.Y), int(bounds.W), int(bounds.H)
|
||||
}
|
||||
|
||||
// Pids get the all process id
|
||||
func Pids() ([]int32, error) {
|
||||
var ret []int32
|
||||
|
@ -12,6 +12,16 @@
|
||||
|
||||
package robotgo
|
||||
|
||||
// GetBounds get the window bounds
|
||||
func GetBounds(pid int32, args ...int) (int, int, int, int) {
|
||||
var hwnd int
|
||||
if len(args) > 0 {
|
||||
hwnd = args[0]
|
||||
}
|
||||
|
||||
return internalGetBounds(pid, hwnd)
|
||||
}
|
||||
|
||||
// ActivePID active the window by PID,
|
||||
// If args[0] > 0 on the Windows platform via a window handle to active
|
||||
func ActivePID(pid int32, args ...int) error {
|
||||
|
@ -23,6 +23,24 @@ import (
|
||||
|
||||
var xu *xgbutil.XUtil
|
||||
|
||||
// GetBounds get the window bounds
|
||||
func GetBounds(pid int32, args ...int) (int, int, int, int) {
|
||||
var hwnd int
|
||||
if len(args) > 0 {
|
||||
hwnd = args[0]
|
||||
|
||||
return internalGetBounds(pid, hwnd)
|
||||
}
|
||||
|
||||
xid, err := GetXId(xu, pid)
|
||||
if err != nil {
|
||||
log.Println("GetXidFromPid errors is: ", err)
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
|
||||
return internalGetBounds(int32(xid), hwnd)
|
||||
}
|
||||
|
||||
// ActivePIDC active the window by PID,
|
||||
// If args[0] > 0 on the unix platform via a xid to active
|
||||
func ActivePIDC(pid int32, args ...int) {
|
||||
@ -34,18 +52,9 @@ func ActivePIDC(pid int32, args ...int) {
|
||||
return
|
||||
}
|
||||
|
||||
if xu == nil {
|
||||
var err error
|
||||
xu, err = xgbutil.NewConn()
|
||||
if err != nil {
|
||||
log.Println("xgbutil.NewConn errors is: ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
xid, err := getXidFromPid(xu, pid)
|
||||
xid, err := GetXId(xu, pid)
|
||||
if err != nil {
|
||||
log.Println("getXidFromPid errors is: ", err)
|
||||
log.Println("GetXidFromPid errors is: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -73,7 +82,8 @@ func ActivePID(pid int32, args ...int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
xid, err := getXidFromPid(xu, pid)
|
||||
// get xid from pid
|
||||
xid, err := GetXidFromPid(xu, pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -86,7 +96,23 @@ func ActivePID(pid int32, args ...int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getXidFromPid(xu *xgbutil.XUtil, pid int32) (xproto.Window, error) {
|
||||
// GetXId get the xid
|
||||
func GetXId(xu *xgbutil.XUtil, pid int32) (xproto.Window, error) {
|
||||
if xu == nil {
|
||||
var err error
|
||||
xu, err = xgbutil.NewConn()
|
||||
if err != nil {
|
||||
// log.Println("xgbutil.NewConn errors is: ", err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
xid, err := GetXidFromPid(xu, pid)
|
||||
return xid, err
|
||||
}
|
||||
|
||||
// GetXidFromPid get the xide from pid
|
||||
func GetXidFromPid(xu *xgbutil.XUtil, pid int32) (xproto.Window, error) {
|
||||
windows, err := ewmh.ClientListGet(xu)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -102,5 +128,5 @@ func getXidFromPid(xu *xgbutil.XUtil, pid int32) (xproto.Window, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("failed to find a window with a matching pid")
|
||||
return 0, errors.New("failed to find a window with a matching pid.")
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "alert_c.h"
|
||||
#include "window.h"
|
||||
#include "win32.h"
|
||||
#include "win_sys.h"
|
||||
|
||||
int show_alert(const char *title, const char *msg,
|
||||
const char *defaultButton, const char *cancelButton){
|
||||
|
33
window/pub.h
33
window/pub.h
@ -26,6 +26,15 @@ typedef struct _MData MData;
|
||||
|
||||
MData mData;
|
||||
|
||||
struct _Bounds{
|
||||
int32 X; // Top left X coordinate
|
||||
int32 Y; // Top left Y coordinate
|
||||
int32 W; // Total bounds width
|
||||
int32 H; // Total bounds height
|
||||
};
|
||||
|
||||
typedef struct _Bounds Bounds;
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
|
||||
static Boolean(*gAXIsProcessTrustedWithOptions) (CFDictionaryRef);
|
||||
@ -255,6 +264,30 @@ MData mData;
|
||||
}
|
||||
}
|
||||
|
||||
static Bounds GetFrame(MData win){
|
||||
Bounds frame;
|
||||
// Retrieve frame bounds
|
||||
if (WM_EXTENTS != None) {
|
||||
long* result; uint32 nItems = 0;
|
||||
// Get the window extents property
|
||||
result = (long*) GetWindowProperty(win, WM_EXTENTS, &nItems);
|
||||
|
||||
// Verify the results
|
||||
if (result != NULL) {
|
||||
if (nItems == 4) {
|
||||
frame.X = (int32) result[0];
|
||||
frame.Y = (int32) result[2];
|
||||
frame.W = (int32) result[0] + (int32) result[1];
|
||||
frame.H = (int32) result[2] + (int32) result[3];
|
||||
}
|
||||
|
||||
XFree(result);
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
//
|
||||
|
162
window/win_sys.h
Normal file
162
window/win_sys.h
Normal file
@ -0,0 +1,162 @@
|
||||
// #include "../base/os.h"
|
||||
Bounds get_client(uintptr pid, uintptr isHwnd);
|
||||
|
||||
Bounds get_bounds(uintptr pid, uintptr isHwnd){
|
||||
// Check if the window is valid
|
||||
Bounds bounds;
|
||||
if (!IsValid()) { return bounds; }
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
|
||||
// Bounds bounds;
|
||||
AXValueRef axp = NULL;
|
||||
AXValueRef axs = NULL;
|
||||
AXUIElementRef AxID = AXUIElementCreateApplication(pid);
|
||||
|
||||
// Determine the current point of the window
|
||||
if (AXUIElementCopyAttributeValue(AxID,
|
||||
kAXPositionAttribute, (CFTypeRef*) &axp)
|
||||
!= kAXErrorSuccess || axp == NULL){
|
||||
goto exit;
|
||||
}
|
||||
|
||||
// Determine the current size of the window
|
||||
if (AXUIElementCopyAttributeValue(AxID,
|
||||
kAXSizeAttribute, (CFTypeRef*) &axs)
|
||||
!= kAXErrorSuccess || axs == NULL){
|
||||
goto exit;
|
||||
}
|
||||
|
||||
CGPoint p; CGSize s;
|
||||
// Attempt to convert both values into atomic types
|
||||
if (AXValueGetValue(axp, kAXValueCGPointType, &p) &&
|
||||
AXValueGetValue(axs, kAXValueCGSizeType, &s)){
|
||||
bounds.X = p.x;
|
||||
bounds.Y = p.y;
|
||||
bounds.W = s.width;
|
||||
bounds.H = s.height;
|
||||
}
|
||||
|
||||
exit:
|
||||
if (axp != NULL) { CFRelease(axp); }
|
||||
if (axs != NULL) { CFRelease(axs); }
|
||||
|
||||
return bounds;
|
||||
|
||||
#elif defined(USE_X11)
|
||||
|
||||
// Ignore X errors
|
||||
XDismissErrors();
|
||||
MData win;
|
||||
win.XWin = (Window)pid;
|
||||
|
||||
Bounds client = get_client(pid, isHwnd);
|
||||
Bounds frame = GetFrame(win);
|
||||
|
||||
bounds.X = client.X - frame.X;
|
||||
bounds.Y = client.Y - frame.Y;
|
||||
bounds.W = client.W + frame.W;
|
||||
bounds.H = client.H + frame.H;
|
||||
|
||||
return bounds;
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
HWND hwnd;
|
||||
if (isHwnd == 0) {
|
||||
hwnd= GetHwndByPId(pid);
|
||||
} else {
|
||||
hwnd = (HWND)pid;
|
||||
}
|
||||
|
||||
RECT rect = { 0 };
|
||||
GetWindowRect(hwnd, &rect);
|
||||
|
||||
bounds.X = rect.left;
|
||||
bounds.Y = rect.top;
|
||||
bounds.W = rect.right - rect.left;
|
||||
bounds.H = rect.bottom - rect.top;
|
||||
|
||||
return bounds;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
Bounds get_client(uintptr pid, uintptr isHwnd){
|
||||
// Check if the window is valid
|
||||
Bounds bounds;
|
||||
if (!IsValid()) { return bounds; }
|
||||
|
||||
#if defined(IS_MACOSX)
|
||||
|
||||
return get_bounds(pid, isHwnd);
|
||||
|
||||
#elif defined(USE_X11)
|
||||
|
||||
Display *rDisplay = XOpenDisplay(NULL);
|
||||
|
||||
// Ignore X errors
|
||||
XDismissErrors();
|
||||
MData win;
|
||||
win.XWin = (Window)pid;
|
||||
|
||||
// Property variables
|
||||
Window root, parent;
|
||||
Window* children;
|
||||
unsigned int count;
|
||||
int32 x = 0, y = 0;
|
||||
|
||||
// Check if the window is the root
|
||||
XQueryTree(rDisplay, win.XWin,
|
||||
&root, &parent, &children, &count);
|
||||
if (children) { XFree(children); }
|
||||
|
||||
// Retrieve window attributes
|
||||
XWindowAttributes attr = { 0 };
|
||||
XGetWindowAttributes(rDisplay, win.XWin, &attr);
|
||||
|
||||
// Coordinates must be translated
|
||||
if (parent != attr.root){
|
||||
XTranslateCoordinates(rDisplay, win.XWin, attr.root, attr.x,
|
||||
attr.y, &x, &y, &parent);
|
||||
}
|
||||
// Coordinates can be left alone
|
||||
else {
|
||||
x = attr.x;
|
||||
y = attr.y;
|
||||
}
|
||||
|
||||
// Return resulting window bounds
|
||||
bounds.X = x;
|
||||
bounds.Y = y;
|
||||
bounds.W = attr.width;
|
||||
bounds.H = attr.height;
|
||||
return bounds;
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
HWND hwnd;
|
||||
if (isHwnd == 0) {
|
||||
hwnd= GetHwndByPId(pid);
|
||||
} else {
|
||||
hwnd = (HWND)pid;
|
||||
}
|
||||
|
||||
|
||||
RECT rect = { 0 };
|
||||
GetClientRect(hwnd, &rect);
|
||||
|
||||
POINT point;
|
||||
point.x = rect.left;
|
||||
point.y = rect.top;
|
||||
|
||||
// Convert the client point to screen
|
||||
ClientToScreen(hwnd, &point);
|
||||
|
||||
bounds.X = point.x;
|
||||
bounds.Y = point.y;
|
||||
bounds.W = rect.right - rect.left;
|
||||
bounds.H = rect.bottom - rect.top;
|
||||
|
||||
return bounds;
|
||||
|
||||
#endif
|
||||
}
|
@ -170,8 +170,8 @@ bool setHandle(uintptr handle){
|
||||
mData.AxID = 0;
|
||||
|
||||
if (handle == 0){
|
||||
return 0;
|
||||
// return true;
|
||||
// return 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve the window element
|
||||
@ -275,7 +275,7 @@ bool IsMinimized(void){
|
||||
|
||||
// Ignore X errors
|
||||
// XDismissErrors();
|
||||
// return GetState (mData.XWin, STATE_MINIMIZE);
|
||||
// return GetState(mData.XWin, STATE_MINIMIZE);
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
|
||||
@ -298,7 +298,7 @@ bool IsMaximized(void){
|
||||
|
||||
// Ignore X errors
|
||||
// XDismissErrors();
|
||||
// return GetState (mData.XWin, STATE_MAXIMIZE);
|
||||
// return GetState(mData.XWin, STATE_MAXIMIZE);
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
|
||||
@ -447,7 +447,7 @@ MData GetActive(void){
|
||||
|
||||
// Get the current active window
|
||||
result.XWin = XDefaultRootWindow(rDisplay);
|
||||
void* active = GetWindowProperty(result,WM_ACTIVE,NULL);
|
||||
void* active = GetWindowProperty(result, WM_ACTIVE, NULL);
|
||||
|
||||
// Check result value
|
||||
if (active != NULL) {
|
||||
@ -607,8 +607,9 @@ char *GetTitle(){
|
||||
|
||||
#elif defined(IS_WINDOWS)
|
||||
|
||||
return GetWindowText
|
||||
(mData.HWnd, mData.Title, 512) > 0 ? mData.Title : "";
|
||||
return GetWindowText(
|
||||
mData.HWnd, mData.Title, 512) > 0 ?
|
||||
mData.Title : "";
|
||||
// return GetWindowText
|
||||
// (mData.HWnd, name, 512) > 0 ?
|
||||
// _UTF8Encode(name) : "null";
|
||||
@ -624,10 +625,9 @@ int32 WGetPID(void){
|
||||
|
||||
pid_t pid = 0;
|
||||
// Attempt to retrieve the window pid
|
||||
if (AXUIElementGetPid(mData.AxID, &pid)
|
||||
== kAXErrorSuccess) {
|
||||
return pid;
|
||||
}
|
||||
if (AXUIElementGetPid(mData.AxID, &pid)== kAXErrorSuccess) {
|
||||
return pid;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user