diff --git a/README.md b/README.md index 47861bf..9f917d3 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,35 @@ This simple Python script gives you a Windows-like autoscroll feature on Linux. It works system-wide on every distribution with Xorg. ## Installation - +There are two versions of the script. One of them (`autoscroll.py`) displays an icon indicating the place where the scroll mode has been entered and the other (`autoscroll_no_icon.py`) does not. 1. Clone the repository: ``` git clone https://github.com/TWolczanski/linux-autoscroll.git +cd linux-autoscroll/ ``` 2. Create a Python virtual environment and activate it: ``` python3 -m venv .autoscroll source .autoscroll/bin/activate ``` -3. Install pynput: +3. Install necessary Python libraries. For `autoscroll_no_icon.py` you don't need PyQt5. ``` +python3 -m pip install wheel python3 -m pip install pynput +python3 -m pip install PyQt5 ``` -4. Add the following shebang to the script (substitute `/path/to` with the actual path to your virtual environment): +4. Add the following shebang to the script (substitute `/path/to` with the actual path): ``` -#!/path/to/.autoscroll/bin/python3 +#!/path/to/linux-autoscroll/.autoscroll/bin/python3 ``` 5. Make the script executable: ``` chmod u+x autoscroll.py ``` +or +``` +chmod u+x autoscroll_no_icon.py +``` 6. Add the script to the list of autostart commands. ## Configuration @@ -47,8 +54,10 @@ with Listener(on_click = on_click) as listener: \ By default the scrolling begins when the mouse pointer is 30 px below or above the point where `BUTTON_START` was pressed. In order to change that you can modify `DEAD_AREA`. If you set it to 0 (which is the minimum value), the scrolling will be paused only when the vertical position of the cursor is exactly the same as the position in which the scroll mode was activated. +In `autoscroll.py` you can also modify the `ICON_PATH` and `ICON_SIZE` constants. If you don't like the default icon displayed in the scroll mode, in `ICON_PATH` you can specify the absolute path to the image you want to be used instead. `ICON_SIZE` is the size (maximum of width and height) you want your image to be scaled to. + ## Usage Click the middle mouse button (or the button you assigned to `BUTTON_START`) and move your mouse to start scrolling. The further you move the mouse (vertically) from the point where you have clicked the button, the faster the scrolling becomes. To leave the scroll mode, simply press the middle mouse button again (or press the button you assigned to `BUTTON_STOP`). -Note that at slow speed the scrolling is not smooth and (probably) there is no way to make it smooth. However, one can easily get used to it. +Note that at slow speed the scrolling is not smooth and (probably) there is no way to make it smooth. The smoothness depends on the distance your mouse scrolls per one wheel click. There are some programs in which this distance is very small (e.g. Chrome, Teams and Discord) and in these programs the autoscroll is smoother than in other programs. diff --git a/autoscroll.py b/autoscroll.py index bfb0566..f8f08e3 100644 --- a/autoscroll.py +++ b/autoscroll.py @@ -1,54 +1,116 @@ from pynput.mouse import Button, Controller, Listener -from threading import Event +from threading import Event, Thread from time import sleep +from PyQt5.QtWidgets import QApplication, QLabel +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtSvg import QSvgWidget +from PyQt5.QtGui import QPixmap +from pathlib import Path +import sys -def on_move(x, y): - global pos, scroll_mode, direction, interval, DELAY, DEAD_AREA - if scroll_mode.is_set(): - delta = pos[1] - y - if abs(delta) <= DEAD_AREA: - direction = 0 - elif delta > 0: - direction = 1 - elif delta < 0: - direction = -1 - if abs(delta) <= DEAD_AREA + DELAY * 2: - interval = 0.5 - else: - interval = DELAY / (abs(delta) - DEAD_AREA) +class AutoscrollIconSvg(QSvgWidget): + scroll_mode_entered = pyqtSignal() + scroll_mode_exited = pyqtSignal() + + def __init__(self, path, size): + super().__init__(path) + self.size = size + self.renderer().setAspectRatioMode(Qt.KeepAspectRatio) + self.resize(self.size, self.size) + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.X11BypassWindowManagerHint) + 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() -def on_click(x, y, button, pressed): - global pos, scroll_mode, direction, interval, BUTTON_START, BUTTON_STOP - if button == BUTTON_START and pressed and not scroll_mode.is_set(): - pos = (x, y) - direction = 0 - interval = 0 - scroll_mode.set() - elif button == BUTTON_STOP and pressed and scroll_mode.is_set(): - scroll_mode.clear() +class AutoscrollIconRaster(QLabel): + scroll_mode_entered = pyqtSignal() + scroll_mode_exited = pyqtSignal() + + def __init__(self, path, size): + super().__init__() + self.size = size + self.resize(self.size, self.size) + self.img = QPixmap(path).scaled(self.size, self.size, Qt.KeepAspectRatio) + self.setPixmap(self.img) + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.X11BypassWindowManagerHint) + self.setAttribute(Qt.WA_TranslucentBackground) + self.scroll_mode_entered.connect(self.show) + self.scroll_mode_exited.connect(self.close) -def autoscroll(): - global mouse, scroll_mode, direction, interval - while True: - scroll_mode.wait() - sleep(interval) - mouse.scroll(0, direction) + def show(self): + x = self.pos[0] - self.size // 2 + y = self.pos[1] - self.size // 2 + self.move(x, y) + super().show() -mouse = Controller() -listener = Listener(on_move = on_move, on_click = on_click) -scroll_mode = Event() -pos = mouse.position -direction = 0 -interval = 0 +class Autoscroll(): + def __init__(self): + # modify this to adjust the speed of scrolling + self.DELAY = 5 + # modify this to change the button used for entering the scroll mode + self.BUTTON_START = Button.middle + # modify this to change the button used for exiting the scroll mode + 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 scroll mode icon + # supported formats: svg, png, jpg, jpeg, gif, bmp, pbm, pgm, ppm, xbm, xpm + # the path MUST be absolute + self.ICON_PATH = str(Path(__file__).parent.resolve()) + "/icon.svg" + # 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.listener = Listener(on_move=self.on_move, on_click=self.on_click) + self.listener.start() + self.looper = Thread(target=self.loop) + self.looper.start() + + def on_move(self, x, y): + if self.scroll_mode.is_set(): + delta = self.icon.pos[1] - y + if abs(delta) <= self.DEAD_AREA: + self.direction = 0 + elif delta > 0: + self.direction = 1 + elif delta < 0: + self.direction = -1 + if abs(delta) <= self.DEAD_AREA + self.DELAY * 2: + self.interval = 0.5 + else: + self.interval = self.DELAY / (abs(delta) - self.DEAD_AREA) -# modify this to adjust the speed of scrolling -DELAY = 5 -# modify this to change the button used for entering the scroll mode -BUTTON_START = Button.middle -# modify this to change the button used for exiting the scroll mode -BUTTON_STOP = Button.middle -# modify this to change the size (in px) of the area below and above the starting point where the scrolling is paused -DEAD_AREA = 30 + def on_click(self, x, y, button, pressed): + if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set(): + 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) -listener.start() -autoscroll() +app = QApplication(sys.argv) +app.setQuitOnLastWindowClosed(False) +autoscroll = Autoscroll() +sys.exit(app.exec()) diff --git a/autoscroll_no_icon.py b/autoscroll_no_icon.py new file mode 100644 index 0000000..69ab05d --- /dev/null +++ b/autoscroll_no_icon.py @@ -0,0 +1,51 @@ +from pynput.mouse import Button, Controller, Listener +from threading import Event +from time import sleep + +class Autoscroll(): + def __init__(self): + # modify this to adjust the speed of scrolling + self.DELAY = 5 + # modify this to change the button used for entering the scroll mode + self.BUTTON_START = Button.middle + # modify this to change the button used for exiting the scroll mode + 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 + + self.mouse = Controller() + self.scroll_mode = Event() + self.listener = Listener(on_move=self.on_move, on_click=self.on_click) + self.listener.start() + + def on_move(self, x, y): + if self.scroll_mode.is_set(): + delta = self.pos[1] - y + if abs(delta) <= self.DEAD_AREA: + self.direction = 0 + elif delta > 0: + self.direction = 1 + elif delta < 0: + self.direction = -1 + if abs(delta) <= self.DEAD_AREA + self.DELAY * 2: + self.interval = 0.5 + else: + 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.pos = (x, y) + self.direction = 0 + self.interval = 0.5 + self.scroll_mode.set() + elif button == self.BUTTON_STOP and pressed and self.scroll_mode.is_set(): + self.scroll_mode.clear() + + def start(self): + while True: + self.scroll_mode.wait() + sleep(self.interval) + self.mouse.scroll(0, self.direction) + +autoscroll = Autoscroll() +autoscroll.start() \ No newline at end of file diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..71dc7e3 --- /dev/null +++ b/icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + +