1249 lines
38 KiB
C#
1249 lines
38 KiB
C#
/*
|
||
* UniWindowController.cs
|
||
*
|
||
* Author: Kirurobo http://twitter.com/kirurobo
|
||
* License: MIT
|
||
*/
|
||
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
using System.Reflection;
|
||
using UnityEngine.Events;
|
||
using System.Linq;
|
||
|
||
#endif
|
||
|
||
#if ENABLE_INPUT_SYSTEM
|
||
using UnityEngine.InputSystem;
|
||
#endif
|
||
|
||
namespace Kirurobo
|
||
{
|
||
/// @cond DOXYGEN_SHOW_INTERNAL_CLASSES
|
||
/// <summary>
|
||
/// 使布尔属性可编辑
|
||
/// </summary>
|
||
[System.AttributeUsage(System.AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
|
||
public class EditablePropertyAttribute : UnityEngine.PropertyAttribute
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将属性设置为只读
|
||
/// </summary>
|
||
[System.AttributeUsage(System.AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
|
||
public class ReadOnlyAttribute : UnityEngine.PropertyAttribute
|
||
{
|
||
}
|
||
|
||
/// @endcond
|
||
/// <summary>
|
||
/// Windows/Mac 统一窗口控制器
|
||
/// </summary>
|
||
public class UniWindowController : MonoBehaviour
|
||
{
|
||
/// <summary>
|
||
/// 与 UniWinCore.TransparentType 相同
|
||
/// </summary>
|
||
public enum TransparentType : int
|
||
{
|
||
None = 0,
|
||
Alpha = 1,
|
||
ColorKey = 2,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定点击测试方法(即切换点击穿透)
|
||
/// </summary>
|
||
public enum HitTestType : int
|
||
{
|
||
None = 0,
|
||
Opacity = 1,
|
||
Raycast = 2,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 标识 <see cref="OnStateChanged">OnStateChanged</see> 事件发生时的类型
|
||
/// </summary>
|
||
[Flags]
|
||
public enum WindowStateEventType : int
|
||
{
|
||
None = 0,
|
||
StyleChanged = 1,
|
||
Resized = 2,
|
||
|
||
// 以下规格可能会有变化
|
||
TopMostEnabled = 16 + 1 + 8,
|
||
TopMostDisabled = 16 + 1,
|
||
BottomMostEnabled = 32 + 1 + 8,
|
||
BottomMostDisabled = 32 + 1,
|
||
WallpaperModeEnabled = 64 + 1 + 8,
|
||
WallpaperModeDisabled = 64 + 1,
|
||
};
|
||
|
||
/// <summary>
|
||
/// 鼠标按键
|
||
/// </summary>
|
||
[Flags]
|
||
public enum MouseButton : int
|
||
{
|
||
None = 0,
|
||
Left = 1,
|
||
Right = 2,
|
||
Middle = 4,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 修饰键
|
||
/// </summary>
|
||
[Flags]
|
||
public enum ModifierKey : int
|
||
{
|
||
None = 0,
|
||
Alt = 1,
|
||
Control = 2,
|
||
Shift = 4,
|
||
Command = 8,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前 UniWindowController 实例
|
||
/// </summary>
|
||
public static UniWindowController current => _current ? _current : FindOrCreateInstance();
|
||
|
||
private static UniWindowController _current;
|
||
|
||
/// <summary>
|
||
/// 底层类
|
||
/// </summary>
|
||
private UniWinCore _uniWinCore = null;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否接收鼠标事件
|
||
/// </summary>
|
||
public bool isClickThrough
|
||
{
|
||
get { return _isClickThrough; }
|
||
set { SetClickThrough(value); }
|
||
}
|
||
|
||
private bool _isClickThrough = false;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否透明
|
||
/// </summary>
|
||
public bool isTransparent
|
||
{
|
||
get { return _isTransparent; }
|
||
set { SetTransparent(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("选中后启动时设置为透明")]
|
||
private bool _isTransparent = false;
|
||
|
||
/// <summary>
|
||
/// 窗口透明度(0.0 到 1.0)
|
||
/// </summary>
|
||
public float alphaValue
|
||
{
|
||
get { return _alphaValue; }
|
||
set { SetAlphaValue(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("窗口透明度"), Range(0f, 1f)]
|
||
private float _alphaValue = 1.0f;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否置顶
|
||
/// </summary>
|
||
public bool isTopmost
|
||
{
|
||
get { return ((_uniWinCore == null) ? _isTopmost : _isTopmost = _uniWinCore.IsTopmost); }
|
||
set { SetTopmost(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("选中后启动时置顶")]
|
||
private bool _isTopmost = false;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否置底
|
||
/// </summary>
|
||
public bool isBottommost
|
||
{
|
||
get { return ((_uniWinCore == null) ? _isBottommost : _isBottommost = _uniWinCore.IsBottommost); }
|
||
set { SetBottommost(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("选中后启动时置底")]
|
||
private bool _isBottommost = false;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否最大化
|
||
/// </summary>
|
||
public bool isZoomed
|
||
{
|
||
get { return ((_uniWinCore == null) ? _isZoomed : _isZoomed = _uniWinCore.GetZoomed()); }
|
||
set { SetZoomed(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("选中后启动时最大化")]
|
||
private bool _isZoomed = false;
|
||
|
||
/// <summary>
|
||
/// 此窗口是否适配到显示器
|
||
/// </summary>
|
||
public bool shouldFitMonitor
|
||
{
|
||
get { return _shouldFitMonitor; }
|
||
set { FitToMonitor(value, _monitorToFit); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("选中后将窗口适配到显示器")]
|
||
private bool _shouldFitMonitor = false;
|
||
|
||
/// <summary>
|
||
/// 适配窗口的目标显示器索引(0, 1, ...)
|
||
/// </summary>
|
||
public int monitorToFit
|
||
{
|
||
get { return _monitorToFit; }
|
||
set { FitToMonitor(_shouldFitMonitor, value); }
|
||
}
|
||
|
||
private int _monitorToFit = 0;
|
||
|
||
/// <summary>
|
||
/// 启用/禁用接受文件拖放
|
||
/// </summary>
|
||
public bool allowDropFiles
|
||
{
|
||
get { return _allowDropFiles; }
|
||
set { SetAllowDrop(value); }
|
||
}
|
||
|
||
[SerializeField, EditableProperty, Tooltip("启用文件或文件夹拖放")]
|
||
private bool _allowDropFiles = false;
|
||
|
||
/// <summary>
|
||
/// 是否启用点击穿透自动判定
|
||
/// 如果禁用,可以手动修改 isClickThrough
|
||
/// </summary>
|
||
public bool isHitTestEnabled = true;
|
||
|
||
/// <summary>
|
||
/// 点击穿透自动判定的方法
|
||
/// </summary>
|
||
[Tooltip("选择方法")] public HitTestType hitTestType = HitTestType.Opacity;
|
||
|
||
/// <summary>
|
||
/// 点击穿透判定方法为不透明度时使用的阈值
|
||
/// 鼠标下方像素的 alpha 达到此值时判定为命中
|
||
/// </summary>
|
||
[Tooltip("点击测试类型为 Opacity 时可用"), RangeAttribute(0f, 1f)]
|
||
public float opacityThreshold = 0.1f;
|
||
|
||
/// <summary>
|
||
/// 点击穿透判定方法为 raycast 时的最远距离
|
||
/// </summary>
|
||
private float raycastMaxDepth = 100.0f;
|
||
|
||
/// <summary>
|
||
/// 启用后,窗口透明时会自动将相机背景改为单色黑色透明
|
||
/// </summary>
|
||
[Header("高级设置")] [Tooltip("窗口透明时更改相机背景")]
|
||
public bool autoSwitchCameraBackground = true;
|
||
|
||
/// <summary>
|
||
/// 启用后,启动时如果是全屏则会强制退出全屏
|
||
///
|
||
/// 用于即使启动对话框设置了全屏,也能切换为窗口模式
|
||
/// 仅在启动时生效
|
||
/// 在 Mac 上,即使强制退出全屏,似乎仍会变成另一个画面,效果不大
|
||
/// </summary>
|
||
[Tooltip("启动时强制窗口模式")] public bool forceWindowed = false;
|
||
|
||
/// <summary>
|
||
/// 相机实例
|
||
/// </summary>
|
||
[Tooltip("未设置时使用主相机")] public Camera currentCamera;
|
||
|
||
/// <summary>
|
||
/// 透明方式的指定
|
||
/// </summary>
|
||
[Header("仅限 Windows")] [Tooltip("选择透明方式。*仅 Windows 可用")]
|
||
public TransparentType transparentType = TransparentType.Alpha;
|
||
|
||
/// <summary>
|
||
/// 当透明类型为 ColorKey 时使用的键色
|
||
/// </summary>
|
||
[Tooltip("将在窗口下次变为透明时使用")] public Color32 keyColor = new Color32(0x01, 0x00, 0x01, 0x00);
|
||
|
||
/// <summary>
|
||
/// 在 macOS 上,是否允许将窗口放置在菜单栏上方
|
||
/// </summary>
|
||
public bool isFreePositioningEnabled
|
||
{
|
||
get
|
||
{
|
||
return ((_uniWinCore == null)
|
||
? _isFreePositioningEnabled
|
||
: _isFreePositioningEnabled = _uniWinCore.IsFreePositioningEnabled);
|
||
}
|
||
set { SetFreePositioning(value); }
|
||
}
|
||
|
||
[Header("仅限 macOS")] [Tooltip("禁用 constrainFrameRect() *仅 macOS 可用")] [SerializeField, EditableProperty]
|
||
private bool _isFreePositioningEnabled = false;
|
||
|
||
/// <summary>
|
||
/// 鼠标指针是否在不透明像素或物体上
|
||
/// </summary>
|
||
[Header("状态")] [SerializeField, ReadOnly, Tooltip("鼠标指针是否在不透明像素上?(只读)")]
|
||
private bool onObject = true;
|
||
|
||
/// <summary>
|
||
/// 鼠标指针下方的像素颜色。(只读)
|
||
/// </summary>
|
||
[SerializeField, ReadOnly, Tooltip("鼠标指针下方的像素颜色。(只读)")]
|
||
public Color pickedColor;
|
||
|
||
/// <summary>
|
||
/// 获取/设置窗口坐标
|
||
/// </summary>
|
||
public Vector2 windowPosition
|
||
{
|
||
get { return (_uniWinCore != null ? _uniWinCore.GetWindowPosition() : Vector2.zero); }
|
||
set { _uniWinCore?.SetWindowPosition(value); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取/设置窗口大小
|
||
/// </summary>
|
||
public Vector2 windowSize
|
||
{
|
||
get { return (_uniWinCore != null ? _uniWinCore.GetWindowSize() : Vector2.zero); }
|
||
set { _uniWinCore?.SetWindowSize(value); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取客户区域大小
|
||
/// </summary>
|
||
public Vector2 clientSize
|
||
{
|
||
get { return (_uniWinCore != null ? _uniWinCore.GetClientSize() : Vector2.zero); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取/设置鼠标光标坐标
|
||
/// </summary>
|
||
public Vector2 cursorPosition
|
||
{
|
||
get { return UniWinCore.GetCursorPosition(); }
|
||
set { UniWinCore.SetCursorPosition(value); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始状态的窗口位置和大小
|
||
/// </summary>
|
||
private Rect originalWindowRectangle;
|
||
|
||
// 存储相机原有背景,以便在窗口透明时替换为 alpha 为零的黑色
|
||
private CameraClearFlags originalCameraClearFlags;
|
||
private Color originalCameraBackground;
|
||
|
||
/// <summary>
|
||
/// 存储光标下方 1 像素颜色的纹理
|
||
/// </summary>
|
||
private Texture2D colorPickerTexture = null;
|
||
|
||
/// <summary>
|
||
/// Raycast 使用的鼠标事件信息
|
||
/// </summary>
|
||
private PointerEventData pointerEventData;
|
||
|
||
/// <summary>
|
||
/// Raycast 时的图层遮罩
|
||
/// </summary>
|
||
private int hitTestLayerMask;
|
||
|
||
/// <summary>
|
||
/// 窗口样式改变时发生
|
||
/// </summary>
|
||
public event OnStateChangedDelegate OnStateChanged;
|
||
|
||
public delegate void OnStateChangedDelegate(WindowStateEventType type);
|
||
|
||
public delegate void FilesDelegate(string[] files);
|
||
|
||
/// <summary>
|
||
/// 文件或文件夹被拖放后发生
|
||
/// </summary>
|
||
public event FilesDelegate OnDropFiles;
|
||
|
||
/// <summary>
|
||
/// 显示器设置或分辨率改变时发生
|
||
/// </summary>
|
||
public event OnMonitorChangedDelegate OnMonitorChanged;
|
||
|
||
public delegate void OnMonitorChangedDelegate();
|
||
|
||
|
||
// 用于初始化
|
||
void Awake()
|
||
{
|
||
// 用作单例。如果已有实例,则销毁自身
|
||
if (this != current)
|
||
{
|
||
Destroy(this.gameObject);
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
_current = this;
|
||
}
|
||
|
||
// 强制退出全屏。在编辑器中不做任何操作
|
||
#if !UNITY_EDITOR
|
||
if (forceWindowed && Screen.fullScreen)
|
||
{
|
||
Screen.fullScreen = false;
|
||
}
|
||
#endif
|
||
|
||
if (!currentCamera)
|
||
{
|
||
// 寻找主相机
|
||
currentCamera = Camera.main;
|
||
|
||
// 如果主相机未找到,则使用 Find 查找
|
||
//if (!currentCamera)
|
||
//{
|
||
// currentCamera = GameObject.FindAnyObjectByType<Camera>();
|
||
//}
|
||
}
|
||
|
||
// 记录相机原始背景
|
||
if (currentCamera)
|
||
{
|
||
originalCameraClearFlags = currentCamera.clearFlags;
|
||
originalCameraBackground = currentCamera.backgroundColor;
|
||
}
|
||
|
||
// 鼠标事件信息
|
||
pointerEventData = new PointerEventData(EventSystem.current);
|
||
|
||
// 使用 Ignore Raycast 之外的图层作为有效遮罩
|
||
hitTestLayerMask = ~LayerMask.GetMask("Ignore Raycast");
|
||
|
||
// 准备用于提取鼠标下方像素颜色的纹理
|
||
colorPickerTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
|
||
|
||
// 创建窗口控制实例
|
||
_uniWinCore = new UniWinCore();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 适配到指定显示器
|
||
/// </summary>
|
||
private void UpdateMonitorFitting()
|
||
{
|
||
if (!_shouldFitMonitor) return;
|
||
|
||
int monitors = UniWinCore.GetMonitorCount();
|
||
int targetMonitorIndex = _monitorToFit;
|
||
|
||
if (targetMonitorIndex < 0)
|
||
{
|
||
targetMonitorIndex = 0;
|
||
}
|
||
|
||
if (monitors <= targetMonitorIndex)
|
||
{
|
||
targetMonitorIndex = monitors - 1;
|
||
}
|
||
|
||
if (targetMonitorIndex >= 0)
|
||
{
|
||
_uniWinCore.FitToMonitor(targetMonitorIndex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查找现有实例或创建新实例
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
private static UniWindowController FindOrCreateInstance()
|
||
{
|
||
#if UNITY_2022_1_OR_NEWER
|
||
var instance = GameObject.FindAnyObjectByType<UniWindowController>();
|
||
#else
|
||
var instance = GameObject.FindObjectOfType<UniWindowController>();
|
||
#endif
|
||
|
||
// 目前禁止自动创建
|
||
// // 场景中未找到时创建新实例
|
||
// if (!instance)
|
||
// {
|
||
// var obj = new GameObject(nameof(UniWindowController));
|
||
// obj.AddComponent<UniWindowController>();
|
||
// }
|
||
|
||
return instance;
|
||
}
|
||
|
||
void Start()
|
||
{
|
||
//// New Input System 存在兼容性问题,用于验证输出
|
||
// #if ENABLE_LEGACY_INPUT_MANAGER
|
||
// Debug.Log("使用旧版输入管理器。");
|
||
// #elif ENABLE_INPUT_SYSTEM
|
||
// Debug.Log("使用新版输入系统。");
|
||
// Debug.Log("后台运行 " + Mouse.current.canRunInBackground);
|
||
// #else
|
||
// Debug.Log("鼠标位置不可用。");
|
||
// #endif
|
||
|
||
// 启动获取鼠标光标下颜色的协程
|
||
StartCoroutine(HitTestCoroutine());
|
||
|
||
// 获取初始窗口大小和位置
|
||
StoreOriginalWindowRectangle();
|
||
|
||
// 适配到所选显示器
|
||
OnMonitorChanged += UpdateMonitorFitting;
|
||
UpdateMonitorFitting();
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
_uniWinCore.Dispose();
|
||
}
|
||
|
||
// 同时销毁实例
|
||
if (this == current)
|
||
{
|
||
_current = null;
|
||
}
|
||
}
|
||
|
||
void StoreOriginalWindowRectangle()
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
var size = _uniWinCore.GetWindowSize();
|
||
var pos = _uniWinCore.GetWindowPosition();
|
||
originalWindowRectangle = new Rect(pos, size);
|
||
}
|
||
}
|
||
|
||
// 每一帧调用 Update
|
||
void Update()
|
||
{
|
||
// 如果尚未获取自身窗口,则获取
|
||
if (_uniWinCore == null || !_uniWinCore.IsActive)
|
||
{
|
||
UpdateTargetWindow();
|
||
}
|
||
else
|
||
{
|
||
_uniWinCore.Update();
|
||
}
|
||
|
||
// 处理事件
|
||
UpdateEvents();
|
||
|
||
// 更新键盘、鼠标操作对下方窗口的穿透状态
|
||
UpdateClickThrough();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查并处理 UniWinCore 事件
|
||
/// </summary>
|
||
private void UpdateEvents()
|
||
{
|
||
if (_uniWinCore == null) return;
|
||
|
||
if (_uniWinCore.ObserveDroppedFiles(out var droppedFiles))
|
||
{
|
||
OnDropFiles?.Invoke(droppedFiles);
|
||
}
|
||
|
||
if (_uniWinCore.ObserveMonitorChanged())
|
||
{
|
||
OnMonitorChanged?.Invoke();
|
||
}
|
||
|
||
if (_uniWinCore.ObserveWindowStyleChanged(out var type))
|
||
{
|
||
// // 指定了适配显示器时,最大化被取消的情况下
|
||
// if (shouldFitMonitor && !uniWinCore.GetZoomed())
|
||
// {
|
||
// //StartCoroutine("ForceZoomed"); // 延迟强制最大化
|
||
// //SetZoomed(true); // 强制最大化 ←不一定生效
|
||
// //shouldFitMonitor = false; // 禁用适配
|
||
// }
|
||
if (_shouldFitMonitor) StartCoroutine("ForceZoomed"); // 延迟强制最大化
|
||
|
||
OnStateChanged?.Invoke((WindowStateEventType)type);
|
||
}
|
||
}
|
||
|
||
IEnumerator ForceZoomed()
|
||
{
|
||
yield return new WaitForSeconds(0.5f);
|
||
if (_shouldFitMonitor && !_uniWinCore.GetZoomed()) SetZoomed(true);
|
||
yield return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定相机。如果之前有相机,则恢复其背景
|
||
/// </summary>
|
||
/// <param name="newCamera"></param>
|
||
public void SetCamera(Camera newCamera)
|
||
{
|
||
// 如果相机已更改,恢复其设置
|
||
if (newCamera != currentCamera)
|
||
{
|
||
SetCameraBackground(false);
|
||
}
|
||
|
||
currentCamera = newCamera;
|
||
|
||
// 记录相机的原始背景
|
||
if (currentCamera)
|
||
{
|
||
originalCameraClearFlags = currentCamera.clearFlags;
|
||
originalCameraBackground = currentCamera.backgroundColor;
|
||
|
||
SetCameraBackground(_isTransparent);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将鼠标/触摸操作穿透到下方窗口
|
||
/// </summary>
|
||
/// <param name="isThrough"></param>
|
||
void SetClickThrough(bool isThrough)
|
||
{
|
||
_uniWinCore?.EnableClickThrough(isThrough);
|
||
_isClickThrough = isThrough;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据像素颜色切换操作接收状态
|
||
/// </summary>
|
||
void UpdateClickThrough()
|
||
{
|
||
// 没有自动点击测试则结束
|
||
if (!isHitTestEnabled || hitTestType == HitTestType.None) return;
|
||
|
||
// 鼠标光标隐藏状态视为在透明像素上
|
||
bool hit = (onObject);
|
||
|
||
if (_isClickThrough)
|
||
{
|
||
// 如果当前是点击穿透状态,仅在命中时取消穿透
|
||
if (hit)
|
||
{
|
||
SetClickThrough(false);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 如果当前不是穿透状态,仅在透明且未命中时启用穿透
|
||
if (isTransparent && !hit)
|
||
{
|
||
SetClickThrough(true);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在协程中重复进行光标下颜色或 Raycast 的点击测试
|
||
/// 使用 WaitForEndOfFrame() 所以采用协程
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
private IEnumerator HitTestCoroutine()
|
||
{
|
||
while (Application.isPlaying)
|
||
{
|
||
yield return new WaitForEndOfFrame();
|
||
|
||
// Windows 下,如果是单色透明则点击测试由 OS 负责,所以始终为命中
|
||
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
|
||
if (transparentType == TransparentType.ColorKey)
|
||
{
|
||
onObject = true;
|
||
}
|
||
else
|
||
#endif
|
||
if (hitTestType == HitTestType.Opacity)
|
||
{
|
||
HitTestByOpaquePixel();
|
||
}
|
||
else if (hitTestType == HitTestType.Raycast)
|
||
{
|
||
HitTestByRaycast();
|
||
}
|
||
else
|
||
{
|
||
// 无点击测试时始终为 true
|
||
onObject = true;
|
||
}
|
||
}
|
||
|
||
yield return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将屏幕上的鼠标坐标换算为 Unity 的屏幕坐标系
|
||
/// </summary>
|
||
private Vector2 GetClientCursorPosition()
|
||
{
|
||
// New Input System 在没有焦点时无法获取鼠标坐标,因此自行计算
|
||
Vector2 mousePos = UniWinCore.GetCursorPosition();
|
||
Vector2 winPos = windowPosition;
|
||
Rect clientRect = _uniWinCore.GetClientRectangle();
|
||
Vector2 unityPos = new Vector2(
|
||
(mousePos.x - winPos.x - clientRect.x) * Screen.width / clientRect.width,
|
||
(mousePos.y - winPos.y - clientRect.y) * Screen.height / clientRect.height
|
||
);
|
||
|
||
// // 调试用
|
||
// // 与 Unity 获取的值进行比较
|
||
// #if ENABLE_LEGACY_INPUT_MANAGER
|
||
// Vector2 position = Input.mousePosition;
|
||
// #elif ENABLE_INPUT_SYSTEM
|
||
// Vector2 position = Mouse.current.position.ReadValue();
|
||
// #endif
|
||
// if (!position.Equals(unityPos))
|
||
// {
|
||
// Debug.LogWarning("鼠标位置差异 : " + position + " / " + unityPos);
|
||
// }
|
||
|
||
// 在编辑器中始终使用 Unity 功能获取鼠标坐标
|
||
// Game 窗口可能不是唯一窗口,或 Scale 不同,无法简单计算
|
||
#if UNITY_EDITOR
|
||
#if ENABLE_LEGACY_INPUT_MANAGER
|
||
return Input.mousePosition;
|
||
#elif UNITY_EDITOR && ENABLE_INPUT_SYSTEM
|
||
return Mouse.current.position.ReadValue();
|
||
#else
|
||
return unityPos;
|
||
#endif
|
||
#else
|
||
return unityPos;
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查鼠标下方是否有不透明像素
|
||
/// </summary>
|
||
private void HitTestByOpaquePixel()
|
||
{
|
||
Vector2 mousePos = GetClientCursorPosition();
|
||
|
||
// 检查鼠标坐标
|
||
if (GetOnOpaquePixel(mousePos))
|
||
{
|
||
//Debug.Log("鼠标 " + mousePos);
|
||
onObject = true;
|
||
//activeFingerId = -1; // 取消触摸追踪
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
onObject = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回指定坐标的像素是否透明
|
||
/// </summary>
|
||
/// <param name="mousePos">坐标[px]。必须在绘制范围内。</param>
|
||
/// <returns></returns>
|
||
private bool GetOnOpaquePixel(Vector2 mousePos)
|
||
{
|
||
float w = Screen.width;
|
||
float h = Screen.height;
|
||
//Debug.Log(w + ", " + h);
|
||
|
||
// 在屏幕外则视为透明
|
||
if (
|
||
mousePos.x < 0 || mousePos.x >= w
|
||
|| mousePos.y < 0 || mousePos.y >= h
|
||
)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 如果不是透明状态,在范围内则视为不透明
|
||
if (!_isTransparent) return true;
|
||
|
||
// 如果是 LayeredWindow,点击测试由 OS 负责,窗口内返回true
|
||
if (transparentType == TransparentType.ColorKey) return true;
|
||
|
||
// 根据指定坐标的绘制结果进行判断
|
||
try // 在 WaitForEndOfFrame 时机执行的话,不需要 try 应该也没问题
|
||
{
|
||
// 参考 http://tsubakit1.hateblo.jp/entry/20131203/1386000440
|
||
colorPickerTexture.ReadPixels(new Rect(mousePos, Vector2.one), 0, 0);
|
||
Color color = colorPickerTexture.GetPixels32()[0];
|
||
pickedColor = color;
|
||
|
||
return (color.a >= opacityThreshold); // alpha 达到阈值则视为不透明
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogError(ex.Message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查鼠标下方是否有对象
|
||
/// </summary>
|
||
private void HitTestByRaycast()
|
||
{
|
||
Vector2 position = GetClientCursorPosition();
|
||
|
||
// // 判断是否在 uGUI 上
|
||
var raycastResults = new List<RaycastResult>();
|
||
pointerEventData.position = position;
|
||
EventSystem.current.RaycastAll(pointerEventData, raycastResults);
|
||
foreach (var result in raycastResults)
|
||
{
|
||
// 考虑图层遮罩(Ignore Raycast 以外的命中)
|
||
if (((1 << result.gameObject.layer) & hitTestLayerMask) > 0)
|
||
{
|
||
onObject = true;
|
||
return;
|
||
}
|
||
}
|
||
// 如果忽略图层限制直接命中,使用下面代码
|
||
// // 如果判定为在 uGUI 上,则结束
|
||
// if (EventSystem.current.IsPointerOverGameObject())
|
||
// {
|
||
// onObject = true;
|
||
// return;
|
||
// }
|
||
|
||
if (currentCamera && currentCamera.isActiveAndEnabled)
|
||
{
|
||
Ray ray = currentCamera.ScreenPointToRay(position);
|
||
|
||
// 判断是否在 3D 对象上
|
||
if (Physics.Raycast(ray, out _, raycastMaxDepth))
|
||
{
|
||
onObject = true;
|
||
return;
|
||
}
|
||
|
||
// 判断是否在 2D 对象上
|
||
var rayHit2D = Physics2D.GetRayIntersection(ray);
|
||
Debug.DrawRay(ray.origin, ray.direction, Color.blue, 2f, false);
|
||
if (rayHit2D.collider != null)
|
||
{
|
||
onObject = true;
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 如果相机无效,则获取主相机
|
||
currentCamera = Camera.main;
|
||
}
|
||
|
||
// 若均未命中,则判定为不在对象上
|
||
onObject = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 如果自己的窗口句柄不确定,则重新查找
|
||
/// </summary>
|
||
private void UpdateTargetWindow()
|
||
{
|
||
if (_uniWinCore == null)
|
||
{
|
||
_uniWinCore = new UniWinCore();
|
||
}
|
||
|
||
// 如果尚未获取窗口,则执行获取处理
|
||
if (!_uniWinCore.IsActive)
|
||
{
|
||
_uniWinCore.AttachMyWindow();
|
||
|
||
// 获取到窗口后设置初始值
|
||
if (_uniWinCore.IsActive)
|
||
{
|
||
_uniWinCore.SetTransparentType((UniWinCore.TransparentType)transparentType);
|
||
_uniWinCore.SetKeyColor(keyColor);
|
||
_uniWinCore.SetAlphaValue(_alphaValue);
|
||
SetTransparent(_isTransparent);
|
||
if (_isBottommost)
|
||
{
|
||
SetBottommost(_isBottommost);
|
||
}
|
||
else
|
||
{
|
||
SetTopmost(_isTopmost);
|
||
}
|
||
|
||
SetZoomed(_isZoomed);
|
||
SetClickThrough(_isClickThrough);
|
||
SetAllowDrop(_allowDropFiles);
|
||
SetFreePositioning(_isFreePositioningEnabled);
|
||
|
||
// 获取窗口时执行与显示器更改相同的处理
|
||
OnMonitorChanged?.Invoke();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 在编辑器中,由于 Game 视图可能被关闭或停靠,如果发生变化则更改目标窗口
|
||
// 如果活动窗口与当前目标相同,则不执行任何操作
|
||
_uniWinCore.AttachMyActiveWindow();
|
||
#endif
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 窗口焦点变化时调用
|
||
/// </summary>
|
||
/// <param name="focus"></param>
|
||
private void OnApplicationFocus(bool focus)
|
||
{
|
||
if (focus)
|
||
{
|
||
UpdateTargetWindow();
|
||
|
||
// 获取焦点的瞬间,强制关闭点击穿透
|
||
if (_isTransparent && isHitTestEnabled && transparentType != TransparentType.ColorKey)
|
||
{
|
||
SetClickThrough(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 窗口变为透明状态时,自动将背景改为透明单色
|
||
/// </summary>
|
||
/// <param name="transparent"></param>
|
||
void SetCameraBackground(bool transparent)
|
||
{
|
||
// 如果未指定相机或未启用自动切换,则不执行任何操作
|
||
if (!currentCamera || !autoSwitchCameraBackground) return;
|
||
|
||
// 如果需要透明,则将相机背景改为透明色
|
||
if (transparent)
|
||
{
|
||
// 如果尚未透明化,则记忆当前相机信息
|
||
if (!isTransparent)
|
||
{
|
||
originalCameraClearFlags = currentCamera.clearFlags;
|
||
originalCameraBackground = currentCamera.backgroundColor;
|
||
}
|
||
|
||
currentCamera.clearFlags = CameraClearFlags.SolidColor;
|
||
if (transparentType == TransparentType.ColorKey)
|
||
{
|
||
currentCamera.backgroundColor = keyColor;
|
||
}
|
||
else
|
||
{
|
||
currentCamera.backgroundColor = Color.clear;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
currentCamera.clearFlags = originalCameraClearFlags;
|
||
currentCamera.backgroundColor = originalCameraBackground;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换透明化状态
|
||
/// </summary>
|
||
/// <param name="transparent"></param>
|
||
private void SetTransparent(bool transparent)
|
||
{
|
||
SetCameraBackground(transparent);
|
||
_isTransparent = transparent;
|
||
#if !UNITY_EDITOR
|
||
if (_uniWinCore != null)
|
||
{
|
||
_uniWinCore.EnableTransparent(transparent);
|
||
}
|
||
#endif
|
||
UpdateClickThrough();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更改透明方式
|
||
/// </summary>
|
||
/// <param name="type"></param>
|
||
public void SetTransparentType(TransparentType type)
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
// 如果正在透明中,则先解除再重新透明
|
||
if (_isTransparent)
|
||
{
|
||
SetTransparent(false);
|
||
_uniWinCore.SetTransparentType((UniWinCore.TransparentType)type);
|
||
transparentType = type;
|
||
SetTransparent(true);
|
||
}
|
||
else
|
||
{
|
||
_uniWinCore.SetTransparentType((UniWinCore.TransparentType)type);
|
||
transparentType = type;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置窗口透明度
|
||
/// </summary>
|
||
/// <param name="alpha">0.0 到 1.0</param>
|
||
private void SetAlphaValue(float alpha)
|
||
{
|
||
_alphaValue = alpha;
|
||
_uniWinCore?.SetAlphaValue(_alphaValue);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换置顶
|
||
/// </summary>
|
||
/// <param name="topmost"></param>
|
||
private void SetTopmost(bool topmost)
|
||
{
|
||
//if (_isTopmost == topmost) return;
|
||
if (_uniWinCore == null) return;
|
||
|
||
_uniWinCore.EnableTopmost(topmost);
|
||
_isTopmost = _uniWinCore.IsTopmost;
|
||
_isBottommost = _uniWinCore.IsBottommost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换始终置底
|
||
/// </summary>
|
||
/// <param name="bottommost"></param>
|
||
private void SetBottommost(bool bottommost)
|
||
{
|
||
if (_uniWinCore == null) return;
|
||
|
||
_uniWinCore.EnableBottommost(bottommost);
|
||
_isBottommost = _uniWinCore.IsBottommost;
|
||
_isTopmost = _uniWinCore.IsTopmost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最大化/还原
|
||
/// </summary>
|
||
/// <param name="zoomed"></param>
|
||
private void SetZoomed(bool zoomed)
|
||
{
|
||
if (_uniWinCore == null) return;
|
||
|
||
_uniWinCore.SetZoomed(zoomed);
|
||
_isZoomed = _uniWinCore.GetZoomed();
|
||
}
|
||
|
||
private void SetAllowDrop(bool enabled)
|
||
{
|
||
if (_uniWinCore == null) return;
|
||
|
||
_uniWinCore.SetAllowDrop(enabled);
|
||
_allowDropFiles = enabled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在 macOS 上,允许将窗口放置在包含菜单栏上方在内的任意位置
|
||
/// </summary>
|
||
/// <param name="enabled"></param>
|
||
private void SetFreePositioning(bool enabled)
|
||
{
|
||
if (_uniWinCore == null) return;
|
||
|
||
_uniWinCore.EnableFreePositioning(enabled);
|
||
_isFreePositioningEnabled = _uniWinCore.IsFreePositioningEnabled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取连接的显示器数量
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public static int GetMonitorCount()
|
||
{
|
||
//if (uniWinCore == null) return 0;
|
||
return UniWinCore.GetMonitorCount();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取显示器的位置和大小
|
||
/// </summary>
|
||
/// <param name="index"></param>
|
||
/// <returns></returns>
|
||
public static Rect GetMonitorRect(int index)
|
||
{
|
||
if (UniWinCore.GetMonitorRectangle(index, out Vector2 position, out Vector2 size))
|
||
{
|
||
return new Rect(position, size);
|
||
}
|
||
|
||
return Rect.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 适配到指定显示器
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
private bool FitToMonitor(bool shouldFit, int monitorIndex)
|
||
{
|
||
if (_uniWinCore == null)
|
||
{
|
||
_shouldFitMonitor = shouldFit;
|
||
_monitorToFit = monitorIndex;
|
||
return false;
|
||
}
|
||
|
||
if (shouldFit)
|
||
{
|
||
if (!_shouldFitMonitor)
|
||
{
|
||
// 之前未适配的情况
|
||
_monitorToFit = monitorIndex;
|
||
_shouldFitMonitor = shouldFit;
|
||
UpdateMonitorFitting();
|
||
}
|
||
else
|
||
{
|
||
if (_monitorToFit != monitorIndex)
|
||
{
|
||
// 适配的显示器发生变化的情况
|
||
_monitorToFit = monitorIndex;
|
||
UpdateMonitorFitting();
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (_shouldFitMonitor)
|
||
{
|
||
// 之前是适配状态,现在被取消的情况
|
||
_monitorToFit = monitorIndex;
|
||
_shouldFitMonitor = shouldFit;
|
||
UpdateMonitorFitting();
|
||
|
||
_uniWinCore.SetZoomed(false);
|
||
//uniWinCore.SetWindowSize(originalWindowRectangle.size);
|
||
//uniWinCore.SetWindowPosition(originalWindowRectangle.position);
|
||
}
|
||
else
|
||
{
|
||
// 未在适配中时,仅更改选择
|
||
_monitorToFit = monitorIndex;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取鼠标光标位置
|
||
/// </summary>
|
||
/// <returns>光标位置</returns>
|
||
public static Vector2 GetCursorPosition()
|
||
{
|
||
return UniWinCore.GetCursorPosition();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置鼠标光标位置
|
||
/// </summary>
|
||
/// <param name="position"></param>
|
||
public static void SetCursorPosition(Vector2 position)
|
||
{
|
||
UniWinCore.SetCursorPosition(position);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取鼠标按键状态
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public static MouseButton GetMouseButtons()
|
||
{
|
||
int buttons = UniWinCore.GetMouseButtons();
|
||
return (MouseButton)buttons;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取按下的修饰键
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public static ModifierKey GetModifierKeys()
|
||
{
|
||
int mod = UniWinCore.GetModifierKeys();
|
||
return (ModifierKey)mod;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 退出时需要恢复窗口状态
|
||
/// </summary>
|
||
void OnApplicationQuit()
|
||
{
|
||
if (Application.isPlaying)
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
// 在编辑器中恢复窗口状态
|
||
// 在独立构建中会看到恢复过程,因此跳过
|
||
#if UNITY_EDITOR
|
||
_uniWinCore.SetWindowSize(originalWindowRectangle.size);
|
||
_uniWinCore.SetWindowPosition(originalWindowRectangle.position);
|
||
|
||
_uniWinCore.DetachWindow();
|
||
#endif
|
||
_uniWinCore.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将焦点给予自身窗口
|
||
/// </summary>
|
||
public void Focus()
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
//uniWin.SetFocus();
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 仅供调试用。用于获取各阶段参考信息的函数
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[Obsolete]
|
||
public int GetDebugInfo()
|
||
{
|
||
if (_uniWinCore != null)
|
||
{
|
||
return UniWinCore.GetDebugInfo();
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
} |