Skip to content

Commit 6c49f5a

Browse files
committed
scroll mode icon
1 parent 0241d0f commit 6c49f5a

File tree

4 files changed

+214
-51
lines changed

4 files changed

+214
-51
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,35 @@
11
This simple Python script gives you a Windows-like autoscroll feature on Linux. It works system-wide on every distribution with Xorg.
22

33
## Installation
4-
4+
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.
55
1. Clone the repository:
66
```
77
git clone https://github.com/TWolczanski/linux-autoscroll.git
8+
cd linux-autoscroll/
89
```
910
2. Create a Python virtual environment and activate it:
1011
```
1112
python3 -m venv .autoscroll
1213
source .autoscroll/bin/activate
1314
```
14-
3. Install pynput:
15+
3. Install necessary Python libraries. For `autoscroll_no_icon.py` you don't need the last one.
1516
```
17+
python3 -m pip install wheel
1618
python3 -m pip install pynput
19+
python3 -m pip install PyQt5
1720
```
18-
4. Add the following shebang to the script (substitute `/path/to` with the actual path to your virtual environment):
21+
4. Add the following shebang to the script (substitute `/path/to` with the actual path):
1922
```
20-
#!/path/to/.autoscroll/bin/python3
23+
#!/path/to/linux-autoscroll/.autoscroll/bin/python3
2124
```
2225
5. Make the script executable:
2326
```
2427
chmod u+x autoscroll.py
2528
```
29+
or
30+
```
31+
chmod u+x autoscroll_no_icon.py
32+
```
2633
6. Add the script to the list of autostart commands.
2734

2835
## Configuration
@@ -47,8 +54,10 @@ with Listener(on_click = on_click) as listener:
4754
\
4855
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.
4956

57+
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.
58+
5059
## Usage
5160

5261
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`).
5362

54-
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.
63+
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.

autoscroll.py

Lines changed: 108 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,116 @@
11
from pynput.mouse import Button, Controller, Listener
2-
from threading import Event
2+
from threading import Event, Thread
33
from time import sleep
4+
from PyQt5.QtWidgets import QApplication, QLabel
5+
from PyQt5.QtCore import Qt, pyqtSignal
6+
from PyQt5.QtSvg import QSvgWidget
7+
from PyQt5.QtGui import QPixmap
8+
from pathlib import Path
9+
import sys
410

5-
def on_move(x, y):
6-
global pos, scroll_mode, direction, interval, DELAY, DEAD_AREA
7-
if scroll_mode.is_set():
8-
delta = pos[1] - y
9-
if abs(delta) <= DEAD_AREA:
10-
direction = 0
11-
elif delta > 0:
12-
direction = 1
13-
elif delta < 0:
14-
direction = -1
15-
if abs(delta) <= DEAD_AREA + DELAY * 2:
16-
interval = 0.5
17-
else:
18-
interval = DELAY / (abs(delta) - DEAD_AREA)
11+
class AutoscrollIconSvg(QSvgWidget):
12+
scroll_mode_entered = pyqtSignal()
13+
scroll_mode_exited = pyqtSignal()
14+
15+
def __init__(self, path, size):
16+
super().__init__(path)
17+
self.size = size
18+
self.renderer().setAspectRatioMode(Qt.KeepAspectRatio)
19+
self.resize(self.size, self.size)
20+
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.X11BypassWindowManagerHint)
21+
self.setAttribute(Qt.WA_TranslucentBackground)
22+
self.scroll_mode_entered.connect(self.show)
23+
self.scroll_mode_exited.connect(self.close)
24+
25+
def show(self):
26+
x = self.pos[0] - self.size // 2
27+
y = self.pos[1] - self.size // 2
28+
self.move(x, y)
29+
super().show()
1930

20-
def on_click(x, y, button, pressed):
21-
global pos, scroll_mode, direction, interval, BUTTON_START, BUTTON_STOP
22-
if button == BUTTON_START and pressed and not scroll_mode.is_set():
23-
pos = (x, y)
24-
direction = 0
25-
interval = 0
26-
scroll_mode.set()
27-
elif button == BUTTON_STOP and pressed and scroll_mode.is_set():
28-
scroll_mode.clear()
31+
class AutoscrollIconRaster(QLabel):
32+
scroll_mode_entered = pyqtSignal()
33+
scroll_mode_exited = pyqtSignal()
34+
35+
def __init__(self, path, size):
36+
super().__init__()
37+
self.size = size
38+
self.resize(self.size, self.size)
39+
self.img = QPixmap(path).scaled(self.size, self.size, Qt.KeepAspectRatio)
40+
self.setPixmap(self.img)
41+
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.X11BypassWindowManagerHint)
42+
self.setAttribute(Qt.WA_TranslucentBackground)
43+
self.scroll_mode_entered.connect(self.show)
44+
self.scroll_mode_exited.connect(self.close)
2945

30-
def autoscroll():
31-
global mouse, scroll_mode, direction, interval
32-
while True:
33-
scroll_mode.wait()
34-
sleep(interval)
35-
mouse.scroll(0, direction)
46+
def show(self):
47+
x = self.pos[0] - self.size // 2
48+
y = self.pos[1] - self.size // 2
49+
self.move(x, y)
50+
super().show()
3651

37-
mouse = Controller()
38-
listener = Listener(on_move = on_move, on_click = on_click)
39-
scroll_mode = Event()
40-
pos = mouse.position
41-
direction = 0
42-
interval = 0
52+
class Autoscroll():
53+
def __init__(self):
54+
# modify this to adjust the speed of scrolling
55+
self.DELAY = 5
56+
# modify this to change the button used for entering the scroll mode
57+
self.BUTTON_START = Button.middle
58+
# modify this to change the button used for exiting the scroll mode
59+
self.BUTTON_STOP = Button.middle
60+
# modify this to change the size (in px) of the area below and above the starting point where scrolling is paused
61+
self.DEAD_AREA = 30
62+
# modify this to change the scroll mode icon
63+
# supported formats: svg, png, jpg, jpeg, gif, bmp, pbm, pgm, ppm, xbm, xpm
64+
# the path MUST be absolute
65+
self.ICON_PATH = str(Path(__file__).parent.resolve()) + "/icon.svg"
66+
# modify this to change the size (in px) of the icon
67+
# note that only svg images can be resized without loss of quality
68+
self.ICON_SIZE = 30
69+
70+
if self.ICON_PATH[-4:] == ".svg":
71+
self.icon = AutoscrollIconSvg(self.ICON_PATH, self.ICON_SIZE)
72+
else:
73+
self.icon = AutoscrollIconRaster(self.ICON_PATH, self.ICON_SIZE)
74+
75+
self.mouse = Controller()
76+
self.scroll_mode = Event()
77+
self.listener = Listener(on_move=self.on_move, on_click=self.on_click)
78+
self.listener.start()
79+
self.looper = Thread(target=self.loop)
80+
self.looper.start()
81+
82+
def on_move(self, x, y):
83+
if self.scroll_mode.is_set():
84+
delta = self.icon.pos[1] - y
85+
if abs(delta) <= self.DEAD_AREA:
86+
self.direction = 0
87+
elif delta > 0:
88+
self.direction = 1
89+
elif delta < 0:
90+
self.direction = -1
91+
if abs(delta) <= self.DEAD_AREA + self.DELAY * 2:
92+
self.interval = 0.5
93+
else:
94+
self.interval = self.DELAY / (abs(delta) - self.DEAD_AREA)
4395

44-
# modify this to adjust the speed of scrolling
45-
DELAY = 5
46-
# modify this to change the button used for entering the scroll mode
47-
BUTTON_START = Button.middle
48-
# modify this to change the button used for exiting the scroll mode
49-
BUTTON_STOP = Button.middle
50-
# modify this to change the size (in px) of the area below and above the starting point where the scrolling is paused
51-
DEAD_AREA = 30
96+
def on_click(self, x, y, button, pressed):
97+
if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set():
98+
self.icon.pos = (x, y)
99+
self.direction = 0
100+
self.interval = 0.5
101+
self.scroll_mode.set()
102+
self.icon.scroll_mode_entered.emit()
103+
elif button == self.BUTTON_STOP and pressed and self.scroll_mode.is_set():
104+
self.scroll_mode.clear()
105+
self.icon.scroll_mode_exited.emit()
106+
107+
def loop(self):
108+
while True:
109+
self.scroll_mode.wait()
110+
sleep(self.interval)
111+
self.mouse.scroll(0, self.direction)
52112

53-
listener.start()
54-
autoscroll()
113+
app = QApplication(sys.argv)
114+
app.setQuitOnLastWindowClosed(False)
115+
autoscroll = Autoscroll()
116+
sys.exit(app.exec())

autoscroll_no_icon.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from pynput.mouse import Button, Controller, Listener
2+
from threading import Event
3+
from time import sleep
4+
5+
class Autoscroll():
6+
def __init__(self):
7+
# modify this to adjust the speed of scrolling
8+
self.DELAY = 5
9+
# modify this to change the button used for entering the scroll mode
10+
self.BUTTON_START = Button.middle
11+
# modify this to change the button used for exiting the scroll mode
12+
self.BUTTON_STOP = Button.middle
13+
# modify this to change the size (in px) of the area below and above the starting point where scrolling is paused
14+
self.DEAD_AREA = 30
15+
16+
self.mouse = Controller()
17+
self.scroll_mode = Event()
18+
self.listener = Listener(on_move=self.on_move, on_click=self.on_click)
19+
self.listener.start()
20+
21+
def on_move(self, x, y):
22+
if self.scroll_mode.is_set():
23+
delta = self.pos[1] - y
24+
if abs(delta) <= self.DEAD_AREA:
25+
self.direction = 0
26+
elif delta > 0:
27+
self.direction = 1
28+
elif delta < 0:
29+
self.direction = -1
30+
if abs(delta) <= self.DEAD_AREA + self.DELAY * 2:
31+
self.interval = 0.5
32+
else:
33+
self.interval = self.DELAY / (abs(delta) - self.DEAD_AREA)
34+
35+
def on_click(self, x, y, button, pressed):
36+
if button == self.BUTTON_START and pressed and not self.scroll_mode.is_set():
37+
self.pos = (x, y)
38+
self.direction = 0
39+
self.interval = 0.5
40+
self.scroll_mode.set()
41+
elif button == self.BUTTON_STOP and pressed and self.scroll_mode.is_set():
42+
self.scroll_mode.clear()
43+
44+
def start(self):
45+
while True:
46+
self.scroll_mode.wait()
47+
sleep(self.interval)
48+
self.mouse.scroll(0, self.direction)
49+
50+
autoscroll = Autoscroll()
51+
autoscroll.start()

icon.svg

Lines changed: 41 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)