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:
212
rdp_client.py
212
rdp_client.py
@@ -324,6 +324,60 @@ class RDPClient:
|
|||||||
pass
|
pass
|
||||||
return 1 # Default to 1 monitor if detection fails
|
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):
|
def _get_multimon_options(self):
|
||||||
"""Get available multi-monitor options based on system"""
|
"""Get available multi-monitor options based on system"""
|
||||||
monitor_count = self._get_available_monitors_count()
|
monitor_count = self._get_available_monitors_count()
|
||||||
@@ -334,6 +388,7 @@ class RDPClient:
|
|||||||
options.append(f"{i} Monitors")
|
options.append(f"{i} Monitors")
|
||||||
|
|
||||||
if monitor_count > 1:
|
if monitor_count > 1:
|
||||||
|
options.append("Custom...")
|
||||||
options.extend(["All Monitors", "Span"])
|
options.extend(["All Monitors", "Span"])
|
||||||
|
|
||||||
return options
|
return options
|
||||||
@@ -641,9 +696,6 @@ class RDPClient:
|
|||||||
# Add tooltip information for shortcuts
|
# Add tooltip information for shortcuts
|
||||||
self._add_keyboard_shortcuts_info()
|
self._add_keyboard_shortcuts_info()
|
||||||
|
|
||||||
# Add tooltip information for shortcuts
|
|
||||||
self._add_keyboard_shortcuts_info()
|
|
||||||
|
|
||||||
def _add_keyboard_shortcuts_info(self):
|
def _add_keyboard_shortcuts_info(self):
|
||||||
"""Add keyboard shortcuts information to the status bar or help"""
|
"""Add keyboard shortcuts information to the status bar or help"""
|
||||||
def show_shortcuts_hint(event):
|
def show_shortcuts_hint(event):
|
||||||
@@ -687,8 +739,9 @@ Tips:
|
|||||||
|
|
||||||
Multi-Monitor Support:
|
Multi-Monitor Support:
|
||||||
• "2 Monitors" - Use first 2 monitors (0,1)
|
• "2 Monitors" - Use first 2 monitors (0,1)
|
||||||
• "3 Monitors" - Use first 3 monitors (0,1,2)
|
• "3 Monitors" - Use first 3 monitors (0,1,2)
|
||||||
• "4 Monitors" - Use first 4 monitors (0,1,2,3)
|
• "4 Monitors" - Use first 4 monitors (0,1,2,3)
|
||||||
|
• "Custom..." - Choose specific monitors via dialog
|
||||||
• "All Monitors" - Use all available monitors
|
• "All Monitors" - Use all available monitors
|
||||||
• "Span" - Span desktop across monitors
|
• "Span" - Span desktop across monitors
|
||||||
• "No" - Single monitor only"""
|
• "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["domain"].config(text=conn.get("domain", "N/A"))
|
||||||
self.details_labels["resolution"].config(text=conn.get("resolution", "1920x1080"))
|
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["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["sound"].config(text=conn.get("sound", "Yes"))
|
||||||
self.details_labels["clipboard"].config(text=conn.get("clipboard", "Yes"))
|
self.details_labels["clipboard"].config(text=conn.get("clipboard", "Yes"))
|
||||||
self.details_labels["drives"].config(text=conn.get("drives", "No"))
|
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
|
# Check monitor configuration first to determine resolution handling
|
||||||
multimon = conn.get("multimon", "No")
|
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
|
# Get monitor list if needed
|
||||||
monitor_list = None
|
monitor_list = None
|
||||||
if use_specific_monitors:
|
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)
|
monitor_list = self._get_best_monitor_selection(2)
|
||||||
elif multimon == "3 Monitors":
|
elif multimon == "3 Monitors":
|
||||||
monitor_list = self._get_best_monitor_selection(3)
|
monitor_list = self._get_best_monitor_selection(3)
|
||||||
@@ -1354,6 +1412,73 @@ Multi-Monitor Support:
|
|||||||
self.root.mainloop()
|
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:
|
class ConnectionDialog:
|
||||||
def __init__(self, parent, title, initial_data=None, rdp_client=None):
|
def __init__(self, parent, title, initial_data=None, rdp_client=None):
|
||||||
self.result = None
|
self.result = None
|
||||||
@@ -1449,15 +1574,31 @@ class ConnectionDialog:
|
|||||||
("Gateway Mode:", "gateway", "combo", ["Auto", "RPC", "HTTP"]),
|
("Gateway Mode:", "gateway", "combo", ["Auto", "RPC", "HTTP"]),
|
||||||
("Network Detection:", "network_auto_detect", "combo", ["Yes", "No"]),
|
("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):
|
for i, (label, field, widget_type, values) in enumerate(advanced_fields):
|
||||||
row = i + 1 # Offset by 1 for description label
|
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))
|
ttk.Label(advanced_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=5, padx=(10, 5))
|
||||||
|
|
||||||
var = tk.StringVar(value=self.initial_data.get(field, values[0]))
|
var = tk.StringVar(value=self.initial_data.get(field, values[0]))
|
||||||
widget = ttk.Combobox(advanced_frame, textvariable=var, values=values, width=37, state="readonly")
|
widget = ttk.Combobox(advanced_frame, textvariable=var, values=values, width=37, state="readonly")
|
||||||
widget.grid(row=row, column=1, sticky=tk.W, pady=5, padx=(5, 10))
|
widget.grid(row=row, column=1, sticky=tk.W, pady=5, padx=(5, 10))
|
||||||
self.fields[field] = var
|
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
|
||||||
performance_fields = [
|
performance_fields = [
|
||||||
@@ -1488,26 +1629,69 @@ class ConnectionDialog:
|
|||||||
ttk.Button(button_frame, text="Cancel", command=self._cancel).pack(side=tk.RIGHT, padx=(10, 0))
|
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)
|
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):
|
def _save(self):
|
||||||
"""Save the connection"""
|
"""Save the connection"""
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if not self.fields["name"].get().strip():
|
if not self.fields["name"].get().strip():
|
||||||
messagebox.showerror("Error", "Connection name is required.")
|
messagebox.showerror("Error", "Connection name is required.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.fields["server"].get().strip():
|
if not self.fields["server"].get().strip():
|
||||||
messagebox.showerror("Error", "Server/IP is required.")
|
messagebox.showerror("Error", "Server/IP is required.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.fields["username"].get().strip():
|
if not self.fields["username"].get().strip():
|
||||||
messagebox.showerror("Error", "Username is required.")
|
messagebox.showerror("Error", "Username is required.")
|
||||||
return
|
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
|
# Collect data
|
||||||
self.result = {}
|
self.result = {}
|
||||||
for field, var in self.fields.items():
|
for field, var in self.fields.items():
|
||||||
self.result[field] = var.get().strip()
|
self.result[field] = var.get().strip()
|
||||||
|
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
def _cancel(self):
|
def _cancel(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user