feat: Custom Monitor-Auswahl im RDP Client
Neue "Custom..." Option im Multi-Monitor-Dropdown, die einen Checkbox-Dialog öffnet zur manuellen Auswahl einzelner Monitore. Löst das Problem, dass bei "2 Monitors" immer die linkesten Monitore gewählt wurden statt der gewünschten. Neue Komponenten: - _get_monitor_details(): Erkennt Monitore via xfreerdp + xrandr - MonitorSelectionDialog: Checkbox-Dialog für Monitor-Auswahl - monitor_ids wird pro Verbindung gespeichert Bugfix: Doppelter _add_keyboard_shortcuts_info() Aufruf entfernt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
196
rdp_client.py
196
rdp_client.py
@@ -324,6 +324,60 @@ class RDPClient:
|
||||
pass
|
||||
return 1 # Default to 1 monitor if detection fails
|
||||
|
||||
def _get_monitor_details(self):
|
||||
"""Get detailed information about all available monitors using xfreerdp and xrandr"""
|
||||
monitors = []
|
||||
|
||||
# Get xrandr output names for better readability
|
||||
xrandr_names = {}
|
||||
try:
|
||||
result = subprocess.run(['xrandr', '--listmonitors'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.strip().split('\n')[1:]: # Skip "Monitors: N" header
|
||||
# Format: " 0: +*eDP-1 1920/344x1200/215+0+0 eDP-1"
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 3:
|
||||
idx = int(parts[0].rstrip(':'))
|
||||
# Output name is after the +/* prefix
|
||||
name_part = parts[1]
|
||||
# Strip leading +, * characters
|
||||
output_name = name_part.lstrip('+*')
|
||||
xrandr_names[idx] = output_name
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not get xrandr monitor names: {e}")
|
||||
|
||||
# Get xfreerdp monitor list for IDs, resolutions, positions
|
||||
try:
|
||||
result = subprocess.run(['xfreerdp', '/monitor-list'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
cleaned_line = line.strip()
|
||||
is_primary = cleaned_line.startswith('*')
|
||||
cleaned_line = cleaned_line.replace('*', '').strip()
|
||||
|
||||
if '[' in cleaned_line and ']' in cleaned_line and 'x' in cleaned_line and '+' in cleaned_line:
|
||||
parts = cleaned_line.split()
|
||||
if len(parts) >= 3:
|
||||
id_part = parts[0] # [0]
|
||||
res_part = parts[1] # 1920x1080
|
||||
pos_part = parts[2] # +3840+0
|
||||
|
||||
if '[' in id_part and ']' in id_part:
|
||||
monitor_id = int(id_part.strip('[]'))
|
||||
monitors.append({
|
||||
'id': monitor_id,
|
||||
'name': xrandr_names.get(monitor_id, f"Monitor {monitor_id}"),
|
||||
'resolution': res_part,
|
||||
'position': pos_part,
|
||||
'is_primary': is_primary,
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not get xfreerdp monitor list: {e}")
|
||||
|
||||
return monitors
|
||||
|
||||
def _get_multimon_options(self):
|
||||
"""Get available multi-monitor options based on system"""
|
||||
monitor_count = self._get_available_monitors_count()
|
||||
@@ -334,6 +388,7 @@ class RDPClient:
|
||||
options.append(f"{i} Monitors")
|
||||
|
||||
if monitor_count > 1:
|
||||
options.append("Custom...")
|
||||
options.extend(["All Monitors", "Span"])
|
||||
|
||||
return options
|
||||
@@ -641,9 +696,6 @@ class RDPClient:
|
||||
# Add tooltip information for shortcuts
|
||||
self._add_keyboard_shortcuts_info()
|
||||
|
||||
# Add tooltip information for shortcuts
|
||||
self._add_keyboard_shortcuts_info()
|
||||
|
||||
def _add_keyboard_shortcuts_info(self):
|
||||
"""Add keyboard shortcuts information to the status bar or help"""
|
||||
def show_shortcuts_hint(event):
|
||||
@@ -689,6 +741,7 @@ Multi-Monitor Support:
|
||||
• "2 Monitors" - Use first 2 monitors (0,1)
|
||||
• "3 Monitors" - Use first 3 monitors (0,1,2)
|
||||
• "4 Monitors" - Use first 4 monitors (0,1,2,3)
|
||||
• "Custom..." - Choose specific monitors via dialog
|
||||
• "All Monitors" - Use all available monitors
|
||||
• "Span" - Span desktop across monitors
|
||||
• "No" - Single monitor only"""
|
||||
@@ -718,7 +771,10 @@ Multi-Monitor Support:
|
||||
self.details_labels["domain"].config(text=conn.get("domain", "N/A"))
|
||||
self.details_labels["resolution"].config(text=conn.get("resolution", "1920x1080"))
|
||||
self.details_labels["color_depth"].config(text=f"{conn.get('color_depth', 32)}-bit")
|
||||
self.details_labels["multimon"].config(text=conn.get("multimon", "No"))
|
||||
multimon_text = conn.get("multimon", "No")
|
||||
if multimon_text == "Custom..." and conn.get("monitor_ids"):
|
||||
multimon_text = f"Custom (Monitors: {conn['monitor_ids']})"
|
||||
self.details_labels["multimon"].config(text=multimon_text)
|
||||
self.details_labels["sound"].config(text=conn.get("sound", "Yes"))
|
||||
self.details_labels["clipboard"].config(text=conn.get("clipboard", "Yes"))
|
||||
self.details_labels["drives"].config(text=conn.get("drives", "No"))
|
||||
@@ -928,12 +984,14 @@ Multi-Monitor Support:
|
||||
|
||||
# Check monitor configuration first to determine resolution handling
|
||||
multimon = conn.get("multimon", "No")
|
||||
use_specific_monitors = multimon in ["2 Monitors", "3 Monitors", "4 Monitors"]
|
||||
use_specific_monitors = multimon in ["2 Monitors", "3 Monitors", "4 Monitors", "Custom..."]
|
||||
|
||||
# Get monitor list if needed
|
||||
monitor_list = None
|
||||
if use_specific_monitors:
|
||||
if multimon == "2 Monitors":
|
||||
if multimon == "Custom...":
|
||||
monitor_list = conn.get("monitor_ids", "0")
|
||||
elif multimon == "2 Monitors":
|
||||
monitor_list = self._get_best_monitor_selection(2)
|
||||
elif multimon == "3 Monitors":
|
||||
monitor_list = self._get_best_monitor_selection(3)
|
||||
@@ -1354,6 +1412,73 @@ Multi-Monitor Support:
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
class MonitorSelectionDialog:
|
||||
"""Dialog for selecting specific monitors via checkboxes"""
|
||||
|
||||
def __init__(self, parent, monitors, preselected_ids=None):
|
||||
"""
|
||||
Args:
|
||||
parent: Parent window
|
||||
monitors: List of dicts with keys: id, name, resolution, position, is_primary
|
||||
preselected_ids: List of monitor IDs to pre-check, or None for all
|
||||
"""
|
||||
self.result = None # Will be set to comma-separated IDs string on OK
|
||||
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Select Monitors")
|
||||
self.dialog.resizable(False, False)
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
main_frame = ttk.Frame(self.dialog, padding="20")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(main_frame, text="Select which monitors to use:",
|
||||
font=('Segoe UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 10))
|
||||
|
||||
# Build checkboxes for each monitor
|
||||
self.check_vars = {}
|
||||
for mon in monitors:
|
||||
mid = mon['id']
|
||||
primary_tag = " (primary)" if mon['is_primary'] else ""
|
||||
label = f"Monitor {mid}: {mon['name']} {mon['resolution']} {mon['position']}{primary_tag}"
|
||||
|
||||
var = tk.BooleanVar(value=(preselected_ids is None or mid in preselected_ids))
|
||||
self.check_vars[mid] = var
|
||||
|
||||
cb = ttk.Checkbutton(main_frame, text=label, variable=var)
|
||||
cb.pack(anchor=tk.W, pady=2)
|
||||
|
||||
# Buttons
|
||||
btn_frame = ttk.Frame(main_frame)
|
||||
btn_frame.pack(fill=tk.X, pady=(15, 0))
|
||||
|
||||
ttk.Button(btn_frame, text="Cancel", command=self._cancel).pack(side=tk.RIGHT, padx=(10, 0))
|
||||
ttk.Button(btn_frame, text="OK", command=self._ok).pack(side=tk.RIGHT)
|
||||
|
||||
# Size and center dialog
|
||||
self.dialog.update_idletasks()
|
||||
w = self.dialog.winfo_reqwidth() + 40
|
||||
h = self.dialog.winfo_reqheight() + 20
|
||||
x = (self.dialog.winfo_screenwidth() // 2) - (w // 2)
|
||||
y = (self.dialog.winfo_screenheight() // 2) - (h // 2)
|
||||
self.dialog.geometry(f"{w}x{h}+{x}+{y}")
|
||||
|
||||
self.dialog.wait_window()
|
||||
|
||||
def _ok(self):
|
||||
selected = [mid for mid, var in self.check_vars.items() if var.get()]
|
||||
if not selected:
|
||||
messagebox.showwarning("No Selection", "Please select at least one monitor.",
|
||||
parent=self.dialog)
|
||||
return
|
||||
self.result = ','.join(str(m) for m in sorted(selected))
|
||||
self.dialog.destroy()
|
||||
|
||||
def _cancel(self):
|
||||
self.dialog.destroy()
|
||||
|
||||
|
||||
class ConnectionDialog:
|
||||
def __init__(self, parent, title, initial_data=None, rdp_client=None):
|
||||
self.result = None
|
||||
@@ -1450,6 +1575,12 @@ class ConnectionDialog:
|
||||
("Network Detection:", "network_auto_detect", "combo", ["Yes", "No"]),
|
||||
]
|
||||
|
||||
# Hidden field for custom monitor IDs
|
||||
self.fields["monitor_ids"] = tk.StringVar(value=self.initial_data.get("monitor_ids", ""))
|
||||
|
||||
self._multimon_combo = None
|
||||
self._monitor_ids_label = None
|
||||
|
||||
for i, (label, field, widget_type, values) in enumerate(advanced_fields):
|
||||
row = i + 1 # Offset by 1 for description label
|
||||
ttk.Label(advanced_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=5, padx=(10, 5))
|
||||
@@ -1459,6 +1590,16 @@ class ConnectionDialog:
|
||||
widget.grid(row=row, column=1, sticky=tk.W, pady=5, padx=(5, 10))
|
||||
self.fields[field] = var
|
||||
|
||||
if field == "multimon":
|
||||
self._multimon_combo = widget
|
||||
# Add label to show current custom monitor selection
|
||||
self._monitor_ids_label = ttk.Label(advanced_frame, text="", font=('Segoe UI', 8, 'italic'))
|
||||
self._monitor_ids_label.grid(row=row, column=2, sticky=tk.W, padx=(5, 0))
|
||||
# Update label if initial data has Custom...
|
||||
if self.initial_data.get("multimon") == "Custom..." and self.initial_data.get("monitor_ids"):
|
||||
self._monitor_ids_label.config(text=f"Monitors: {self.initial_data['monitor_ids']}")
|
||||
widget.bind("<<ComboboxSelected>>", self._on_multimon_changed)
|
||||
|
||||
# Performance fields
|
||||
performance_fields = [
|
||||
("Compression:", "compression", "combo", ["Yes", "No"]),
|
||||
@@ -1488,6 +1629,44 @@ class ConnectionDialog:
|
||||
ttk.Button(button_frame, text="Cancel", command=self._cancel).pack(side=tk.RIGHT, padx=(10, 0))
|
||||
ttk.Button(button_frame, text="Save", command=self._save).pack(side=tk.RIGHT)
|
||||
|
||||
def _on_multimon_changed(self, event=None):
|
||||
"""Handle multimon dropdown changes - open monitor dialog for Custom..."""
|
||||
value = self.fields["multimon"].get()
|
||||
if value == "Custom...":
|
||||
if not self.rdp_client:
|
||||
messagebox.showwarning("Error", "Cannot detect monitors.", parent=self.dialog)
|
||||
return
|
||||
|
||||
monitors = self.rdp_client._get_monitor_details()
|
||||
if not monitors:
|
||||
messagebox.showwarning("No Monitors", "Could not detect any monitors.", parent=self.dialog)
|
||||
self.fields["multimon"].set("No")
|
||||
return
|
||||
|
||||
# Parse preselected IDs from stored value
|
||||
preselected = None
|
||||
stored = self.fields["monitor_ids"].get()
|
||||
if stored:
|
||||
try:
|
||||
preselected = [int(x.strip()) for x in stored.split(',')]
|
||||
except ValueError:
|
||||
preselected = None
|
||||
|
||||
dlg = MonitorSelectionDialog(self.dialog, monitors, preselected)
|
||||
if dlg.result:
|
||||
self.fields["monitor_ids"].set(dlg.result)
|
||||
self._monitor_ids_label.config(text=f"Monitors: {dlg.result}")
|
||||
else:
|
||||
# User cancelled - revert to previous selection if no monitor_ids stored
|
||||
if not self.fields["monitor_ids"].get():
|
||||
self.fields["multimon"].set("No")
|
||||
self._monitor_ids_label.config(text="")
|
||||
else:
|
||||
# Clear monitor_ids and label for non-custom selections
|
||||
self.fields["monitor_ids"].set("")
|
||||
if self._monitor_ids_label:
|
||||
self._monitor_ids_label.config(text="")
|
||||
|
||||
def _save(self):
|
||||
"""Save the connection"""
|
||||
# Validate required fields
|
||||
@@ -1503,6 +1682,11 @@ class ConnectionDialog:
|
||||
messagebox.showerror("Error", "Username is required.")
|
||||
return
|
||||
|
||||
# Validate Custom... has monitor_ids
|
||||
if self.fields["multimon"].get() == "Custom..." and not self.fields["monitor_ids"].get():
|
||||
messagebox.showerror("Error", "Please select monitors for Custom... mode.", parent=self.dialog)
|
||||
return
|
||||
|
||||
# Collect data
|
||||
self.result = {}
|
||||
for field, var in self.fields.items():
|
||||
|
||||
Reference in New Issue
Block a user