diff --git a/docs/Changelog.md b/docs/Changelog.md index 0a3e00f..e9ebac2 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -4,8 +4,9 @@ - New tutorial: [Extending mpv and mpv.net via Lua scripting](https://github.com/stax76/mpv.net/wiki/Extending-mpv-and-mpv.net-via-Lua-scripting) - New options `autofit-image` and `autofit-audio`, like autofit but used for image and audio files. Default 80. - New [auto-mode](https://github.com/stax76/mpv-scripts) script to use mpv and mpv.net as image viewer and audio player. +- New support of the mpv option `snap-window`. - Fix long commands causing key bindings not visible in the command palette. -- Fix compatibility with mpv-osc-tethys, which is a new osc script. +- Fix script compatibility with mordenx and mpv-osc-tethys. - Fix borderless window not resizable with mouse. diff --git a/docs/Manual.md b/docs/Manual.md index fa63bb6..2a9c048 100644 --- a/docs/Manual.md +++ b/docs/Manual.md @@ -434,12 +434,6 @@ Window size is remembered in the current session. **always** Window size is always remembered. -#### --start-threshold=\ - -Threshold in milliseconds to wait for libmpv returning the video -resolution before the window is shown, otherwise default dimensions -are used as defined by autofit and start-size. Default: 1500 - #### --minimum-aspect-ratio=\ Minimum aspect ratio, if the AR is smaller than the defined value then @@ -450,6 +444,11 @@ with cover art. Default: 0 Save the window position on exit. Default: no +#### --start-threshold=\ + +Threshold in milliseconds to wait for libmpv returning the video +resolution before the window is shown, otherwise default dimensions +are used as defined by autofit and start-size. Default: 1500 ### Playback @@ -766,6 +765,7 @@ https://mpv.io/manual/master/#window - [keepaspect-window](https://mpv.io/manual/master/#options-keepaspect-window) - [ontop](https://mpv.io/manual/master/#options-ontop) - [screen](https://mpv.io/manual/master/#options-screen) +- [snap-window](https://mpv.io/manual/master/#options-snap-window) - [title](https://mpv.io/manual/master/#options-title) - [window-maximized](https://mpv.io/manual/master/#options-window-maximized) - [window-minimized](https://mpv.io/manual/master/#options-window-minimized) @@ -774,9 +774,12 @@ https://mpv.io/manual/master/#window **Partly implemented are:** -- [autofit-larger](https://mpv.io/manual/master/#options-autofit-larger) -- [autofit-smaller](https://mpv.io/manual/master/#options-autofit-smaller) -- [autofit](https://mpv.io/manual/master/#options-autofit) +- [autofit-larger](https://mpv.io/manual/master/#options-autofit-larger) + Supported is a single integer value in the range 0-100. +- [autofit-smaller](https://mpv.io/manual/master/#options-autofit-smaller) + Supported is a single integer value in the range 0-100. +- [autofit](https://mpv.io/manual/master/#options-autofit) + Supported is a single integer value in the range 0-100. mpv.net specific window features are documented in the [screen section](#screen). diff --git a/src/Misc/Player.cs b/src/Misc/Player.cs index d0ee5c9..143e446 100644 --- a/src/Misc/Player.cs +++ b/src/Misc/Player.cs @@ -98,6 +98,7 @@ namespace mpvnet public bool KeepaspectWindow { get; set; } public bool Paused { get; set; } public bool Shown { get; set; } + public bool SnapWindow { get; set; } public bool TaskbarProgress { get; set; } = true; public bool WasInitialSizeSet; public bool WindowMaximized { get; set; } @@ -268,16 +269,17 @@ namespace mpvnet AutofitLarger = result / 100f; } break; + case "border": Border = value == "yes"; break; case "fs": case "fullscreen": Fullscreen = value == "yes"; break; - case "border": Border = value == "yes"; break; + case "gpu-api": GPUAPI = value; break; case "keepaspect-window": KeepaspectWindow = value == "yes"; break; + case "screen": Screen = Convert.ToInt32(value); break; + case "snap-window": SnapWindow = value == "yes"; break; + case "taskbar-progress": TaskbarProgress = value == "yes"; break; + case "vo": VO = value; break; case "window-maximized": WindowMaximized = value == "yes"; break; case "window-minimized": WindowMinimized = value == "yes"; break; - case "taskbar-progress": TaskbarProgress = value == "yes"; break; - case "screen": Screen = Convert.ToInt32(value); break; - case "gpu-api": GPUAPI = value; break; - case "vo": VO = value; break; } if (AutofitLarger > 1) diff --git a/src/Native/Native.cs b/src/Native/Native.cs index df545fb..05e95d1 100644 --- a/src/Native/Native.cs +++ b/src/Native/Native.cs @@ -21,6 +21,9 @@ namespace mpvnet [DllImport("user32.dll")] public static extern uint ActivateKeyboardLayout(IntPtr hkl, uint flags); + [DllImport("user32.dll")] + public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr FindWindowEx( IntPtr parentHandle, IntPtr childAfter, string lclassName, string windowTitle); @@ -88,6 +91,37 @@ namespace mpvnet [DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + [DllImport("dwmapi.dll")] + public static extern int DwmGetWindowAttribute( + IntPtr hwnd, uint dwAttribute, out RECT pvAttribute, uint cbAttribute); + + public static bool GetDwmWindowRect(IntPtr handle, out RECT rect) + { + const uint DWMWA_EXTENDED_FRAME_BOUNDS = 9; + return 0 == DwmGetWindowAttribute(handle, DWMWA_EXTENDED_FRAME_BOUNDS, out rect, (uint)Marshal.SizeOf()); + } + + public static Rectangle GetWorkingArea(IntPtr handle, Rectangle workingArea) + { + if (handle != IntPtr.Zero && GetDwmWindowRect(handle, out RECT dwmRect) && + GetWindowRect(handle, out RECT rect)) + { + int left = workingArea.Left; + int top = workingArea.Top; + int right = workingArea.Right; + int bottom = workingArea.Bottom; + + left += rect.Left - dwmRect.Left; + top -= rect.Top - dwmRect.Top; + right -= dwmRect.Right - rect.Right; + bottom -= dwmRect.Bottom - rect.Bottom; + + return new Rectangle(left, top, right - left, bottom - top); + } + + return workingArea; + } + [StructLayout(LayoutKind.Sequential)] public struct RECT { @@ -116,6 +150,16 @@ namespace mpvnet public Size Size => new Size(Right - Left, Bottom - Top); public int Width => Right - Left; public int Height => Bottom - Top; + + public static RECT FromRectangle(Rectangle rect) + { + return new RECT(rect.X, rect.Y, rect.X + rect.Width, rect.Y + rect.Height); + } + + public override string ToString() + { + return "{Left=" + Left + ",Top=" + Top + ",Right=" + Right + ",Bottom=" + Bottom + "}"; + } } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Resources/editor_conf.txt b/src/Resources/editor_conf.txt index 38736b8..6ea1064 100644 --- a/src/Resources/editor_conf.txt +++ b/src/Resources/editor_conf.txt @@ -459,12 +459,6 @@ help = keepaspect-window will lock the window size to the video aspect. Default: option = yes option = no -[setting] -name = start-threshold -file = mpvnet -filter = Screen -help = Threshold in milliseconds to wait for libmpv returning the video resolution before the window is shown, otherwise default dimensions are used as defined by autofit and start-size. Default: 1500 (mpv.net specific option) - [setting] name = minimum-aspect-ratio file = mpvnet @@ -481,6 +475,16 @@ help = Save the window position on exit. (mpv.net specific option) option = yes option = no +[setting] +name = snap-window +file = mpv +default = no +filter = Screen +help = Snap the player window to screen edges. + +option = yes +option = no + [setting] name = window-maximized file = mpv @@ -491,6 +495,12 @@ help = Start with a maximized window. option = yes option = no +[setting] +name = start-threshold +file = mpvnet +filter = Screen +help = Threshold in milliseconds to wait for libmpv returning the video resolution before the window is shown, otherwise default dimensions are used as defined by autofit and start-size. Default: 1500 (mpv.net specific option) + [setting] name = taskbar-progress file = mpv diff --git a/src/Misc/MainForm.Designer.cs b/src/WinForms/MainForm.Designer.cs similarity index 100% rename from src/Misc/MainForm.Designer.cs rename to src/WinForms/MainForm.Designer.cs diff --git a/src/Misc/MainForm.cs b/src/WinForms/MainForm.cs similarity index 96% rename from src/Misc/MainForm.cs rename to src/WinForms/MainForm.cs index e2af0d2..052c05f 100644 --- a/src/Misc/MainForm.cs +++ b/src/WinForms/MainForm.cs @@ -21,6 +21,7 @@ namespace mpvnet { public partial class MainForm : Form { + public SnapManager SnapManager = new SnapManager(); public ElementHost CommandPaletteHost { get; set; } public IntPtr mpvWindowHandle { get; set; } public static MainForm Instance { get; set; } @@ -221,7 +222,7 @@ namespace mpvnet if (FormBorderStyle == FormBorderStyle.None) top = ClientSize.Height * 0.1f; - return pos.Y > ClientSize.Height * 0.8 || pos.Y < top; + return pos.Y > ClientSize.Height * 0.78 || pos.Y < top; } void UpdateMenu() @@ -432,13 +433,14 @@ namespace mpvnet } Screen screen = Screen.FromControl(this); - int autoFitHeight = Convert.ToInt32(screen.WorkingArea.Height * Core.Autofit); + Rectangle workingArea = GetWorkingArea(Handle, screen.WorkingArea); + int autoFitHeight = Convert.ToInt32(workingArea.Height * Core.Autofit); if (App.AutofitAudio > 1) App.AutofitAudio = 1; if (App.AutofitImage > 1) App.AutofitImage = 1; - if (Core.IsAudio) autoFitHeight = Convert.ToInt32(screen.WorkingArea.Height * App.AutofitAudio); - if (Core.IsImage) autoFitHeight = Convert.ToInt32(screen.WorkingArea.Height * App.AutofitImage); + if (Core.IsAudio) autoFitHeight = Convert.ToInt32(workingArea.Height * App.AutofitAudio); + if (Core.IsImage) autoFitHeight = Convert.ToInt32(workingArea.Height * App.AutofitImage); if (Core.VideoSize.Height == 0 || Core.VideoSize.Width == 0 || Core.VideoSize.Width / (float)Core.VideoSize.Height < App.MinimumAspectRatio) @@ -509,8 +511,10 @@ namespace mpvnet void SetSize(int width, int height, Screen screen, bool checkAutofit = true) { - int maxHeight = screen.WorkingArea.Height - (Height - ClientSize.Height) - 2; - int maxWidth = screen.WorkingArea.Width - (Width - ClientSize.Width); + Rectangle workingArea = GetWorkingArea(Handle, screen.WorkingArea); + + int maxHeight = workingArea.Height - (Height - ClientSize.Height) - 2; + int maxWidth = workingArea.Width - (Width - ClientSize.Width); int startWidth = width; int startHeight = height; @@ -565,10 +569,10 @@ namespace mpvnet Screen[] screens = Screen.AllScreens; - int minLeft = screens.Select(val => val.WorkingArea.X).Min(); - int maxRight = screens.Select(val => val.WorkingArea.Right).Max(); - int minTop = screens.Select(val => val.WorkingArea.Y).Min(); - int maxBottom = screens.Select(val => val.WorkingArea.Bottom).Max(); + int minLeft = screens.Select(val => GetWorkingArea(Handle, val.WorkingArea).X).Min(); + int maxRight = screens.Select(val => GetWorkingArea(Handle, val.WorkingArea).Right).Max(); + int minTop = screens.Select(val => GetWorkingArea(Handle, val.WorkingArea).Y).Min(); + int maxBottom = screens.Select(val => GetWorkingArea(Handle, val.WorkingArea).Bottom).Max(); if (left < minLeft) left = minLeft; @@ -629,7 +633,7 @@ namespace mpvnet public int GetHorizontalLocation(Screen screen) { - Rectangle workingArea = screen.WorkingArea; + Rectangle workingArea = GetWorkingArea(Handle, screen.WorkingArea); Rectangle rect = new Rectangle(Left - workingArea.X, Top - workingArea.Y, Width, Height); if (workingArea.Width / (float)Width < 1.1) @@ -646,7 +650,7 @@ namespace mpvnet public int GetVerticalLocation(Screen screen) { - Rectangle workingArea = screen.WorkingArea; + Rectangle workingArea = GetWorkingArea(Handle, screen.WorkingArea); Rectangle rect = new Rectangle(Left - workingArea.X, Top - workingArea.Y, Width, Height); if (workingArea.Height / (float)Height < 1.1) @@ -831,7 +835,7 @@ namespace mpvnet if (mpvWindowHandle != IntPtr.Zero) m.Result = SendMessage(mpvWindowHandle, m.Msg, m.WParam, m.LParam); break; - case 0x051: // WM_INPUTLANGCHANGE + case 0x51: // WM_INPUTLANGCHANGE ActivateKeyboardLayout(m.LParam, 0x00000100u /*KLF_SETFORPROCESS*/); break; case 0x319: // WM_APPCOMMAND @@ -846,10 +850,10 @@ namespace mpvnet } } break; - case 0x0312: // WM_HOTKEY + case 0x312: // WM_HOTKEY GlobalHotkey.Execute(m.WParam.ToInt32()); break; - case 0x0200: // WM_MOUSEMOVE + case 0x200: // WM_MOUSEMOVE if (Environment.TickCount - LastCycleFullscreen > 500) { Point pos = PointToClient(Cursor.Position); @@ -869,7 +873,7 @@ namespace mpvnet Core.Command($"mouse {pos.X} {pos.Y} 0 double"); } break; - case 0x02E0: // WM_DPICHANGED + case 0x2E0: // WM_DPICHANGED { if (!Core.Shown) break; @@ -878,7 +882,7 @@ namespace mpvnet SetWindowPos(Handle, IntPtr.Zero, rect.Left, rect.Top, rect.Width, rect.Height, 0); } break; - case 0x0214: // WM_SIZING + case 0x214: // WM_SIZING if (Core.KeepaspectWindow) { RECT rc = Marshal.PtrToStructure(m.LParam); @@ -906,7 +910,7 @@ namespace mpvnet m.Result = new IntPtr(1); } return; - case 0x004A: // WM_COPYDATA + case 0x4A: // WM_COPYDATA { var copyData = (COPYDATASTRUCT)m.GetLParam(typeof(COPYDATASTRUCT)); string[] args = copyData.lpData.Split('\n'); @@ -990,6 +994,15 @@ namespace mpvnet } } break; + case 0x231: // WM_ENTERSIZEMOVE + case 0x005: // WM_SIZE + if (Core.SnapWindow) + SnapManager.OnSizeAndEnterSizeMove(this); + break; + case 0x216: // WM_MOVING + if (Core.SnapWindow) + SnapManager.OnMoving(ref m); + break; } if (m.Msg == TaskbarButtonCreatedMessage && Core.TaskbarProgress) diff --git a/src/Misc/MainForm.resx b/src/WinForms/MainForm.resx similarity index 100% rename from src/Misc/MainForm.resx rename to src/WinForms/MainForm.resx diff --git a/src/WinForms/SnapManager.cs b/src/WinForms/SnapManager.cs new file mode 100644 index 0000000..7170490 --- /dev/null +++ b/src/WinForms/SnapManager.cs @@ -0,0 +1,81 @@ + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +using static mpvnet.Native; + +namespace mpvnet +{ + public class SnapManager + { + int DragOffsetX { get; set; } + int DragOffsetY { get; set; } + + IntPtr Handle; + + [Flags] + public enum SnapLocation + { + None = 0, + Left = 1 << 0, + Top = 1 << 1, + Right = 1 << 2, + Bottom = 1 << 3, + All = Left | Top | Right | Bottom + } + + public int AnchorDistance { get; set; } + + public int SnapDistance { get; set; } + + bool InSnapRange(int a, int b) => Math.Abs(a - b) < SnapDistance; + + void FindSnap(ref Rectangle effectiveBounds) + { + Screen currentScreen = Screen.FromPoint(effectiveBounds.Location); + Rectangle workingArea = GetWorkingArea(Handle, currentScreen.WorkingArea); + + if (InSnapRange(effectiveBounds.Left, workingArea.Left + AnchorDistance)) + effectiveBounds.X = workingArea.Left + AnchorDistance; + else if (InSnapRange(effectiveBounds.Right, workingArea.Right - AnchorDistance)) + effectiveBounds.X = workingArea.Right - AnchorDistance - effectiveBounds.Width; + if (InSnapRange(effectiveBounds.Top, workingArea.Top + AnchorDistance)) + effectiveBounds.Y = workingArea.Top + AnchorDistance; + else if (InSnapRange(effectiveBounds.Bottom, workingArea.Bottom - AnchorDistance)) + effectiveBounds.Y = workingArea.Bottom - AnchorDistance - effectiveBounds.Height; + } + + public void OnMoving(ref Message m) + { + if (Handle == IntPtr.Zero) + return; + + RECT boundsLtrb = Marshal.PtrToStructure(m.LParam); + Rectangle bounds = boundsLtrb.ToRectangle(); + // This is where the window _would_ be located if snapping + // had not occurred. This prevents the cursor from sliding + // off the title bar if the snap distance is too large. + Rectangle effectiveBounds = new Rectangle( + Cursor.Position.X - DragOffsetX, + Cursor.Position.Y - DragOffsetY, + bounds.Width, + bounds.Height); + FindSnap(ref effectiveBounds); + RECT newLtrb = RECT.FromRectangle(effectiveBounds); + Marshal.StructureToPtr(newLtrb, m.LParam, false); + m.Result = new IntPtr(1); + } + + public void OnSizeAndEnterSizeMove(Form form) + { + Handle = form.Handle; + SnapDistance = form.Font.Height; + // Need to handle window size changed as well when + // un-maximizing the form by dragging the title bar. + DragOffsetX = Cursor.Position.X - form.Left; + DragOffsetY = Cursor.Position.Y - form.Top; + } + } +} diff --git a/src/mpv.net.csproj b/src/mpv.net.csproj index 51e92b5..431779e 100644 --- a/src/mpv.net.csproj +++ b/src/mpv.net.csproj @@ -93,6 +93,7 @@ + @@ -186,10 +187,10 @@ Resources.resx - + Form - + MainForm.cs @@ -211,7 +212,7 @@ InputWindow.xaml - + MainForm.cs Designer