Skip to content

Commit

Permalink
scroll mode icon
Browse files Browse the repository at this point in the history
  • Loading branch information
TWolczanski committed Dec 29, 2021
1 parent 0241d0f commit 84151a1
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 51 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
154 changes: 108 additions & 46 deletions autoscroll.py
Original file line number Diff line number Diff line change
@@ -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())
51 changes: 51 additions & 0 deletions autoscroll_no_icon.py
Original file line number Diff line number Diff line change
@@ -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()
41 changes: 41 additions & 0 deletions icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 84151a1

Please sign in to comment.