From 69c6bbfd62bc2bcfa0cc3f71b976efeb53903daa Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Fri, 9 Aug 2024 03:00:15 -0700 Subject: [PATCH 1/4] add ability to set primary monitor --- src/lib.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++-- wmutil.pyi | 4 +++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8dd6e1..8aabfd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,22 +6,25 @@ use std::{io, mem, ptr}; use std::collections::VecDeque; use std::ffi::{c_void, OsString}; use std::hash::Hash; -use std::ops::BitAnd; +use std::ops::{BitAnd, Neg}; use std::ops::Deref; use std::os::windows::prelude::OsStringExt; use std::sync::OnceLock; - +use std::mem::size_of; +use std::ptr::{null, null_mut}; use dpi::{PhysicalPosition, PhysicalSize}; use pyo3::prelude::*; use pyo3::pymodule; use windows_sys::core::HRESULT; -use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT, S_OK}; +use windows_sys::Win32::Foundation::{BOOL, HWND, WPARAM, LPARAM, POINT, RECT, POINTL, S_OK}; use windows_sys::Win32::Graphics::Gdi::{ DEVMODEW, ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, HDC, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTOPRIMARY, MonitorFromPoint, MonitorFromWindow, MONITORINFO, MONITORINFOEXW, }; +use windows_sys::Win32::Graphics::Gdi::*; + use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; use windows_sys::Win32::UI::HiDpi::{ MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE, @@ -242,6 +245,7 @@ unsafe extern "system" fn monitor_enum_proc( // Python bindings #[pyclass(module = "wmutil")] +#[derive(Clone)] struct Monitor { monitor_handle: MonitorHandle, } @@ -284,6 +288,11 @@ impl Monitor { self.monitor_handle.0 as isize } + pub fn set_primary(&self) -> PyResult<()> { + set_primary_monitor(self.name()); + Ok(()) + } + pub fn __hash__(&self) -> isize { self.handle() } @@ -336,6 +345,90 @@ fn get_monitor_from_point(x: i32, y: i32) -> Monitor { } } +fn wide_string(s: &str) -> Vec { + let mut vec: Vec = s.encode_utf16().collect(); + vec.push(0); + vec +} + + +fn get_dev_mode(display_name: &str) -> Result { + let mut devmode: DEVMODEW = unsafe { std::mem::zeroed() }; + devmode.dmSize = size_of::() as u16; + + let wide_name = wide_string(display_name); + + let success = unsafe { + EnumDisplaySettingsW(wide_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut devmode) + }; + + if success == 0 { + return Err(format!("Failed to retrieve settings for display: {}", display_name)); + } + + Ok(devmode) +} + + +#[pyfunction] +fn set_primary_monitor(display_name: String) -> PyResult { + let all_monitors = enumerate_monitors(); + let mut maybe_this_monitor: Option = None; + for monitor in all_monitors.clone() { + if monitor.name() == display_name { + maybe_this_monitor = Some(monitor); + break + } + } + + assert!(maybe_this_monitor.is_some()); + + let this_monitor = maybe_this_monitor.unwrap(); + + let (this_x, this_y) = this_monitor.position(); + + if (this_x == 0 && this_y == 0) { + // the requested monitor is already the primary monitor + return Ok(true) + } + + let x_offset = this_x.neg(); + let y_offset = this_y.neg(); + + let display_name_string = display_name.as_str(); + let wide_name = wide_string(display_name_string); + + for monitor in all_monitors.clone() { + if monitor.name() != display_name { + let mut devmode: DEVMODEW = get_dev_mode(monitor.name().as_str()).unwrap(); + unsafe { + let (monitor_x, monitor_y) = monitor.position(); + let new_x = monitor_x + x_offset; + let new_y = monitor_y + y_offset; + devmode.Anonymous1.Anonymous2.dmPosition = POINTL { x: new_x, y: new_y }; + // println!("display: {} old: {} {} new: {} {}", monitor.name(), monitor_x, monitor_y, new_x, new_y); + ChangeDisplaySettingsExW(wide_string(monitor.name().as_str()).as_ptr(), &mut devmode, 0, CDS_UPDATEREGISTRY | CDS_NORESET, null_mut()); + } + } + } + let mut devmode: DEVMODEW = get_dev_mode(display_name_string).unwrap(); + unsafe { + // println!("{} being set as primary to 0 0", display_name); + devmode.Anonymous1.Anonymous2.dmPosition = POINTL { x: 0, y: 0 }; + ChangeDisplaySettingsExW(wide_name.as_ptr(), &mut devmode, 0, CDS_SET_PRIMARY | CDS_UPDATEREGISTRY | CDS_NORESET, null_mut()); + } + + let result = unsafe { + ChangeDisplaySettingsExW(null_mut(), null_mut(), 0, 0, null_mut()) + }; + if result == DISP_CHANGE_SUCCESSFUL { + Ok(true) + } else { + Ok(false) + } +} + + #[pymodule] fn wmutil(_py: Python, m: &PyModule) -> PyResult<()> { @@ -344,6 +437,7 @@ fn wmutil(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_window_monitor, m)?); m.add_function(wrap_pyfunction!(get_primary_monitor, m)?); m.add_function(wrap_pyfunction!(get_monitor_from_point, m)?); + m.add_function(wrap_pyfunction!(set_primary_monitor, m)?); Ok(()) } diff --git a/wmutil.pyi b/wmutil.pyi index a0ae37f..0f5c7ac 100644 --- a/wmutil.pyi +++ b/wmutil.pyi @@ -12,8 +12,12 @@ class Monitor: @property def handle(self) -> int: ... + def set_primary(self) -> None: ... + def get_primary_monitor() -> Monitor: ... def get_window_monitor(hwnd: int) -> Monitor: ... def enumerate_monitors() -> list[Monitor]: ... def get_monitor_from_point(x: int, y: int) -> Monitor: ... + +def set_primary_monitor(display_name: str) -> None: ... \ No newline at end of file From a061e3dd1bfabfa484b9e7a14cc15dff107b9bf0 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Fri, 9 Aug 2024 03:08:43 -0700 Subject: [PATCH 2/4] improve error message --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8aabfd2..746b5c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -381,7 +381,8 @@ fn set_primary_monitor(display_name: String) -> PyResult { } } - assert!(maybe_this_monitor.is_some()); + // todo: raise a proper exception instead of a panic exception + assert!(maybe_this_monitor.is_some(), "Monitor with name {:?} not found", display_name); let this_monitor = maybe_this_monitor.unwrap(); From 0f4faf13b55a322d3868599b13a8322641f66233 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Fri, 9 Aug 2024 03:09:19 -0700 Subject: [PATCH 3/4] update readme --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index cde48a6..8b9c3e7 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,23 @@ it is the primary monitor Untitled - Notepad is using monitor \\.\DISPLAY2 ``` +**Changing the primary monitor:** + +You can use the `set_primary_monitor` function, which accepts a display name or you can use the `set_primary` method of a `Monitor` object to change the +primary monitor. If the monitor is already the primary monitor, no change will be made and the operation is considered successful. Returns `True` when successful and +`False` when not successful. If an invalid monitor name is given, an exception is raised. + +```python +import wmutil +monitor: wmutil.Monitor # assume this is already defined + +wmutil.set_primary_monitor(monitor.name) +# or +monitor.set_primary() +``` + + + Notes: - `monitor.size` may not necessarily reflect the monitor's resolution, but rather is the geometry used for drawing or moving windows From 5e40d059dbd77b5247b1a0ac56f53581e3cab857 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Fri, 9 Aug 2024 03:10:22 -0700 Subject: [PATCH 4/4] 0.2.0 :package: --- Cargo.lock | 2 +- Cargo.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c87a16f..c246fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,7 +284,7 @@ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "windows-monitor-functions" -version = "0.1.1" +version = "0.2.0" dependencies = [ "dpi", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 693522d..97c348e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windows-monitor-functions" -version = "0.1.1" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/pyproject.toml b/pyproject.toml index c518b8f..b939639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = 'maturin' [project] name = "wmutil" license = { file = "LICENSE" } -version = "0.1.1" +version = "0.2.0" authors = [ {name = "Spencer Phillip Young", email="spencer.young@spyoung.com"} ]