mirror of https://github.com/mpv-player/mpv
1727 lines
54 KiB
C
1727 lines
54 KiB
C
/*
|
|
* This file is part of mpv.
|
|
*
|
|
* mpv is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* mpv is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <assert.h>
|
|
#include <windows.h>
|
|
#include <windowsx.h>
|
|
#include <dwmapi.h>
|
|
#include <ole2.h>
|
|
#include <shobjidl.h>
|
|
#include <avrt.h>
|
|
|
|
#include "options/options.h"
|
|
#include "input/keycodes.h"
|
|
#include "input/input.h"
|
|
#include "input/event.h"
|
|
#include "stream/stream.h"
|
|
#include "common/msg.h"
|
|
#include "common/common.h"
|
|
#include "vo.h"
|
|
#include "win_state.h"
|
|
#include "w32_common.h"
|
|
#include "win32/displayconfig.h"
|
|
#include "win32/droptarget.h"
|
|
#include "osdep/io.h"
|
|
#include "osdep/threads.h"
|
|
#include "osdep/w32_keyboard.h"
|
|
#include "osdep/atomic.h"
|
|
#include "misc/dispatch.h"
|
|
#include "misc/rendezvous.h"
|
|
#include "mpv_talloc.h"
|
|
|
|
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
|
|
#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
|
|
|
|
#ifndef WM_DPICHANGED
|
|
#define WM_DPICHANGED (0x02E0)
|
|
#endif
|
|
|
|
#ifndef DPI_ENUMS_DECLARED
|
|
typedef enum MONITOR_DPI_TYPE {
|
|
MDT_EFFECTIVE_DPI = 0,
|
|
MDT_ANGULAR_DPI = 1,
|
|
MDT_RAW_DPI = 2,
|
|
MDT_DEFAULT = MDT_EFFECTIVE_DPI
|
|
} MONITOR_DPI_TYPE;
|
|
#endif
|
|
|
|
#define rect_w(r) ((r).right - (r).left)
|
|
#define rect_h(r) ((r).bottom - (r).top)
|
|
|
|
struct w32_api {
|
|
HRESULT (WINAPI *pGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*);
|
|
BOOL (WINAPI *pImmDisableIME)(DWORD);
|
|
};
|
|
|
|
struct vo_w32_state {
|
|
struct mp_log *log;
|
|
struct vo *vo;
|
|
struct mp_vo_opts *opts;
|
|
struct input_ctx *input_ctx;
|
|
|
|
pthread_t thread;
|
|
bool terminate;
|
|
struct mp_dispatch_queue *dispatch; // used to run stuff on the GUI thread
|
|
bool in_dispatch;
|
|
|
|
struct w32_api api; // stores functions from dynamically loaded DLLs
|
|
|
|
HWND window;
|
|
HWND parent; // 0 normally, set in embedding mode
|
|
HHOOK parent_win_hook;
|
|
HWINEVENTHOOK parent_evt_hook;
|
|
|
|
HMONITOR monitor; // Handle of the current screen
|
|
char *color_profile; // Path of the current screen's color profile
|
|
|
|
// Has the window seen a WM_DESTROY? If so, don't call DestroyWindow again.
|
|
bool destroyed;
|
|
|
|
// whether the window position and size were intialized
|
|
bool window_bounds_initialized;
|
|
|
|
bool current_fs;
|
|
bool toggle_fs; // whether the current fullscreen state needs to be switched
|
|
|
|
RECT windowrc; // currently known window rect
|
|
RECT screenrc; // current screen rect
|
|
// last non-fullscreen rect, updated only on fullscreen or on initialization
|
|
RECT prev_windowrc;
|
|
|
|
// video size
|
|
uint32_t o_dwidth;
|
|
uint32_t o_dheight;
|
|
|
|
int dpi;
|
|
|
|
bool disable_screensaver;
|
|
bool cursor_visible;
|
|
atomic_uint event_flags;
|
|
|
|
BOOL tracking;
|
|
TRACKMOUSEEVENT trackEvent;
|
|
|
|
int mouse_x;
|
|
int mouse_y;
|
|
|
|
// Should SetCursor be called when handling VOCTRL_SET_CURSOR_VISIBILITY?
|
|
bool can_set_cursor;
|
|
|
|
// UTF-16 decoding state for WM_CHAR and VK_PACKET
|
|
int high_surrogate;
|
|
|
|
// Whether to fit the window on screen on next window state updating
|
|
bool fit_on_screen;
|
|
|
|
ITaskbarList2 *taskbar_list;
|
|
ITaskbarList3 *taskbar_list3;
|
|
UINT tbtnCreatedMsg;
|
|
bool tbtnCreated;
|
|
|
|
struct voctrl_playback_state current_pstate;
|
|
|
|
// updates on move/resize/displaychange
|
|
double display_fps;
|
|
|
|
bool moving;
|
|
bool snapped;
|
|
int snap_dx;
|
|
int snap_dy;
|
|
|
|
HANDLE avrt_handle;
|
|
};
|
|
|
|
static void add_window_borders(HWND hwnd, RECT *rc)
|
|
{
|
|
AdjustWindowRect(rc, GetWindowLongPtrW(hwnd, GWL_STYLE), 0);
|
|
}
|
|
|
|
// basically a reverse AdjustWindowRect (win32 doesn't appear to have this)
|
|
static void subtract_window_borders(HWND hwnd, RECT *rc)
|
|
{
|
|
RECT b = { 0, 0, 0, 0 };
|
|
add_window_borders(hwnd, &b);
|
|
rc->left -= b.left;
|
|
rc->top -= b.top;
|
|
rc->right -= b.right;
|
|
rc->bottom -= b.bottom;
|
|
}
|
|
|
|
static LRESULT borderless_nchittest(struct vo_w32_state *w32, int x, int y)
|
|
{
|
|
if (IsMaximized(w32->window))
|
|
return HTCLIENT;
|
|
|
|
POINT mouse = { x, y };
|
|
ScreenToClient(w32->window, &mouse);
|
|
|
|
// The horizontal frame should be the same size as the vertical frame,
|
|
// since the NONCLIENTMETRICS structure does not distinguish between them
|
|
int frame_size = GetSystemMetrics(SM_CXFRAME) +
|
|
GetSystemMetrics(SM_CXPADDEDBORDER);
|
|
// The diagonal size handles are slightly wider than the side borders
|
|
int diagonal_width = frame_size * 2 + GetSystemMetrics(SM_CXBORDER);
|
|
|
|
// Hit-test top border
|
|
if (mouse.y < frame_size) {
|
|
if (mouse.x < diagonal_width)
|
|
return HTTOPLEFT;
|
|
if (mouse.x >= rect_w(w32->windowrc) - diagonal_width)
|
|
return HTTOPRIGHT;
|
|
return HTTOP;
|
|
}
|
|
|
|
// Hit-test bottom border
|
|
if (mouse.y >= rect_h(w32->windowrc) - frame_size) {
|
|
if (mouse.x < diagonal_width)
|
|
return HTBOTTOMLEFT;
|
|
if (mouse.x >= rect_w(w32->windowrc) - diagonal_width)
|
|
return HTBOTTOMRIGHT;
|
|
return HTBOTTOM;
|
|
}
|
|
|
|
// Hit-test side borders
|
|
if (mouse.x < frame_size)
|
|
return HTLEFT;
|
|
if (mouse.x >= rect_w(w32->windowrc) - frame_size)
|
|
return HTRIGHT;
|
|
return HTCLIENT;
|
|
}
|
|
|
|
// turn a WMSZ_* input value in v into the border that should be resized
|
|
// returns: 0=left, 1=top, 2=right, 3=bottom, -1=undefined
|
|
static int get_resize_border(int v)
|
|
{
|
|
switch (v) {
|
|
case WMSZ_LEFT: return 3;
|
|
case WMSZ_TOP: return 2;
|
|
case WMSZ_RIGHT: return 3;
|
|
case WMSZ_BOTTOM: return 2;
|
|
case WMSZ_TOPLEFT: return 1;
|
|
case WMSZ_TOPRIGHT: return 1;
|
|
case WMSZ_BOTTOMLEFT: return 3;
|
|
case WMSZ_BOTTOMRIGHT: return 3;
|
|
default: return -1;
|
|
}
|
|
}
|
|
|
|
static bool key_state(int vk)
|
|
{
|
|
return GetKeyState(vk) & 0x8000;
|
|
}
|
|
|
|
static int mod_state(struct vo_w32_state *w32)
|
|
{
|
|
int res = 0;
|
|
|
|
// AltGr is represented as LCONTROL+RMENU on Windows
|
|
bool alt_gr = mp_input_use_alt_gr(w32->input_ctx) &&
|
|
key_state(VK_RMENU) && key_state(VK_LCONTROL);
|
|
|
|
if (key_state(VK_RCONTROL) || (key_state(VK_LCONTROL) && !alt_gr))
|
|
res |= MP_KEY_MODIFIER_CTRL;
|
|
if (key_state(VK_SHIFT))
|
|
res |= MP_KEY_MODIFIER_SHIFT;
|
|
if (key_state(VK_LMENU) || (key_state(VK_RMENU) && !alt_gr))
|
|
res |= MP_KEY_MODIFIER_ALT;
|
|
return res;
|
|
}
|
|
|
|
static int decode_surrogate_pair(wchar_t lead, wchar_t trail)
|
|
{
|
|
return 0x10000 + (((lead & 0x3ff) << 10) | (trail & 0x3ff));
|
|
}
|
|
|
|
static int decode_utf16(struct vo_w32_state *w32, wchar_t c)
|
|
{
|
|
// Decode UTF-16, keeping state in w32->high_surrogate
|
|
if (IS_HIGH_SURROGATE(c)) {
|
|
w32->high_surrogate = c;
|
|
return 0;
|
|
}
|
|
if (IS_LOW_SURROGATE(c)) {
|
|
if (!w32->high_surrogate) {
|
|
MP_ERR(w32, "Invalid UTF-16 input\n");
|
|
return 0;
|
|
}
|
|
int codepoint = decode_surrogate_pair(w32->high_surrogate, c);
|
|
w32->high_surrogate = 0;
|
|
return codepoint;
|
|
}
|
|
if (w32->high_surrogate != 0) {
|
|
w32->high_surrogate = 0;
|
|
MP_ERR(w32, "Invalid UTF-16 input\n");
|
|
return 0;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
static void clear_keyboard_buffer(void)
|
|
{
|
|
static const UINT vkey = VK_DECIMAL;
|
|
static const BYTE keys[256] = { 0 };
|
|
UINT scancode = MapVirtualKey(vkey, MAPVK_VK_TO_VSC);
|
|
wchar_t buf[10];
|
|
int ret = 0;
|
|
|
|
// Use the method suggested by Michael Kaplan to clear any pending dead
|
|
// keys from the current keyboard layout. See:
|
|
// https://web.archive.org/web/20101004154432/http://blogs.msdn.com/b/michkap/archive/2006/04/06/569632.aspx
|
|
// https://web.archive.org/web/20100820152419/http://blogs.msdn.com/b/michkap/archive/2007/10/27/5717859.aspx
|
|
do {
|
|
ret = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
|
|
} while (ret < 0);
|
|
}
|
|
|
|
static int to_unicode(UINT vkey, UINT scancode, const BYTE keys[256])
|
|
{
|
|
// This wraps ToUnicode to be stateless and to return only one character
|
|
|
|
// Make the buffer 10 code units long to be safe, same as here:
|
|
// https://web.archive.org/web/20101013215215/http://blogs.msdn.com/b/michkap/archive/2006/03/24/559169.aspx
|
|
wchar_t buf[10] = { 0 };
|
|
|
|
// Dead keys aren't useful for key shortcuts, so clear the keyboard state
|
|
clear_keyboard_buffer();
|
|
|
|
int len = ToUnicode(vkey, scancode, keys, buf, MP_ARRAY_SIZE(buf), 0);
|
|
|
|
// Return the last complete UTF-16 code point. A negative return value
|
|
// indicates a dead key, however there should still be a non-combining
|
|
// version of the key in the buffer.
|
|
if (len < 0)
|
|
len = -len;
|
|
if (len >= 2 && IS_SURROGATE_PAIR(buf[len - 2], buf[len - 1]))
|
|
return decode_surrogate_pair(buf[len - 2], buf[len - 1]);
|
|
if (len >= 1)
|
|
return buf[len - 1];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int decode_key(struct vo_w32_state *w32, UINT vkey, UINT scancode)
|
|
{
|
|
BYTE keys[256];
|
|
GetKeyboardState(keys);
|
|
|
|
// If mp_input_use_alt_gr is false, detect and remove AltGr so normal
|
|
// characters are generated. Note that AltGr is represented as
|
|
// LCONTROL+RMENU on Windows.
|
|
if ((keys[VK_RMENU] & 0x80) && (keys[VK_LCONTROL] & 0x80) &&
|
|
!mp_input_use_alt_gr(w32->input_ctx))
|
|
{
|
|
keys[VK_RMENU] = keys[VK_LCONTROL] = 0;
|
|
keys[VK_MENU] = keys[VK_LMENU];
|
|
keys[VK_CONTROL] = keys[VK_RCONTROL];
|
|
}
|
|
|
|
int c = to_unicode(vkey, scancode, keys);
|
|
|
|
// Some shift states prevent ToUnicode from working or cause it to produce
|
|
// control characters. If this is detected, remove modifiers until it
|
|
// starts producing normal characters.
|
|
if (c < 0x20 && (keys[VK_MENU] & 0x80)) {
|
|
keys[VK_LMENU] = keys[VK_RMENU] = keys[VK_MENU] = 0;
|
|
c = to_unicode(vkey, scancode, keys);
|
|
}
|
|
if (c < 0x20 && (keys[VK_CONTROL] & 0x80)) {
|
|
keys[VK_LCONTROL] = keys[VK_RCONTROL] = keys[VK_CONTROL] = 0;
|
|
c = to_unicode(vkey, scancode, keys);
|
|
}
|
|
if (c < 0x20)
|
|
return 0;
|
|
|
|
// Decode lone UTF-16 surrogates (VK_PACKET can generate these)
|
|
if (c < 0x10000)
|
|
return decode_utf16(w32, c);
|
|
return c;
|
|
}
|
|
|
|
static bool handle_appcommand(struct vo_w32_state *w32, UINT cmd)
|
|
{
|
|
if (!mp_input_use_media_keys(w32->input_ctx))
|
|
return false;
|
|
int mpkey = mp_w32_appcmd_to_mpkey(cmd);
|
|
if (!mpkey)
|
|
return false;
|
|
mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32));
|
|
return true;
|
|
}
|
|
|
|
static void handle_key_down(struct vo_w32_state *w32, UINT vkey, UINT scancode)
|
|
{
|
|
// Ignore key repeat
|
|
if (scancode & KF_REPEAT)
|
|
return;
|
|
|
|
int mpkey = mp_w32_vkey_to_mpkey(vkey, scancode & KF_EXTENDED);
|
|
if (!mpkey) {
|
|
mpkey = decode_key(w32, vkey, scancode & (0xff | KF_EXTENDED));
|
|
if (!mpkey)
|
|
return;
|
|
}
|
|
|
|
mp_input_put_key(w32->input_ctx, mpkey | mod_state(w32) | MP_KEY_STATE_DOWN);
|
|
}
|
|
|
|
static void handle_key_up(struct vo_w32_state *w32, UINT vkey, UINT scancode)
|
|
{
|
|
switch (vkey) {
|
|
case VK_MENU:
|
|
case VK_CONTROL:
|
|
case VK_SHIFT:
|
|
break;
|
|
default:
|
|
// Releasing all keys on key-up is simpler and ensures no keys can be
|
|
// get "stuck." This matches the behaviour of other VOs.
|
|
mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
|
|
}
|
|
}
|
|
|
|
static bool handle_char(struct vo_w32_state *w32, wchar_t wc)
|
|
{
|
|
int c = decode_utf16(w32, wc);
|
|
|
|
if (c == 0)
|
|
return true;
|
|
if (c < 0x20)
|
|
return false;
|
|
|
|
mp_input_put_key(w32->input_ctx, c | mod_state(w32));
|
|
return true;
|
|
}
|
|
|
|
static bool handle_mouse_down(struct vo_w32_state *w32, int btn, int x, int y)
|
|
{
|
|
btn |= mod_state(w32);
|
|
mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_DOWN);
|
|
|
|
if (btn == MP_MBTN_LEFT && !w32->current_fs &&
|
|
!mp_input_test_dragging(w32->input_ctx, x, y))
|
|
{
|
|
// Window dragging hack
|
|
ReleaseCapture();
|
|
SendMessage(w32->window, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
|
mp_input_put_key(w32->input_ctx, MP_MBTN_LEFT | MP_KEY_STATE_UP);
|
|
|
|
// Indicate the message was handled, so DefWindowProc won't be called
|
|
return true;
|
|
}
|
|
|
|
SetCapture(w32->window);
|
|
return false;
|
|
}
|
|
|
|
static void handle_mouse_up(struct vo_w32_state *w32, int btn)
|
|
{
|
|
btn |= mod_state(w32);
|
|
mp_input_put_key(w32->input_ctx, btn | MP_KEY_STATE_UP);
|
|
|
|
ReleaseCapture();
|
|
}
|
|
|
|
static void handle_mouse_wheel(struct vo_w32_state *w32, bool horiz, int val)
|
|
{
|
|
int code;
|
|
if (horiz)
|
|
code = val > 0 ? MP_WHEEL_RIGHT : MP_WHEEL_LEFT;
|
|
else
|
|
code = val > 0 ? MP_WHEEL_UP : MP_WHEEL_DOWN;
|
|
mp_input_put_wheel(w32->input_ctx, code | mod_state(w32), abs(val) / 120.);
|
|
}
|
|
|
|
static void signal_events(struct vo_w32_state *w32, int events)
|
|
{
|
|
atomic_fetch_or(&w32->event_flags, events);
|
|
vo_wakeup(w32->vo);
|
|
}
|
|
|
|
static void wakeup_gui_thread(void *ctx)
|
|
{
|
|
struct vo_w32_state *w32 = ctx;
|
|
// Wake up the window procedure (which processes the dispatch queue)
|
|
if (GetWindowThreadProcessId(w32->window, NULL) == GetCurrentThreadId()) {
|
|
PostMessageW(w32->window, WM_NULL, 0, 0);
|
|
} else {
|
|
// Use a sent message when cross-thread, since the queue of sent
|
|
// messages is processed in some cases when posted messages are blocked
|
|
SendNotifyMessageW(w32->window, WM_NULL, 0, 0);
|
|
}
|
|
}
|
|
|
|
static double get_refresh_rate_from_gdi(const wchar_t *device)
|
|
{
|
|
DEVMODEW dm = { .dmSize = sizeof dm };
|
|
if (!EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, &dm))
|
|
return 0.0;
|
|
|
|
// May return 0 or 1 which "represent the display hardware's default refresh rate"
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd183565%28v=vs.85%29.aspx
|
|
// mpv validates this value with a threshold of 1, so don't return exactly 1
|
|
if (dm.dmDisplayFrequency == 1)
|
|
return 0.0;
|
|
|
|
// dm.dmDisplayFrequency is an integer which is rounded down, so it's
|
|
// highly likely that 23 represents 24/1.001, 59 represents 60/1.001, etc.
|
|
// A caller can always reproduce the original value by using floor.
|
|
double rv = dm.dmDisplayFrequency;
|
|
switch (dm.dmDisplayFrequency) {
|
|
case 23:
|
|
case 29:
|
|
case 47:
|
|
case 59:
|
|
case 71:
|
|
case 89:
|
|
case 95:
|
|
case 119:
|
|
case 143:
|
|
rv = (rv + 1) / 1.001;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
static char *get_color_profile(void *ctx, const wchar_t *device)
|
|
{
|
|
char *name = NULL;
|
|
|
|
HDC ic = CreateICW(device, NULL, NULL, NULL);
|
|
if (!ic)
|
|
goto done;
|
|
wchar_t wname[MAX_PATH + 1];
|
|
if (!GetICMProfileW(ic, &(DWORD){ MAX_PATH }, wname))
|
|
goto done;
|
|
|
|
name = mp_to_utf8(ctx, wname);
|
|
done:
|
|
if (ic)
|
|
DeleteDC(ic);
|
|
return name;
|
|
}
|
|
|
|
static void update_dpi(struct vo_w32_state *w32)
|
|
{
|
|
UINT dpiX, dpiY;
|
|
if (w32->api.pGetDpiForMonitor && w32->api.pGetDpiForMonitor(w32->monitor,
|
|
MDT_EFFECTIVE_DPI, &dpiX, &dpiY) == S_OK) {
|
|
w32->dpi = (int)dpiX;
|
|
MP_VERBOSE(w32, "DPI detected from the new API: %d\n", w32->dpi);
|
|
return;
|
|
}
|
|
HDC hdc = GetDC(NULL);
|
|
if (hdc) {
|
|
w32->dpi = GetDeviceCaps(hdc, LOGPIXELSX);
|
|
ReleaseDC(NULL, hdc);
|
|
MP_VERBOSE(w32, "DPI detected from the old API: %d\n", w32->dpi);
|
|
} else {
|
|
w32->dpi = 96;
|
|
MP_VERBOSE(w32, "Couldn't determine DPI, falling back to %d\n", w32->dpi);
|
|
}
|
|
}
|
|
|
|
static void update_display_info(struct vo_w32_state *w32)
|
|
{
|
|
HMONITOR monitor = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
|
|
if (w32->monitor == monitor)
|
|
return;
|
|
w32->monitor = monitor;
|
|
|
|
update_dpi(w32);
|
|
|
|
MONITORINFOEXW mi = { .cbSize = sizeof mi };
|
|
GetMonitorInfoW(monitor, (MONITORINFO*)&mi);
|
|
|
|
// Try to get the monitor refresh rate.
|
|
double freq = 0.0;
|
|
|
|
if (freq == 0.0)
|
|
freq = mp_w32_displayconfig_get_refresh_rate(mi.szDevice);
|
|
if (freq == 0.0)
|
|
freq = get_refresh_rate_from_gdi(mi.szDevice);
|
|
|
|
if (freq != w32->display_fps) {
|
|
MP_VERBOSE(w32, "display-fps: %f\n", freq);
|
|
if (freq == 0.0)
|
|
MP_WARN(w32, "Couldn't determine monitor refresh rate\n");
|
|
w32->display_fps = freq;
|
|
signal_events(w32, VO_EVENT_WIN_STATE);
|
|
}
|
|
|
|
char *color_profile = get_color_profile(w32, mi.szDevice);
|
|
if ((color_profile == NULL) != (w32->color_profile == NULL) ||
|
|
(color_profile && strcmp(color_profile, w32->color_profile)))
|
|
{
|
|
if (color_profile)
|
|
MP_VERBOSE(w32, "color-profile: %s\n", color_profile);
|
|
talloc_free(w32->color_profile);
|
|
w32->color_profile = color_profile;
|
|
color_profile = NULL;
|
|
signal_events(w32, VO_EVENT_ICC_PROFILE_CHANGED);
|
|
}
|
|
|
|
talloc_free(color_profile);
|
|
}
|
|
|
|
static void force_update_display_info(struct vo_w32_state *w32)
|
|
{
|
|
w32->monitor = 0;
|
|
update_display_info(w32);
|
|
}
|
|
|
|
static void update_playback_state(struct vo_w32_state *w32)
|
|
{
|
|
struct voctrl_playback_state *pstate = &w32->current_pstate;
|
|
|
|
if (!w32->taskbar_list3 || !w32->tbtnCreated)
|
|
return;
|
|
|
|
if (!pstate->playing || !pstate->taskbar_progress) {
|
|
ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
|
|
TBPF_NOPROGRESS);
|
|
return;
|
|
}
|
|
|
|
ITaskbarList3_SetProgressValue(w32->taskbar_list3, w32->window,
|
|
pstate->percent_pos, 100);
|
|
ITaskbarList3_SetProgressState(w32->taskbar_list3, w32->window,
|
|
pstate->paused ? TBPF_PAUSED :
|
|
TBPF_NORMAL);
|
|
}
|
|
|
|
static bool snap_to_screen_edges(struct vo_w32_state *w32, RECT *rc)
|
|
{
|
|
if (w32->parent || w32->current_fs || IsMaximized(w32->window))
|
|
return false;
|
|
|
|
if (!w32->opts->snap_window) {
|
|
w32->snapped = false;
|
|
return false;
|
|
}
|
|
|
|
RECT rect;
|
|
POINT cursor;
|
|
if (!GetWindowRect(w32->window, &rect) || !GetCursorPos(&cursor))
|
|
return false;
|
|
// Check if window is going to be aero-snapped
|
|
if (rect_w(*rc) != rect_w(rect) || rect_h(*rc) != rect_h(rect))
|
|
return false;
|
|
|
|
// Check if window has already been aero-snapped
|
|
WINDOWPLACEMENT wp = {0};
|
|
wp.length = sizeof(wp);
|
|
if (!GetWindowPlacement(w32->window, &wp))
|
|
return false;
|
|
RECT wr = wp.rcNormalPosition;
|
|
if (rect_w(*rc) != rect_w(wr) || rect_h(*rc) != rect_h(wr))
|
|
return false;
|
|
|
|
MONITORINFO mi = { .cbSize = sizeof(mi) };
|
|
if (!GetMonitorInfoW(w32->monitor, &mi))
|
|
return false;
|
|
// Get the work area to let the window snap to taskbar
|
|
wr = mi.rcWork;
|
|
|
|
// Check for invisible borders and adjust the work area size
|
|
RECT frame = {0};
|
|
if (DwmGetWindowAttribute(w32->window, DWMWA_EXTENDED_FRAME_BOUNDS,
|
|
&frame, sizeof(RECT)) == S_OK) {
|
|
wr.left -= frame.left - rect.left;
|
|
wr.top -= frame.top - rect.top;
|
|
wr.right += rect.right - frame.right;
|
|
wr.bottom += rect.bottom - frame.bottom;
|
|
}
|
|
|
|
// Let the window to unsnap by changing its position,
|
|
// otherwise it will stick to the screen edges forever
|
|
rect = *rc;
|
|
if (w32->snapped) {
|
|
OffsetRect(&rect, cursor.x - rect.left - w32->snap_dx,
|
|
cursor.y - rect.top - w32->snap_dy);
|
|
}
|
|
|
|
int threshold = (w32->dpi * 16) / 96;
|
|
bool snapped = false;
|
|
// Adjust X position
|
|
if (abs(rect.left - wr.left) < threshold) {
|
|
snapped = true;
|
|
OffsetRect(&rect, wr.left - rect.left, 0);
|
|
} else if (abs(rect.right - wr.right) < threshold) {
|
|
snapped = true;
|
|
OffsetRect(&rect, wr.right - rect.right, 0);
|
|
}
|
|
// Adjust Y position
|
|
if (abs(rect.top - wr.top) < threshold) {
|
|
snapped = true;
|
|
OffsetRect(&rect, 0, wr.top - rect.top);
|
|
} else if (abs(rect.bottom - wr.bottom) < threshold) {
|
|
snapped = true;
|
|
OffsetRect(&rect, 0, wr.bottom - rect.bottom);
|
|
}
|
|
|
|
if (!w32->snapped && snapped) {
|
|
w32->snap_dx = cursor.x - rc->left;
|
|
w32->snap_dy = cursor.y - rc->top;
|
|
}
|
|
|
|
w32->snapped = snapped;
|
|
*rc = rect;
|
|
return true;
|
|
}
|
|
|
|
struct get_monitor_data {
|
|
int i;
|
|
int target;
|
|
HMONITOR mon;
|
|
};
|
|
|
|
static BOOL CALLBACK get_monitor_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
|
|
{
|
|
struct get_monitor_data *data = (struct get_monitor_data*)p;
|
|
|
|
if (data->i == data->target) {
|
|
data->mon = mon;
|
|
return FALSE;
|
|
}
|
|
data->i++;
|
|
return TRUE;
|
|
}
|
|
|
|
static HMONITOR get_monitor(int id)
|
|
{
|
|
struct get_monitor_data data = { .target = id };
|
|
EnumDisplayMonitors(NULL, NULL, get_monitor_proc, (LPARAM)&data);
|
|
return data.mon;
|
|
}
|
|
|
|
static void update_screen_rect(struct vo_w32_state *w32)
|
|
{
|
|
struct mp_vo_opts *opts = w32->opts;
|
|
int screen = w32->current_fs ? opts->fsscreen_id : opts->screen_id;
|
|
|
|
// Handle --fs-screen=all
|
|
if (w32->current_fs && screen == -2) {
|
|
const int x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
|
const int y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
|
SetRect(&w32->screenrc, x, y, x + GetSystemMetrics(SM_CXVIRTUALSCREEN),
|
|
y + GetSystemMetrics(SM_CYVIRTUALSCREEN));
|
|
return;
|
|
}
|
|
|
|
// When not using --fs-screen=all, mpv belongs to a specific HMONITOR
|
|
HMONITOR mon;
|
|
if (screen == -1) {
|
|
// Handle --fs-screen=current and --screen=default
|
|
mon = MonitorFromWindow(w32->window, MONITOR_DEFAULTTOPRIMARY);
|
|
} else {
|
|
mon = get_monitor(screen);
|
|
if (!mon) {
|
|
MP_INFO(w32, "Screen %d does not exist, falling back to primary\n",
|
|
screen);
|
|
mon = MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
|
|
}
|
|
}
|
|
|
|
MONITORINFO mi = { .cbSize = sizeof(mi) };
|
|
GetMonitorInfoW(mon, &mi);
|
|
w32->screenrc = mi.rcMonitor;
|
|
}
|
|
|
|
static DWORD update_style(struct vo_w32_state *w32, DWORD style)
|
|
{
|
|
const DWORD NO_FRAME = WS_OVERLAPPED | WS_MINIMIZEBOX;
|
|
const DWORD FRAME = WS_OVERLAPPEDWINDOW;
|
|
const DWORD FULLSCREEN = NO_FRAME | WS_SYSMENU;
|
|
style &= ~(NO_FRAME | FRAME | FULLSCREEN);
|
|
if (w32->current_fs) {
|
|
style |= FULLSCREEN;
|
|
} else {
|
|
style |= w32->opts->border ? FRAME : NO_FRAME;
|
|
}
|
|
return style;
|
|
}
|
|
|
|
static void update_window_style(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent)
|
|
return;
|
|
|
|
// SetWindowLongPtr can trigger a WM_SIZE event, so window rect
|
|
// has to be saved now and restored after setting the new style.
|
|
const RECT wr = w32->windowrc;
|
|
const DWORD style = GetWindowLongPtrW(w32->window, GWL_STYLE);
|
|
SetWindowLongPtrW(w32->window, GWL_STYLE, update_style(w32, style));
|
|
w32->windowrc = wr;
|
|
}
|
|
|
|
// Adjust rc size and position if its size is larger than rc2.
|
|
// returns true if the rectangle was modified.
|
|
static bool fit_rect(RECT *rc, RECT *rc2)
|
|
{
|
|
// Calculate old size and maximum new size
|
|
int o_w = rect_w(*rc), o_h = rect_h(*rc);
|
|
int n_w = rect_w(*rc2), n_h = rect_h(*rc2);
|
|
if (o_w <= n_w && o_h <= n_h)
|
|
return false;
|
|
|
|
// Apply letterboxing
|
|
const float o_asp = o_w / (float)MPMAX(o_h, 1);
|
|
const float n_asp = n_w / (float)MPMAX(n_h, 1);
|
|
if (o_asp > n_asp) {
|
|
n_h = n_w / o_asp;
|
|
} else {
|
|
n_w = n_h * o_asp;
|
|
}
|
|
|
|
// Calculate new position and save the rect
|
|
const int x = rc->left + o_w / 2 - n_w / 2;
|
|
const int y = rc->top + o_h / 2 - n_h / 2;
|
|
SetRect(rc, x, y, x + n_w, y + n_h);
|
|
return true;
|
|
}
|
|
|
|
// Adjust window size and position if its size is larger than the screen size.
|
|
static void fit_window_on_screen(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent || w32->current_fs || IsMaximized(w32->window))
|
|
return;
|
|
|
|
RECT screen = w32->screenrc;
|
|
if (w32->opts->border && w32->opts->fit_border)
|
|
subtract_window_borders(w32->window, &screen);
|
|
|
|
if (fit_rect(&w32->windowrc, &screen)) {
|
|
MP_VERBOSE(w32, "adjusted window bounds: %d:%d:%d:%d\n",
|
|
(int)w32->windowrc.left, (int)w32->windowrc.top,
|
|
(int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
|
|
}
|
|
}
|
|
|
|
// Calculate new fullscreen state and change window size and position.
|
|
// returns true if fullscreen state was changed.
|
|
static bool update_fullscreen_state(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent)
|
|
return false;
|
|
|
|
bool new_fs = w32->opts->fullscreen;
|
|
if (w32->toggle_fs) {
|
|
new_fs = !w32->current_fs;
|
|
w32->toggle_fs = false;
|
|
}
|
|
|
|
bool toggle_fs = w32->current_fs != new_fs;
|
|
w32->current_fs = new_fs;
|
|
|
|
update_screen_rect(w32);
|
|
|
|
if (toggle_fs) {
|
|
RECT rc;
|
|
char msg[50];
|
|
if (w32->current_fs) {
|
|
// Save window rect when switching to fullscreen.
|
|
rc = w32->prev_windowrc = w32->windowrc;
|
|
sprintf(msg, "save window bounds");
|
|
} else {
|
|
// Restore window rect when switching from fullscreen.
|
|
rc = w32->windowrc = w32->prev_windowrc;
|
|
sprintf(msg, "restore window bounds");
|
|
}
|
|
MP_VERBOSE(w32, "%s: %d:%d:%d:%d\n", msg,
|
|
(int)rc.left, (int)rc.top, (int)rect_w(rc), (int)rect_h(rc));
|
|
}
|
|
|
|
if (w32->current_fs)
|
|
w32->windowrc = w32->screenrc;
|
|
|
|
MP_VERBOSE(w32, "reset window bounds: %d:%d:%d:%d\n",
|
|
(int)w32->windowrc.left, (int)w32->windowrc.top,
|
|
(int)rect_w(w32->windowrc), (int)rect_h(w32->windowrc));
|
|
return toggle_fs;
|
|
}
|
|
|
|
static void update_window_state(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent)
|
|
return;
|
|
|
|
RECT wr = w32->windowrc;
|
|
add_window_borders(w32->window, &wr);
|
|
|
|
SetWindowPos(w32->window, w32->opts->ontop ? HWND_TOPMOST : HWND_NOTOPMOST,
|
|
wr.left, wr.top, rect_w(wr), rect_h(wr),
|
|
SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
|
|
|
// Notify the taskbar about the fullscreen state only after the window
|
|
// is visible, to make sure the taskbar item has already been created
|
|
if (w32->taskbar_list) {
|
|
ITaskbarList2_MarkFullscreenWindow(w32->taskbar_list,
|
|
w32->window, w32->current_fs);
|
|
}
|
|
|
|
signal_events(w32, VO_EVENT_RESIZE);
|
|
}
|
|
|
|
static void reinit_window_state(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent)
|
|
return;
|
|
|
|
// The order matters: fs state should be updated prior to changing styles
|
|
bool toggle_fs = update_fullscreen_state(w32);
|
|
update_window_style(w32);
|
|
|
|
// Assume that the window has already been fit on screen before switching fs
|
|
if (!toggle_fs || w32->fit_on_screen) {
|
|
fit_window_on_screen(w32);
|
|
// The fullscreen state might still be active, so set the flag
|
|
// to fit on screen next time the window leaves the fullscreen.
|
|
w32->fit_on_screen = w32->current_fs;
|
|
}
|
|
|
|
// Show and activate the window after all window state parameters were set
|
|
update_window_state(w32);
|
|
}
|
|
|
|
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,
|
|
LPARAM lParam)
|
|
{
|
|
struct vo_w32_state *w32 = (void*)GetWindowLongPtrW(hWnd, GWLP_USERDATA);
|
|
if (!w32) {
|
|
// WM_NCCREATE is supposed to be the first message that a window
|
|
// receives. It allows struct vo_w32_state to be passed from
|
|
// CreateWindow's lpParam to the window procedure. However, as a
|
|
// longstanding Windows bug, overlapped top-level windows will get a
|
|
// WM_GETMINMAXINFO before WM_NCCREATE. This can be ignored.
|
|
if (message != WM_NCCREATE)
|
|
return DefWindowProcW(hWnd, message, wParam, lParam);
|
|
|
|
CREATESTRUCTW *cs = (CREATESTRUCTW *)lParam;
|
|
w32 = cs->lpCreateParams;
|
|
w32->window = hWnd;
|
|
SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)w32);
|
|
}
|
|
|
|
// The dispatch queue should be processed as soon as possible to prevent
|
|
// playback glitches, since it is likely blocking the VO thread
|
|
if (!w32->in_dispatch) {
|
|
w32->in_dispatch = true;
|
|
mp_dispatch_queue_process(w32->dispatch, 0);
|
|
w32->in_dispatch = false;
|
|
}
|
|
|
|
switch (message) {
|
|
case WM_ERASEBKGND: // no need to erase background separately
|
|
return 1;
|
|
case WM_PAINT:
|
|
signal_events(w32, VO_EVENT_EXPOSE);
|
|
break;
|
|
case WM_MOVE: {
|
|
const int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam);
|
|
OffsetRect(&w32->windowrc, x - w32->windowrc.left,
|
|
y - w32->windowrc.top);
|
|
|
|
// Window may intersect with new monitors (see VOCTRL_GET_DISPLAY_NAMES)
|
|
signal_events(w32, VO_EVENT_WIN_STATE);
|
|
|
|
update_display_info(w32); // if we moved between monitors
|
|
MP_DBG(w32, "move window: %d:%d\n", x, y);
|
|
break;
|
|
}
|
|
case WM_MOVING: {
|
|
w32->moving = true;
|
|
RECT *rc = (RECT*)lParam;
|
|
if (snap_to_screen_edges(w32, rc))
|
|
return TRUE;
|
|
break;
|
|
}
|
|
case WM_ENTERSIZEMOVE:
|
|
w32->moving = true;
|
|
if (w32->snapped) {
|
|
// Save the cursor offset from the window borders,
|
|
// so the player window can be unsnapped later
|
|
RECT rc;
|
|
POINT cursor;
|
|
if (GetWindowRect(w32->window, &rc) && GetCursorPos(&cursor)) {
|
|
w32->snap_dx = cursor.x - rc.left;
|
|
w32->snap_dy = cursor.y - rc.top;
|
|
}
|
|
}
|
|
break;
|
|
case WM_EXITSIZEMOVE:
|
|
w32->moving = false;
|
|
break;
|
|
case WM_SIZE: {
|
|
if (w32->moving)
|
|
w32->snapped = false;
|
|
|
|
const int w = LOWORD(lParam), h = HIWORD(lParam);
|
|
if (w > 0 && h > 0) {
|
|
w32->windowrc.right = w32->windowrc.left + w;
|
|
w32->windowrc.bottom = w32->windowrc.top + h;
|
|
signal_events(w32, VO_EVENT_RESIZE);
|
|
MP_VERBOSE(w32, "resize window: %d:%d\n", w, h);
|
|
}
|
|
|
|
// Window may have been minimized or restored
|
|
signal_events(w32, VO_EVENT_WIN_STATE);
|
|
|
|
update_display_info(w32);
|
|
break;
|
|
}
|
|
case WM_SIZING:
|
|
if (w32->opts->keepaspect && w32->opts->keepaspect_window &&
|
|
!w32->current_fs && !w32->parent)
|
|
{
|
|
RECT *rc = (RECT*)lParam;
|
|
// get client area of the windows if it had the rect rc
|
|
// (subtracting the window borders)
|
|
RECT r = *rc;
|
|
subtract_window_borders(w32->window, &r);
|
|
int c_w = rect_w(r), c_h = rect_h(r);
|
|
float aspect = w32->o_dwidth / (float) MPMAX(w32->o_dheight, 1);
|
|
int d_w = c_h * aspect - c_w;
|
|
int d_h = c_w / aspect - c_h;
|
|
int d_corners[4] = { d_w, d_h, -d_w, -d_h };
|
|
int corners[4] = { rc->left, rc->top, rc->right, rc->bottom };
|
|
int corner = get_resize_border(wParam);
|
|
if (corner >= 0)
|
|
corners[corner] -= d_corners[corner];
|
|
*rc = (RECT) { corners[0], corners[1], corners[2], corners[3] };
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case WM_DPICHANGED:
|
|
update_display_info(w32);
|
|
break;
|
|
case WM_CLOSE:
|
|
// Don't destroy the window yet to not lose wakeup events.
|
|
mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
|
|
return 0;
|
|
case WM_NCDESTROY: // Sometimes only WM_NCDESTROY is received in --wid mode
|
|
case WM_DESTROY:
|
|
if (w32->destroyed)
|
|
break;
|
|
// If terminate is not set, something else destroyed the window. This
|
|
// can also happen in --wid mode when the parent window is destroyed.
|
|
if (!w32->terminate)
|
|
mp_input_put_key(w32->input_ctx, MP_KEY_CLOSE_WIN);
|
|
RevokeDragDrop(w32->window);
|
|
w32->destroyed = true;
|
|
w32->window = NULL;
|
|
PostQuitMessage(0);
|
|
break;
|
|
case WM_SYSCOMMAND:
|
|
switch (wParam & 0xFFF0) {
|
|
case SC_SCREENSAVE:
|
|
case SC_MONITORPOWER:
|
|
if (w32->disable_screensaver) {
|
|
MP_VERBOSE(w32, "killing screensaver\n");
|
|
return 0;
|
|
}
|
|
break;
|
|
case SC_RESTORE:
|
|
if (IsMaximized(w32->window) && w32->current_fs) {
|
|
w32->toggle_fs = true;
|
|
reinit_window_state(w32);
|
|
signal_events(w32, VO_EVENT_FULLSCREEN_STATE);
|
|
return 0;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case WM_NCHITTEST:
|
|
// Provide sizing handles for borderless windows
|
|
if (!w32->opts->border && !w32->current_fs) {
|
|
return borderless_nchittest(w32, GET_X_LPARAM(lParam),
|
|
GET_Y_LPARAM(lParam));
|
|
}
|
|
break;
|
|
case WM_APPCOMMAND:
|
|
if (handle_appcommand(w32, GET_APPCOMMAND_LPARAM(lParam)))
|
|
return TRUE;
|
|
break;
|
|
case WM_SYSKEYDOWN:
|
|
// Open the window menu on Alt+Space. Normally DefWindowProc opens the
|
|
// window menu in response to WM_SYSCHAR, but since mpv translates its
|
|
// own keyboard input, WM_SYSCHAR isn't generated, so the window menu
|
|
// must be opened manually.
|
|
if (wParam == VK_SPACE) {
|
|
SendMessage(w32->window, WM_SYSCOMMAND, SC_KEYMENU, ' ');
|
|
return 0;
|
|
}
|
|
|
|
// Handle all other WM_SYSKEYDOWN messages as WM_KEYDOWN
|
|
case WM_KEYDOWN:
|
|
handle_key_down(w32, wParam, HIWORD(lParam));
|
|
if (wParam == VK_F10)
|
|
return 0;
|
|
break;
|
|
case WM_SYSKEYUP:
|
|
case WM_KEYUP:
|
|
handle_key_up(w32, wParam, HIWORD(lParam));
|
|
if (wParam == VK_F10)
|
|
return 0;
|
|
break;
|
|
case WM_CHAR:
|
|
case WM_SYSCHAR:
|
|
if (handle_char(w32, wParam))
|
|
return 0;
|
|
break;
|
|
case WM_KILLFOCUS:
|
|
mp_input_put_key(w32->input_ctx, MP_INPUT_RELEASE_ALL);
|
|
break;
|
|
case WM_SETCURSOR:
|
|
// The cursor should only be hidden if the mouse is in the client area
|
|
// and if the window isn't in menu mode (HIWORD(lParam) is non-zero)
|
|
w32->can_set_cursor = LOWORD(lParam) == HTCLIENT && HIWORD(lParam);
|
|
if (w32->can_set_cursor && !w32->cursor_visible) {
|
|
SetCursor(NULL);
|
|
return TRUE;
|
|
}
|
|
break;
|
|
case WM_MOUSELEAVE:
|
|
w32->tracking = FALSE;
|
|
mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_LEAVE);
|
|
break;
|
|
case WM_MOUSEMOVE: {
|
|
if (!w32->tracking) {
|
|
w32->tracking = TrackMouseEvent(&w32->trackEvent);
|
|
mp_input_put_key(w32->input_ctx, MP_KEY_MOUSE_ENTER);
|
|
}
|
|
// Windows can send spurious mouse events, which would make the mpv
|
|
// core unhide the mouse cursor on completely unrelated events. See:
|
|
// https://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx
|
|
int x = GET_X_LPARAM(lParam);
|
|
int y = GET_Y_LPARAM(lParam);
|
|
if (x != w32->mouse_x || y != w32->mouse_y) {
|
|
w32->mouse_x = x;
|
|
w32->mouse_y = y;
|
|
mp_input_set_mouse_pos(w32->input_ctx, x, y);
|
|
}
|
|
break;
|
|
}
|
|
case WM_LBUTTONDOWN:
|
|
if (handle_mouse_down(w32, MP_MBTN_LEFT, GET_X_LPARAM(lParam),
|
|
GET_Y_LPARAM(lParam)))
|
|
return 0;
|
|
break;
|
|
case WM_LBUTTONUP:
|
|
handle_mouse_up(w32, MP_MBTN_LEFT);
|
|
break;
|
|
case WM_MBUTTONDOWN:
|
|
handle_mouse_down(w32, MP_MBTN_MID, GET_X_LPARAM(lParam),
|
|
GET_Y_LPARAM(lParam));
|
|
break;
|
|
case WM_MBUTTONUP:
|
|
handle_mouse_up(w32, MP_MBTN_MID);
|
|
break;
|
|
case WM_RBUTTONDOWN:
|
|
handle_mouse_down(w32, MP_MBTN_RIGHT, GET_X_LPARAM(lParam),
|
|
GET_Y_LPARAM(lParam));
|
|
break;
|
|
case WM_RBUTTONUP:
|
|
handle_mouse_up(w32, MP_MBTN_RIGHT);
|
|
break;
|
|
case WM_MOUSEWHEEL:
|
|
handle_mouse_wheel(w32, false, GET_WHEEL_DELTA_WPARAM(wParam));
|
|
return 0;
|
|
case WM_MOUSEHWHEEL:
|
|
handle_mouse_wheel(w32, true, GET_WHEEL_DELTA_WPARAM(wParam));
|
|
// Some buggy mouse drivers (SetPoint) stop delivering WM_MOUSEHWHEEL
|
|
// events when the message loop doesn't return TRUE (even on Windows 7)
|
|
return TRUE;
|
|
case WM_XBUTTONDOWN:
|
|
handle_mouse_down(w32,
|
|
HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD,
|
|
GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
|
break;
|
|
case WM_XBUTTONUP:
|
|
handle_mouse_up(w32,
|
|
HIWORD(wParam) == 1 ? MP_MBTN_BACK : MP_MBTN_FORWARD);
|
|
break;
|
|
case WM_DISPLAYCHANGE:
|
|
force_update_display_info(w32);
|
|
break;
|
|
}
|
|
|
|
if (message == w32->tbtnCreatedMsg) {
|
|
w32->tbtnCreated = true;
|
|
update_playback_state(w32);
|
|
return 0;
|
|
}
|
|
|
|
return DefWindowProcW(hWnd, message, wParam, lParam);
|
|
}
|
|
|
|
static pthread_once_t window_class_init_once = PTHREAD_ONCE_INIT;
|
|
static ATOM window_class;
|
|
static void register_window_class(void)
|
|
{
|
|
window_class = RegisterClassExW(&(WNDCLASSEXW) {
|
|
.cbSize = sizeof(WNDCLASSEXW),
|
|
.style = CS_HREDRAW | CS_VREDRAW,
|
|
.lpfnWndProc = WndProc,
|
|
.hInstance = HINST_THISCOMPONENT,
|
|
.hIcon = LoadIconW(HINST_THISCOMPONENT, L"IDI_ICON1"),
|
|
.hCursor = LoadCursor(NULL, IDC_ARROW),
|
|
.lpszClassName = L"mpv",
|
|
});
|
|
}
|
|
|
|
static ATOM get_window_class(void)
|
|
{
|
|
pthread_once(&window_class_init_once, register_window_class);
|
|
return window_class;
|
|
}
|
|
|
|
static void resize_child_win(HWND parent)
|
|
{
|
|
// Check if an mpv window is a child of this window. This will not
|
|
// necessarily be the case because the hook functions will run for all
|
|
// windows on the parent window's thread.
|
|
ATOM cls = get_window_class();
|
|
HWND child = FindWindowExW(parent, NULL, (LPWSTR)MAKEINTATOM(cls), NULL);
|
|
if (!child)
|
|
return;
|
|
// Make sure the window was created by this instance
|
|
if (GetWindowLongPtrW(child, GWLP_HINSTANCE) != (LONG_PTR)HINST_THISCOMPONENT)
|
|
return;
|
|
|
|
// Resize the mpv window to match its parent window's size
|
|
RECT rm, rp;
|
|
if (!GetClientRect(child, &rm))
|
|
return;
|
|
if (!GetClientRect(parent, &rp))
|
|
return;
|
|
if (EqualRect(&rm, &rp))
|
|
return;
|
|
SetWindowPos(child, NULL, 0, 0, rp.right, rp.bottom, SWP_ASYNCWINDOWPOS |
|
|
SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOSENDCHANGING);
|
|
}
|
|
|
|
static LRESULT CALLBACK parent_win_hook(int nCode, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
if (nCode != HC_ACTION)
|
|
goto done;
|
|
CWPSTRUCT *cwp = (CWPSTRUCT*)lParam;
|
|
if (cwp->message != WM_WINDOWPOSCHANGED)
|
|
goto done;
|
|
resize_child_win(cwp->hwnd);
|
|
done:
|
|
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
|
}
|
|
|
|
static void CALLBACK parent_evt_hook(HWINEVENTHOOK hWinEventHook, DWORD event,
|
|
HWND hwnd, LONG idObject, LONG idChild, DWORD dwEventThread,
|
|
DWORD dwmsEventTime)
|
|
{
|
|
if (event != EVENT_OBJECT_LOCATIONCHANGE)
|
|
return;
|
|
if (!hwnd || idObject != OBJID_WINDOW || idChild != CHILDID_SELF)
|
|
return;
|
|
resize_child_win(hwnd);
|
|
}
|
|
|
|
static void install_parent_hook(struct vo_w32_state *w32)
|
|
{
|
|
DWORD pid;
|
|
DWORD tid = GetWindowThreadProcessId(w32->parent, &pid);
|
|
|
|
// If the parent lives inside the current process, install a Windows hook
|
|
if (pid == GetCurrentProcessId()) {
|
|
w32->parent_win_hook = SetWindowsHookExW(WH_CALLWNDPROC,
|
|
parent_win_hook, NULL, tid);
|
|
} else {
|
|
// Otherwise, use a WinEvent hook. These don't seem to be as smooth as
|
|
// Windows hooks, but they can be delivered across process boundaries.
|
|
w32->parent_evt_hook = SetWinEventHook(
|
|
EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE,
|
|
NULL, parent_evt_hook, pid, tid, WINEVENT_OUTOFCONTEXT);
|
|
}
|
|
}
|
|
|
|
static void remove_parent_hook(struct vo_w32_state *w32)
|
|
{
|
|
if (w32->parent_win_hook)
|
|
UnhookWindowsHookEx(w32->parent_win_hook);
|
|
if (w32->parent_evt_hook)
|
|
UnhookWinEvent(w32->parent_evt_hook);
|
|
}
|
|
|
|
// Dispatch incoming window events and handle them.
|
|
// This returns only when the thread is asked to terminate.
|
|
static void run_message_loop(struct vo_w32_state *w32)
|
|
{
|
|
MSG msg;
|
|
while (GetMessageW(&msg, 0, 0, 0) > 0)
|
|
DispatchMessageW(&msg);
|
|
|
|
// Even if the message loop somehow exits, we still have to respond to
|
|
// external requests until termination is requested.
|
|
while (!w32->terminate)
|
|
mp_dispatch_queue_process(w32->dispatch, 1000);
|
|
}
|
|
|
|
static void gui_thread_reconfig(void *ptr)
|
|
{
|
|
struct vo_w32_state *w32 = ptr;
|
|
struct vo *vo = w32->vo;
|
|
|
|
struct vo_win_geometry geo;
|
|
struct mp_rect screen = { w32->screenrc.left, w32->screenrc.top,
|
|
w32->screenrc.right, w32->screenrc.bottom };
|
|
vo_calc_window_geometry(vo, &screen, &geo);
|
|
vo_apply_window_geometry(vo, &geo);
|
|
|
|
bool reset_size = w32->o_dwidth != vo->dwidth ||
|
|
w32->o_dheight != vo->dheight;
|
|
|
|
w32->o_dwidth = vo->dwidth;
|
|
w32->o_dheight = vo->dheight;
|
|
|
|
if (!w32->parent && !w32->window_bounds_initialized) {
|
|
SetRect(&w32->windowrc, geo.win.x0, geo.win.y0,
|
|
geo.win.x0 + vo->dwidth, geo.win.y0 + vo->dheight);
|
|
w32->prev_windowrc = w32->windowrc;
|
|
w32->window_bounds_initialized = true;
|
|
w32->fit_on_screen = true;
|
|
goto finish;
|
|
}
|
|
|
|
// The rect which size is going to be modified.
|
|
RECT *rc = &w32->windowrc;
|
|
|
|
// The desired size always matches the window size in wid mode.
|
|
if (!reset_size || w32->parent) {
|
|
RECT r;
|
|
GetClientRect(w32->window, &r);
|
|
// Restore vo_dwidth and vo_dheight, which were reset in vo_config()
|
|
vo->dwidth = r.right;
|
|
vo->dheight = r.bottom;
|
|
} else {
|
|
if (w32->current_fs)
|
|
rc = &w32->prev_windowrc;
|
|
w32->fit_on_screen = true;
|
|
}
|
|
|
|
// Save new window size and position.
|
|
const int x = rc->left + rect_w(*rc) / 2 - vo->dwidth / 2;
|
|
const int y = rc->top + rect_h(*rc) / 2 - vo->dheight / 2;
|
|
SetRect(rc, x, y, x + vo->dwidth, y + vo->dheight);
|
|
|
|
finish:
|
|
reinit_window_state(w32);
|
|
}
|
|
|
|
// Resize the window. On the first call, it's also made visible.
|
|
void vo_w32_config(struct vo *vo)
|
|
{
|
|
struct vo_w32_state *w32 = vo->w32;
|
|
mp_dispatch_run(w32->dispatch, gui_thread_reconfig, w32);
|
|
}
|
|
|
|
static void w32_api_load(struct vo_w32_state *w32)
|
|
{
|
|
HMODULE shcore_dll = LoadLibraryW(L"shcore.dll");
|
|
// Available since Win8.1
|
|
w32->api.pGetDpiForMonitor = !shcore_dll ? NULL :
|
|
(void *)GetProcAddress(shcore_dll, "GetDpiForMonitor");
|
|
|
|
// imm32.dll must be loaded dynamically
|
|
// to account for machines without East Asian language support
|
|
HMODULE imm32_dll = LoadLibraryW(L"imm32.dll");
|
|
w32->api.pImmDisableIME = !imm32_dll ? NULL :
|
|
(void *)GetProcAddress(imm32_dll, "ImmDisableIME");
|
|
}
|
|
|
|
static void *gui_thread(void *ptr)
|
|
{
|
|
struct vo_w32_state *w32 = ptr;
|
|
bool ole_ok = false;
|
|
int res = 0;
|
|
|
|
mpthread_set_name("win32 window");
|
|
|
|
w32_api_load(w32);
|
|
|
|
// Disables the IME for windows on this thread
|
|
if (w32->api.pImmDisableIME)
|
|
w32->api.pImmDisableIME(0);
|
|
|
|
if (w32->opts->WinID >= 0)
|
|
w32->parent = (HWND)(intptr_t)(w32->opts->WinID);
|
|
|
|
ATOM cls = get_window_class();
|
|
if (w32->parent) {
|
|
RECT r;
|
|
GetClientRect(w32->parent, &r);
|
|
CreateWindowExW(WS_EX_NOPARENTNOTIFY, (LPWSTR)MAKEINTATOM(cls), L"mpv",
|
|
WS_CHILD | WS_VISIBLE, 0, 0, r.right, r.bottom,
|
|
w32->parent, 0, HINST_THISCOMPONENT, w32);
|
|
|
|
// Install a hook to get notifications when the parent changes size
|
|
if (w32->window)
|
|
install_parent_hook(w32);
|
|
} else {
|
|
CreateWindowExW(0, (LPWSTR)MAKEINTATOM(cls), L"mpv",
|
|
update_style(w32, 0), CW_USEDEFAULT, SW_HIDE, 100, 100,
|
|
0, 0, HINST_THISCOMPONENT, w32);
|
|
}
|
|
|
|
if (!w32->window) {
|
|
MP_ERR(w32, "unable to create window!\n");
|
|
goto done;
|
|
}
|
|
|
|
if (SUCCEEDED(OleInitialize(NULL))) {
|
|
ole_ok = true;
|
|
|
|
IDropTarget *dt = mp_w32_droptarget_create(w32->log, w32->input_ctx);
|
|
RegisterDragDrop(w32->window, dt);
|
|
|
|
// ITaskbarList2 has the MarkFullscreenWindow method, which is used to
|
|
// make sure the taskbar is hidden when mpv goes fullscreen
|
|
if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
|
|
CLSCTX_INPROC_SERVER, &IID_ITaskbarList2,
|
|
(void**)&w32->taskbar_list)))
|
|
{
|
|
if (FAILED(ITaskbarList2_HrInit(w32->taskbar_list))) {
|
|
ITaskbarList2_Release(w32->taskbar_list);
|
|
w32->taskbar_list = NULL;
|
|
}
|
|
}
|
|
|
|
// ITaskbarList3 has methods for status indication on taskbar buttons,
|
|
// however that interface is only available on Win7/2008 R2 or newer
|
|
if (SUCCEEDED(CoCreateInstance(&CLSID_TaskbarList, NULL,
|
|
CLSCTX_INPROC_SERVER, &IID_ITaskbarList3,
|
|
(void**)&w32->taskbar_list3)))
|
|
{
|
|
if (FAILED(ITaskbarList3_HrInit(w32->taskbar_list3))) {
|
|
ITaskbarList3_Release(w32->taskbar_list3);
|
|
w32->taskbar_list3 = NULL;
|
|
} else {
|
|
w32->tbtnCreatedMsg = RegisterWindowMessage(L"TaskbarButtonCreated");
|
|
}
|
|
}
|
|
} else {
|
|
MP_ERR(w32, "Failed to initialize OLE/COM\n");
|
|
}
|
|
|
|
w32->tracking = FALSE;
|
|
w32->trackEvent = (TRACKMOUSEEVENT){
|
|
.cbSize = sizeof(TRACKMOUSEEVENT),
|
|
.dwFlags = TME_LEAVE,
|
|
.hwndTrack = w32->window,
|
|
};
|
|
|
|
if (w32->parent)
|
|
EnableWindow(w32->window, 0);
|
|
|
|
w32->cursor_visible = true;
|
|
w32->moving = false;
|
|
w32->snapped = false;
|
|
w32->snap_dx = w32->snap_dy = 0;
|
|
|
|
update_screen_rect(w32);
|
|
|
|
mp_dispatch_set_wakeup_fn(w32->dispatch, wakeup_gui_thread, w32);
|
|
|
|
res = 1;
|
|
done:
|
|
|
|
mp_rendezvous(w32, res); // init barrier
|
|
|
|
// This blocks until the GUI thread is to be exited.
|
|
if (res)
|
|
run_message_loop(w32);
|
|
|
|
MP_VERBOSE(w32, "uninit\n");
|
|
|
|
remove_parent_hook(w32);
|
|
if (w32->window && !w32->destroyed)
|
|
DestroyWindow(w32->window);
|
|
if (w32->taskbar_list)
|
|
ITaskbarList2_Release(w32->taskbar_list);
|
|
if (w32->taskbar_list3)
|
|
ITaskbarList3_Release(w32->taskbar_list3);
|
|
if (ole_ok)
|
|
OleUninitialize();
|
|
SetThreadExecutionState(ES_CONTINUOUS);
|
|
return NULL;
|
|
}
|
|
|
|
// Returns: 1 = Success, 0 = Failure
|
|
int vo_w32_init(struct vo *vo)
|
|
{
|
|
assert(!vo->w32);
|
|
|
|
struct vo_w32_state *w32 = talloc_ptrtype(vo, w32);
|
|
*w32 = (struct vo_w32_state){
|
|
.log = mp_log_new(w32, vo->log, "win32"),
|
|
.vo = vo,
|
|
.opts = vo->opts,
|
|
.input_ctx = vo->input_ctx,
|
|
.dispatch = mp_dispatch_create(w32),
|
|
};
|
|
vo->w32 = w32;
|
|
|
|
if (pthread_create(&w32->thread, NULL, gui_thread, w32))
|
|
goto fail;
|
|
|
|
if (!mp_rendezvous(w32, 0)) { // init barrier
|
|
pthread_join(w32->thread, NULL);
|
|
goto fail;
|
|
}
|
|
|
|
// While the UI runs in its own thread, the thread in which this function
|
|
// runs in will be the renderer thread. Apply magic MMCSS cargo-cult,
|
|
// which might stop Windows from throttling clock rate and so on.
|
|
if (vo->opts->mmcss_profile[0]) {
|
|
wchar_t *profile = mp_from_utf8(NULL, vo->opts->mmcss_profile);
|
|
w32->avrt_handle = AvSetMmThreadCharacteristicsW(profile, &(DWORD){0});
|
|
talloc_free(profile);
|
|
}
|
|
|
|
return 1;
|
|
fail:
|
|
talloc_free(w32);
|
|
vo->w32 = NULL;
|
|
return 0;
|
|
}
|
|
|
|
struct disp_names_data {
|
|
HMONITOR assoc;
|
|
int count;
|
|
char **names;
|
|
};
|
|
|
|
static BOOL CALLBACK disp_names_proc(HMONITOR mon, HDC dc, LPRECT r, LPARAM p)
|
|
{
|
|
struct disp_names_data *data = (struct disp_names_data*)p;
|
|
|
|
// get_disp_names() adds data->assoc to the list, so skip it here
|
|
if (mon == data->assoc)
|
|
return TRUE;
|
|
|
|
MONITORINFOEXW mi = { .cbSize = sizeof mi };
|
|
if (GetMonitorInfoW(mon, (MONITORINFO*)&mi)) {
|
|
MP_TARRAY_APPEND(NULL, data->names, data->count,
|
|
mp_to_utf8(NULL, mi.szDevice));
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static char **get_disp_names(struct vo_w32_state *w32)
|
|
{
|
|
// Get the client area of the window in screen space
|
|
RECT rect = { 0 };
|
|
GetClientRect(w32->window, &rect);
|
|
MapWindowPoints(w32->window, NULL, (POINT*)&rect, 2);
|
|
|
|
struct disp_names_data data = { .assoc = w32->monitor };
|
|
|
|
// Make sure the monitor that Windows considers to be associated with the
|
|
// window is first in the list
|
|
MONITORINFOEXW mi = { .cbSize = sizeof mi };
|
|
if (GetMonitorInfoW(data.assoc, (MONITORINFO*)&mi)) {
|
|
MP_TARRAY_APPEND(NULL, data.names, data.count,
|
|
mp_to_utf8(NULL, mi.szDevice));
|
|
}
|
|
|
|
// Get the names of the other monitors that intersect the client rect
|
|
EnumDisplayMonitors(NULL, &rect, disp_names_proc, (LPARAM)&data);
|
|
MP_TARRAY_APPEND(NULL, data.names, data.count, NULL);
|
|
return data.names;
|
|
}
|
|
|
|
static int gui_thread_control(struct vo_w32_state *w32, int request, void *arg)
|
|
{
|
|
switch (request) {
|
|
case VOCTRL_FULLSCREEN:
|
|
if (w32->opts->fullscreen != w32->current_fs)
|
|
reinit_window_state(w32);
|
|
return VO_TRUE;
|
|
case VOCTRL_ONTOP:
|
|
update_window_state(w32);
|
|
return VO_TRUE;
|
|
case VOCTRL_BORDER:
|
|
update_window_style(w32);
|
|
update_window_state(w32);
|
|
return VO_TRUE;
|
|
case VOCTRL_GET_FULLSCREEN:
|
|
*(bool *)arg = w32->current_fs;
|
|
return VO_TRUE;
|
|
case VOCTRL_GET_UNFS_WINDOW_SIZE: {
|
|
int *s = arg;
|
|
|
|
if (!w32->window_bounds_initialized)
|
|
return VO_FALSE;
|
|
|
|
RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
|
|
s[0] = rect_w(*rc);
|
|
s[1] = rect_h(*rc);
|
|
return VO_TRUE;
|
|
}
|
|
case VOCTRL_SET_UNFS_WINDOW_SIZE: {
|
|
int *s = arg;
|
|
|
|
if (!w32->window_bounds_initialized)
|
|
return VO_FALSE;
|
|
|
|
RECT *rc = w32->current_fs ? &w32->prev_windowrc : &w32->windowrc;
|
|
const int x = rc->left + rect_w(*rc) / 2 - s[0] / 2;
|
|
const int y = rc->top + rect_h(*rc) / 2 - s[1] / 2;
|
|
SetRect(rc, x, y, x + s[0], y + s[1]);
|
|
|
|
w32->fit_on_screen = true;
|
|
reinit_window_state(w32);
|
|
return VO_TRUE;
|
|
}
|
|
case VOCTRL_GET_WIN_STATE:
|
|
*(int *)arg = IsIconic(w32->window) ? VO_WIN_STATE_MINIMIZED : 0;
|
|
return VO_TRUE;
|
|
case VOCTRL_SET_CURSOR_VISIBILITY:
|
|
w32->cursor_visible = *(bool *)arg;
|
|
|
|
if (w32->can_set_cursor && w32->tracking) {
|
|
if (w32->cursor_visible)
|
|
SetCursor(LoadCursor(NULL, IDC_ARROW));
|
|
else
|
|
SetCursor(NULL);
|
|
}
|
|
return VO_TRUE;
|
|
case VOCTRL_KILL_SCREENSAVER:
|
|
w32->disable_screensaver = true;
|
|
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED |
|
|
ES_DISPLAY_REQUIRED);
|
|
return VO_TRUE;
|
|
case VOCTRL_RESTORE_SCREENSAVER:
|
|
w32->disable_screensaver = false;
|
|
SetThreadExecutionState(ES_CONTINUOUS);
|
|
return VO_TRUE;
|
|
case VOCTRL_UPDATE_WINDOW_TITLE: {
|
|
wchar_t *title = mp_from_utf8(NULL, (char *)arg);
|
|
SetWindowTextW(w32->window, title);
|
|
talloc_free(title);
|
|
return VO_TRUE;
|
|
}
|
|
case VOCTRL_UPDATE_PLAYBACK_STATE: {
|
|
w32->current_pstate = *(struct voctrl_playback_state *)arg;
|
|
|
|
update_playback_state(w32);
|
|
return VO_TRUE;
|
|
}
|
|
case VOCTRL_GET_DISPLAY_FPS:
|
|
update_display_info(w32);
|
|
*(double*) arg = w32->display_fps;
|
|
return VO_TRUE;
|
|
case VOCTRL_GET_DISPLAY_NAMES:
|
|
*(char ***)arg = get_disp_names(w32);
|
|
return VO_TRUE;
|
|
case VOCTRL_GET_ICC_PROFILE:
|
|
update_display_info(w32);
|
|
if (w32->color_profile) {
|
|
bstr *p = arg;
|
|
*p = stream_read_file(w32->color_profile, NULL,
|
|
w32->vo->global, 100000000); // 100 MB
|
|
return p->len ? VO_TRUE : VO_FALSE;
|
|
}
|
|
return VO_FALSE;
|
|
}
|
|
return VO_NOTIMPL;
|
|
}
|
|
|
|
static void do_control(void *ptr)
|
|
{
|
|
void **p = ptr;
|
|
struct vo_w32_state *w32 = p[0];
|
|
int *events = p[1];
|
|
int request = *(int *)p[2];
|
|
void *arg = p[3];
|
|
int *ret = p[4];
|
|
*ret = gui_thread_control(w32, request, arg);
|
|
*events |= atomic_fetch_and(&w32->event_flags, 0);
|
|
// Safe access, since caller (owner of vo) is blocked.
|
|
if (*events & VO_EVENT_RESIZE) {
|
|
w32->vo->dwidth = rect_w(w32->windowrc);
|
|
w32->vo->dheight = rect_h(w32->windowrc);
|
|
}
|
|
}
|
|
|
|
int vo_w32_control(struct vo *vo, int *events, int request, void *arg)
|
|
{
|
|
struct vo_w32_state *w32 = vo->w32;
|
|
if (request == VOCTRL_CHECK_EVENTS) {
|
|
*events |= atomic_fetch_and(&w32->event_flags, 0);
|
|
if (*events & VO_EVENT_RESIZE) {
|
|
mp_dispatch_lock(w32->dispatch);
|
|
vo->dwidth = rect_w(w32->windowrc);
|
|
vo->dheight = rect_h(w32->windowrc);
|
|
mp_dispatch_unlock(w32->dispatch);
|
|
}
|
|
return VO_TRUE;
|
|
} else {
|
|
int r;
|
|
void *p[] = {w32, events, &request, arg, &r};
|
|
mp_dispatch_run(w32->dispatch, do_control, p);
|
|
return r;
|
|
}
|
|
}
|
|
|
|
static void do_terminate(void *ptr)
|
|
{
|
|
struct vo_w32_state *w32 = ptr;
|
|
w32->terminate = true;
|
|
|
|
if (!w32->destroyed)
|
|
DestroyWindow(w32->window);
|
|
|
|
mp_dispatch_interrupt(w32->dispatch);
|
|
}
|
|
|
|
void vo_w32_uninit(struct vo *vo)
|
|
{
|
|
struct vo_w32_state *w32 = vo->w32;
|
|
if (!w32)
|
|
return;
|
|
|
|
mp_dispatch_run(w32->dispatch, do_terminate, w32);
|
|
pthread_join(w32->thread, NULL);
|
|
|
|
AvRevertMmThreadCharacteristics(w32->avrt_handle);
|
|
|
|
talloc_free(w32);
|
|
vo->w32 = NULL;
|
|
}
|
|
|
|
HWND vo_w32_hwnd(struct vo *vo)
|
|
{
|
|
struct vo_w32_state *w32 = vo->w32;
|
|
return w32->window; // immutable, so no synchronization needed
|
|
}
|
|
|
|
void vo_w32_run_on_thread(struct vo *vo, void (*cb)(void *ctx), void *ctx)
|
|
{
|
|
struct vo_w32_state *w32 = vo->w32;
|
|
mp_dispatch_run(w32->dispatch, cb, ctx);
|
|
}
|