Skip to content

Commit 4ceb33e

Browse files
committed
multi-monitor
1 parent 4fdd2a2 commit 4ceb33e

File tree

4 files changed

+114
-21
lines changed

4 files changed

+114
-21
lines changed

modules/overview.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
icon_resolver = IconResolver()
2929
connection = Hyprland()
30-
SCALE = 0.1
30+
BASE_SCALE = 0.1 # Base scale factor for overview
3131

3232
# Credit to Aylur for the drag and drop code
3333
TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)]
@@ -175,18 +175,22 @@ def on_button_click(self, *_):
175175

176176

177177
class WorkspaceEventBox(EventBox):
178-
def __init__(self, workspace_id: int, fixed: Gtk.Fixed | None = None, monitor_width: int = None, monitor_height: int = None):
178+
def __init__(self, workspace_id: int, fixed: Gtk.Fixed | None = None, monitor_width: int = None, monitor_height: int = None, monitor_scale: float = 1.0):
179179
self.fixed = fixed
180180

181181
# Use provided monitor dimensions or fallback to current screen
182182
width = monitor_width or CURRENT_WIDTH
183183
height = monitor_height or CURRENT_HEIGHT
184184

185+
# Workspace containers should maintain consistent size across monitors
186+
# Only use BASE_SCALE, don't multiply by monitor_scale for the container
187+
container_scale = BASE_SCALE
188+
185189
super().__init__(
186190
name="overview-workspace-bg",
187191
h_expand=True,
188192
v_expand=True,
189-
size=(int(width * SCALE), int(height * SCALE)),
193+
size=(int(width * container_scale), int(height * container_scale)),
190194
child=fixed
191195
if fixed
192196
else Label(
@@ -365,15 +369,21 @@ def update(self, signal_update=False):
365369

366370
self.children = [Box(spacing=8) for _ in range(rows)]
367371

368-
# Get monitor dimensions for scaling
372+
# Get monitor dimensions and scale for scaling
369373
monitor_width = CURRENT_WIDTH
370374
monitor_height = CURRENT_HEIGHT
375+
monitor_scale = 1.0
371376

372377
if self.monitor_manager:
373378
monitor_info = self.monitor_manager.get_monitor_by_id(self.monitor_id)
374379
if monitor_info:
375380
monitor_width = monitor_info['width']
376381
monitor_height = monitor_info['height']
382+
monitor_scale = monitor_info.get('scale', 1.0)
383+
384+
# Calculate effective scale for this monitor
385+
# Higher scale monitors need larger overview elements to appear the same physical size
386+
effective_scale = BASE_SCALE * monitor_scale
377387

378388
monitors = {
379389
monitor["id"]: (monitor["x"], monitor["y"], monitor["transform"])
@@ -389,7 +399,7 @@ def update(self, signal_update=False):
389399
title=client["title"],
390400
address=client["address"],
391401
app_id=client["initialClass"],
392-
size=(client["size"][0] * SCALE, client["size"][1] * SCALE),
402+
size=(client["size"][0] * effective_scale, client["size"][1] * effective_scale),
393403
transform=monitors[client["monitor"]][2],
394404
)
395405
self.clients[client["address"]] = btn
@@ -398,8 +408,8 @@ def update(self, signal_update=False):
398408
self.workspace_boxes[w_id] = Gtk.Fixed.new()
399409
self.workspace_boxes[w_id].put(
400410
btn,
401-
abs(client["at"][0] - monitors[client["monitor"]][0]) * SCALE,
402-
abs(client["at"][1] - monitors[client["monitor"]][1]) * SCALE,
411+
abs(client["at"][0] - monitors[client["monitor"]][0]) * effective_scale,
412+
abs(client["at"][1] - monitors[client["monitor"]][1]) * effective_scale,
403413
)
404414

405415
# Generate workspaces only for this monitor's range
@@ -420,7 +430,8 @@ def update(self, signal_update=False):
420430
w_id,
421431
self.workspace_boxes.get(w_id),
422432
monitor_width=monitor_width,
423-
monitor_height=monitor_height
433+
monitor_height=monitor_height,
434+
monitor_scale=monitor_scale
424435
),
425436
],
426437
)

services/monitor_focus.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __init__(self):
4444

4545
self._initialized = True
4646
self._monitor_name_to_id = {}
47+
self._monitor_info = {} # Store rich monitor information
4748
self._current_workspace = 1
4849
self._current_monitor_name = ""
4950
self._listening = False
@@ -57,19 +58,32 @@ def __init__(self):
5758
self.start_listening()
5859

5960
def _update_monitor_mapping(self):
60-
"""Update the monitor name to ID mapping."""
61+
"""Update the monitor name to ID mapping with rich monitor information."""
6162
try:
6263
# Import here to avoid circular imports
6364
from utils.monitor_manager import get_monitor_manager
6465
manager = get_monitor_manager()
6566
monitors = manager.get_monitors()
6667

6768
self._monitor_name_to_id = {}
69+
self._monitor_info = {} # Store rich monitor information
6870
for monitor in monitors:
69-
self._monitor_name_to_id[monitor['name']] = monitor['id']
71+
monitor_name = monitor['name']
72+
monitor_id = monitor['id']
73+
self._monitor_name_to_id[monitor_name] = monitor_id
74+
self._monitor_info[monitor_id] = {
75+
'name': monitor_name,
76+
'width': monitor.get('width', 1920),
77+
'height': monitor.get('height', 1080),
78+
'x': monitor.get('x', 0),
79+
'y': monitor.get('y', 0),
80+
'scale': monitor.get('scale', 1.0),
81+
'focused': monitor.get('focused', False)
82+
}
7083
except ImportError:
7184
# Fallback if monitor manager not available yet
7285
self._monitor_name_to_id = {}
86+
self._monitor_info = {}
7387

7488
def start_listening(self):
7589
"""Start listening to Hyprland events in a separate thread."""
@@ -188,6 +202,24 @@ def get_current_workspace(self) -> int:
188202
def get_monitor_id_by_name(self, monitor_name: str) -> Optional[int]:
189203
"""Get monitor ID by name."""
190204
return self._monitor_name_to_id.get(monitor_name)
205+
206+
def get_monitor_info(self, monitor_id: int) -> Optional[dict]:
207+
"""Get rich monitor information by ID."""
208+
return self._monitor_info.get(monitor_id)
209+
210+
def get_current_monitor_info(self) -> Optional[dict]:
211+
"""Get rich information for current monitor."""
212+
current_id = self.get_current_monitor_id()
213+
return self.get_monitor_info(current_id)
214+
215+
def get_monitor_scale(self, monitor_id: int) -> float:
216+
"""Get monitor scale factor by ID."""
217+
info = self.get_monitor_info(monitor_id)
218+
return info.get('scale', 1.0) if info else 1.0
219+
220+
def get_current_monitor_scale(self) -> float:
221+
"""Get current monitor scale factor."""
222+
return self.get_monitor_scale(self.get_current_monitor_id())
191223

192224

193225
# Singleton accessor

utils/monitor_manager.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,45 @@ def set_monitor_focus_service(self, service):
6565
if service:
6666
service.monitor_focused.connect(self._on_monitor_focused)
6767

68+
def _get_gtk_monitor_info(self) -> List[Dict]:
69+
"""Get monitor information using GTK/GDK including scale factors."""
70+
gtk_monitors = []
71+
try:
72+
display = Gdk.Display.get_default()
73+
if display and hasattr(display, 'get_n_monitors'):
74+
n_monitors = display.get_n_monitors()
75+
for i in range(n_monitors):
76+
monitor = display.get_monitor(i)
77+
if monitor:
78+
geometry = monitor.get_geometry()
79+
scale_factor = monitor.get_scale_factor()
80+
model = monitor.get_model() or f'monitor-{i}'
81+
82+
gtk_monitors.append({
83+
'id': i,
84+
'name': model,
85+
'width': geometry.width,
86+
'height': geometry.height,
87+
'x': geometry.x,
88+
'y': geometry.y,
89+
'scale': scale_factor
90+
})
91+
except Exception as e:
92+
print(f"Error getting GTK monitor info: {e}")
93+
94+
return gtk_monitors
95+
6896
def refresh_monitors(self) -> List[Dict]:
6997
"""
70-
Detect monitors using Hyprland API with GTK fallback.
98+
Detect monitors using Hyprland API for accurate info, with GTK for scale detection.
7199
72100
Returns:
73-
List of monitor dictionaries with id, name, width, height, x, y
101+
List of monitor dictionaries with id, name, width, height, x, y, scale
74102
"""
75103
self._monitors = []
76104

77105
try:
78-
# Try Hyprland first
106+
# Try Hyprland first for primary info (more accurate)
79107
result = subprocess.run(
80108
["hyprctl", "monitors", "-j"],
81109
capture_output=True,
@@ -85,14 +113,20 @@ def refresh_monitors(self) -> List[Dict]:
85113
hypr_monitors = json.loads(result.stdout)
86114

87115
for i, monitor in enumerate(hypr_monitors):
116+
monitor_name = monitor.get('name', f'monitor-{i}')
117+
118+
# Get scale directly from Hyprland (more reliable)
119+
hypr_scale = monitor.get('scale', 1.0)
120+
88121
self._monitors.append({
89122
'id': i,
90-
'name': monitor.get('name', f'monitor-{i}'),
123+
'name': monitor_name,
91124
'width': monitor.get('width', 1920),
92125
'height': monitor.get('height', 1080),
93126
'x': monitor.get('x', 0),
94127
'y': monitor.get('y', 0),
95-
'focused': monitor.get('focused', False)
128+
'focused': monitor.get('focused', False),
129+
'scale': hypr_scale
96130
})
97131

98132
# Initialize states for new monitors
@@ -101,7 +135,7 @@ def refresh_monitors(self) -> List[Dict]:
101135
self._current_notch_module[i] = None
102136

103137
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
104-
# Fallback to GTK
138+
# Fallback to GTK only if Hyprland fails
105139
self._fallback_to_gtk()
106140

107141
# Ensure we have at least one monitor
@@ -113,7 +147,8 @@ def refresh_monitors(self) -> List[Dict]:
113147
'height': 1080,
114148
'x': 0,
115149
'y': 0,
116-
'focused': True
150+
'focused': True,
151+
'scale': 1.0
117152
}]
118153
self._notch_states[0] = False
119154
self._current_notch_module[0] = None
@@ -128,14 +163,15 @@ def refresh_monitors(self) -> List[Dict]:
128163
return self._monitors
129164

130165
def _fallback_to_gtk(self):
131-
"""Fallback monitor detection using GTK."""
166+
"""Fallback monitor detection using GTK with scale information."""
132167
try:
133168
display = Gdk.Display.get_default()
134169
if display and hasattr(display, 'get_n_monitors'):
135170
n_monitors = display.get_n_monitors()
136171
for i in range(n_monitors):
137172
monitor = display.get_monitor(i)
138173
geometry = monitor.get_geometry()
174+
scale_factor = monitor.get_scale_factor()
139175

140176
self._monitors.append({
141177
'id': i,
@@ -144,7 +180,8 @@ def _fallback_to_gtk(self):
144180
'height': geometry.height,
145181
'x': geometry.x,
146182
'y': geometry.y,
147-
'focused': i == 0 # Assume first monitor is focused
183+
'focused': i == 0, # Assume first monitor is focused
184+
'scale': scale_factor
148185
})
149186

150187
if i not in self._notch_states:
@@ -200,6 +237,19 @@ def get_monitor_for_workspace(self, workspace_id: int) -> int:
200237
return 0
201238
return (workspace_id - 1) // 10
202239

240+
def get_monitor_scale(self, monitor_id: int) -> float:
241+
"""
242+
Get scale factor for a monitor.
243+
244+
Args:
245+
monitor_id: Monitor ID
246+
247+
Returns:
248+
Scale factor (default 1.0 if not found)
249+
"""
250+
monitor = self.get_monitor_by_id(monitor_id)
251+
return monitor.get('scale', 1.0) if monitor else 1.0
252+
203253
def is_notch_open(self, monitor_id: int) -> bool:
204254
"""Check if notch is open on a monitor."""
205255
return self._notch_states.get(monitor_id, False)

version.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"version": "0.0.38",
2+
"version": "0.0.40",
33
"pkg_update": false,
44
"changelog": [
5-
"<b>tweak:</b> General optimizations and bug fixes."
5+
"<b>feat:</b> Multi-monitor support"
66
]
77
}

0 commit comments

Comments
 (0)