From be395fe2b17e561d141e16d35b335f16c08e221b Mon Sep 17 00:00:00 2001 From: lilleo Date: Sun, 19 Jun 2022 10:53:42 +0300 Subject: [PATCH] idk --- autoscroll.py | 140 +++++++++++++++++++++------------------- autoscroll_no_icon.py | 54 +++++++++++++++- pyXCursor.py | 147 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+), 67 deletions(-) create mode 100644 pyXCursor.py diff --git a/autoscroll.py b/autoscroll.py index 4cb3b68..3c7a0f5 100644 --- a/autoscroll.py +++ b/autoscroll.py @@ -1,5 +1,5 @@ -from functools import partial -from queue import Queue +#!/home/kusti420/Downloads/git/linux-autoscroll/.autoscroll/bin/python3 + from pynput.mouse import Button, Controller, Listener from threading import Event, Thread from time import sleep @@ -8,14 +8,62 @@ from PyQt5.QtSvg import QSvgWidget from PyQt5.QtGui import QPixmap from pathlib import Path -import subprocess import sys +import pyXCursor +import tkinter as tk +from tkinter import ttk +import os +import hashlib + +CURSOR_REFRENCE_IMAGE = "hand_cursor_refrence.png" +os.chdir(os.path.dirname(os.path.realpath(__file__))) + + +class CursorInfo(): + def __init__(self) -> None: + self.cursor_refrence_image_exists = CURSOR_REFRENCE_IMAGE in os.listdir(os.path.dirname(os.path.realpath(__file__))) + self.cursor = pyXCursor.Xcursor() + self.imgarray = self.cursor.getCursorImageArrayFast() + self.root = None + self.sense() + + def sense(self) -> None: + self.imgarray = self.cursor.getCursorImageArrayFast() + if not self.cursor_refrence_image_exists: + self.root = tk.Tk() + ttk.Button(self.root, text = "Click here to calibrate.\nKeep your cursor still on this button.",\ + width = 40, command = self.save_cursor_image, cursor = "hand1").pack(padx = 50, pady = 50) + self.root.mainloop() + + def save_cursor_image(self, filename = CURSOR_REFRENCE_IMAGE) -> None: + if not self.cursor_refrence_image_exists and filename == CURSOR_REFRENCE_IMAGE or filename != CURSOR_REFRENCE_IMAGE: + # print(f"saving '{filename}' to '{os.getcwd()}'") + self.cursor.saveImage(self.imgarray, filename) + # print("saved") + if filename == CURSOR_REFRENCE_IMAGE: + self.cursor_refrence_image_exists = True + self.root.destroy() + + def is_hand_cursor(self) -> bool: + with open("hand_cursor_refrence.png", "rb") as f: + cursor_image_hash = hashlib.md5(f.read()).hexdigest() + f.close() + with open("cursor_image.png", "rb") as f: + cursor_image_hash_2 = hashlib.md5(f.read()).hexdigest() + f.close() + if cursor_image_hash != cursor_image_hash_2: + print("cursor image hash mismatch") + return False + else: + print("cursor image hash match") + return True +cursorInfo = CursorInfo() class AutoscrollIconSvg(QSvgWidget): scroll_mode_entered = pyqtSignal() scroll_mode_exited = pyqtSignal() - + def __init__(self, path, size): super().__init__(path) self.size = size @@ -25,18 +73,17 @@ def __init__(self, path, size): self.setAttribute(Qt.WA_TranslucentBackground) self.scroll_mode_entered.connect(self.show) self.scroll_mode_exited.connect(self.close) - + def show(self): x = self.pos[0] - self.size // 2 y = self.pos[1] - self.size // 2 self.move(x, y) super().show() - class AutoscrollIconRaster(QLabel): scroll_mode_entered = pyqtSignal() scroll_mode_exited = pyqtSignal() - + def __init__(self, path, size): super().__init__() self.size = size @@ -47,15 +94,14 @@ def __init__(self, path, size): self.setAttribute(Qt.WA_TranslucentBackground) self.scroll_mode_entered.connect(self.show) self.scroll_mode_exited.connect(self.close) - + def show(self): x = self.pos[0] - self.size // 2 y = self.pos[1] - self.size // 2 self.move(x, y) super().show() - -class Autoscroll: +class Autoscroll(): def __init__(self): # modify this to adjust the speed of scrolling self.DELAY = 5 @@ -65,14 +111,6 @@ def __init__(self): self.BUTTON_STOP = Button.middle # modify this to change the size (in px) of the area below and above the starting point where scrolling is paused self.DEAD_AREA = 30 - # modify this to change the time you have to hold BUTTON_START for in order to enter the scroll mode - self.TRIGGER_DELAY = 0 - # set this to True if you want the clipboard to be cleared before entering the scroll mode - # applicable only if you are using Button.middle for BUTTON_START or BUTTON_STOP - # requires xsel - self.CLEAR_CLIPBOARD = False - # set this to True if you want to autoscroll only while BUTTON_START is held down - self.HOLD_MODE = False # modify this to change the scroll mode icon # supported formats: svg, png, jpg, jpeg, gif, bmp, pbm, pgm, ppm, xbm, xpm # the path MUST be absolute @@ -80,19 +118,18 @@ def __init__(self): # modify this to change the size (in px) of the icon # note that only svg images can be resized without loss of quality self.ICON_SIZE = 30 - + if self.ICON_PATH[-4:] == ".svg": self.icon = AutoscrollIconSvg(self.ICON_PATH, self.ICON_SIZE) else: self.icon = AutoscrollIconRaster(self.ICON_PATH, self.ICON_SIZE) - + self.mouse = Controller() self.scroll_mode = Event() - self.queue = Queue() - self.cancelled = Event() self.listener = Listener(on_move=self.on_move, on_click=self.on_click) + self.listener.start() self.looper = Thread(target=self.loop) - self.consumer = Thread(target=self.consume) + self.looper.start() def on_move(self, x, y): if self.scroll_mode.is_set(): @@ -109,55 +146,26 @@ def on_move(self, x, y): self.interval = self.DELAY / (abs(delta) - self.DEAD_AREA) def on_click(self, x, y, button, pressed): - if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set(): - self.queue.put(partial(self.enter_scroll_mode, x, y)) - self.started = True - elif button == self.BUTTON_START and not pressed and self.started: - self.cancelled.set() - self.started = False - if self.HOLD_MODE: - self.queue.put(partial(self.exit_scroll_mode)) - elif button == self.BUTTON_STOP and not pressed and self.scroll_mode.is_set(): - self.queue.put(partial(self.exit_scroll_mode)) - - def enter_scroll_mode(self, x, y): - if self.CLEAR_CLIPBOARD: - subprocess.run(["xsel", "-c"]) - self.icon.pos = (x, y) - self.direction = 0 - self.interval = 0.5 - self.scroll_mode.set() - self.icon.scroll_mode_entered.emit() - - def exit_scroll_mode(self): - self.scroll_mode.clear() - self.icon.scroll_mode_exited.emit() - - def consume(self): - while True: - f = self.queue.get(block=True) - if f.func == self.enter_scroll_mode: - self.cancelled.clear() - self.cancelled.wait(self.TRIGGER_DELAY) - if not self.cancelled.is_set(): - f() - elif f.func == self.exit_scroll_mode: - f() - + cursorInfo.sense() + cursorInfo.save_cursor_image("cursor_image.png") + isHand = cursorInfo.is_hand_cursor() + if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set() and not isHand: + self.icon.pos = (x, y) + self.direction = 0 + self.interval = 0.5 + self.scroll_mode.set() + self.icon.scroll_mode_entered.emit() + elif button == self.BUTTON_STOP and pressed and self.scroll_mode.is_set(): + self.scroll_mode.clear() + self.icon.scroll_mode_exited.emit() + def loop(self): while True: self.scroll_mode.wait() sleep(self.interval) self.mouse.scroll(0, self.direction) - def start(self): - self.consumer.start() - self.listener.start() - self.looper.start() - - app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) autoscroll = Autoscroll() -autoscroll.start() -sys.exit(app.exec()) \ No newline at end of file +sys.exit(app.exec()) diff --git a/autoscroll_no_icon.py b/autoscroll_no_icon.py index b05d402..4ed5002 100644 --- a/autoscroll_no_icon.py +++ b/autoscroll_no_icon.py @@ -4,7 +4,56 @@ from functools import partial from queue import Queue import subprocess +import pyXCursor +import tkinter as tk +from tkinter import ttk +import os +import hashlib +CURSOR_REFRENCE_IMAGE = "hand_cursor_refrence.png" +os.chdir(os.path.dirname(os.path.realpath(__file__))) + + +class CursorInfo(): + def __init__(self) -> None: + self.cursor_refrence_image_exists = CURSOR_REFRENCE_IMAGE in os.listdir(os.path.dirname(os.path.realpath(__file__))) + self.cursor = pyXCursor.Xcursor() + self.imgarray = self.cursor.getCursorImageArrayFast() + self.root = None + self.sense() + + def sense(self) -> None: + self.imgarray = self.cursor.getCursorImageArrayFast() + if not self.cursor_refrence_image_exists: + self.root = tk.Tk() + ttk.Button(self.root, text = "Click here to calibrate.\nKeep your cursor still on this button.",\ + width = 40, command = self.save_cursor_image, cursor = "hand1").pack(padx = 50, pady = 50) + self.root.mainloop() + + def save_cursor_image(self, filename = CURSOR_REFRENCE_IMAGE) -> None: + if not self.cursor_refrence_image_exists and filename == CURSOR_REFRENCE_IMAGE or filename != CURSOR_REFRENCE_IMAGE: + # print(f"saving '{filename}' to '{os.getcwd()}'") + self.cursor.saveImage(self.imgarray, filename) + # print("saved") + if filename == CURSOR_REFRENCE_IMAGE: + self.cursor_refrence_image_exists = True + self.root.destroy() + + def is_hand_cursor(self) -> bool: + with open("hand_cursor_refrence.png", "rb") as f: + cursor_image_hash = hashlib.md5(f.read()).hexdigest() + f.close() + with open("cursor_image.png", "rb") as f: + cursor_image_hash_2 = hashlib.md5(f.read()).hexdigest() + f.close() + if cursor_image_hash != cursor_image_hash_2: + print("cursor image hash mismatch") + return False + else: + print("cursor image hash match") + return True + +cursorInfo = CursorInfo() class Autoscroll: def __init__(self): @@ -47,7 +96,10 @@ def on_move(self, x, y): self.interval = self.DELAY / (abs(delta) - self.DEAD_AREA) def on_click(self, x, y, button, pressed): - if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set(): + cursorInfo.sense() + cursorInfo.save_cursor_image("cursor_image.png") + isHand = cursorInfo.is_hand_cursor() + if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set() and not isHand: self.queue.put(partial(self.enter_scroll_mode, x, y)) self.started = True elif button == self.BUTTON_START and not pressed and self.started: diff --git a/pyXCursor.py b/pyXCursor.py new file mode 100644 index 0000000..90c8e17 --- /dev/null +++ b/pyXCursor.py @@ -0,0 +1,147 @@ +""" +pyXCursor script made by zorvios: +https://github.com/zorvios/PyXCursor +""" + +import os +import ctypes +import ctypes.util +import numpy as np + +# A helper function to convert data from Xlib to byte array. +import struct, array + +# Define ctypes version of XFixesCursorImage structure. +PIXEL_DATA_PTR = ctypes.POINTER(ctypes.c_ulong) +Atom = ctypes.c_ulong + +class XFixesCursorImage (ctypes.Structure): + """ + See /usr/include/X11/extensions/Xfixes.h + + typedef struct { + short x, y; + unsigned short width, height; + unsigned short xhot, yhot; + unsigned long cursor_serial; + unsigned long *pixels; + if XFIXES_MAJOR >= 2 + Atom atom; /* Version >= 2 only */ + const char *name; /* Version >= 2 only */ + endif + } XFixesCursorImage; + """ + _fields_ = [('x', ctypes.c_short), + ('y', ctypes.c_short), + ('width', ctypes.c_ushort), + ('height', ctypes.c_ushort), + ('xhot', ctypes.c_ushort), + ('yhot', ctypes.c_ushort), + ('cursor_serial', ctypes.c_ulong), + ('pixels', PIXEL_DATA_PTR), + ('atom', Atom), + ('name', ctypes.c_char_p)] + +class Display(ctypes.Structure): + pass + + +class Xcursor: + display = None + def __init__(self, display=None): + if not display: + try: + display = os.environ["DISPLAY"].encode("utf-8") + except KeyError: + raise Exception("$DISPLAY not set.") + + #XFixeslib = ctypes.CDLL('libXfixes.so') + XFixes = ctypes.util.find_library("Xfixes") + if not XFixes: + raise Exception("No XFixes library found.") + self.XFixeslib = ctypes.cdll.LoadLibrary(XFixes) + + #xlib = ctypes.CDLL('libX11.so.6') + x11 = ctypes.util.find_library("X11") + if not x11: + raise Exception("No X11 library found.") + self.xlib = ctypes.cdll.LoadLibrary(x11) + + # Define ctypes' version of XFixesGetCursorImage function + XFixesGetCursorImage = self.XFixeslib.XFixesGetCursorImage + XFixesGetCursorImage.restype = ctypes.POINTER(XFixesCursorImage) + XFixesGetCursorImage.argtypes = [ctypes.POINTER(Display)] + self.XFixesGetCursorImage = XFixesGetCursorImage + + XOpenDisplay = self.xlib.XOpenDisplay + XOpenDisplay.restype = ctypes.POINTER(Display) + XOpenDisplay.argtypes = [ctypes.c_char_p] + + if not self.display: + self.display = self.xlib.XOpenDisplay(display) # (display) or (None) + + def argbdata_to_pixdata(self, data, len): + if data == None or len < 1: return None + + # Create byte array + b = array.array('b', b'\x00'*4*len) + + offset,i = 0,0 + while i < len: + argb = data[i] & 0xffffffff + rgba = (argb << 8) | (argb >> 24) + b1 = (rgba >> 24) & 0xff + b2 = (rgba >> 16) & 0xff + b3 = (rgba >> 8) & 0xff + b4 = rgba & 0xff + + struct.pack_into("=BBBB", b, offset, b1, b2, b3, b4) + offset = offset + 4 + i = i + 1 + + return b + + def getCursorImageData(self): + # Call the function. Read data of cursor/mouse-pointer. + cursor_data = self.XFixesGetCursorImage(self.display) + + if not (cursor_data and cursor_data[0]): + raise Exception("Cannot read XFixesGetCursorImage()") + + # Note: cursor_data is a pointer, take cursor_data[0] + return cursor_data[0] + + def getCursorImageArray(self): + data = self.getCursorImageData() + # x, y = data.x, data.y + height,width = data.height, data.width + + bytearr = self.argbdata_to_pixdata(data.pixels, height*width) + + imgarray = np.array(bytearr, dtype=np.uint8) + imgarray = imgarray.reshape(height,width,4) + del bytearr + + return imgarray + + def getCursorImageArrayFast(self): + data = self.getCursorImageData() + # x, y = data.x, data.y + height,width = data.height, data.width + + bytearr = ctypes.cast(data.pixels, ctypes.POINTER(ctypes.c_ulong *height * width))[0] + imgarray = np.array(bytearray(bytearr)) + imgarray = imgarray.reshape(height,width,8)[:, :, (0, 1, 2, 3)] + del bytearr + + return imgarray + + def saveImage(self,imgarray,text): + from PIL import Image + img = Image.fromarray(imgarray) + img.save(text) + +if __name__ == "__main__": + cursor = Xcursor() + imgarray = cursor.getCursorImageArrayFast() + cursor.saveImage(imgarray,'cursor_image.png')