2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2018-03-21 11:29:44 +08:00
using System ;
2023-01-10 00:37:16 +08:00
using System.IO ;
2018-04-13 20:13:09 +08:00
using System.Threading ;
using System.Threading.Tasks ;
2018-03-14 05:17:12 +08:00
using osu.Framework.Allocation ;
2018-03-22 19:35:07 +08:00
using osu.Framework.Audio ;
using osu.Framework.Audio.Sample ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2020-02-07 04:02:03 +08:00
using osu.Framework.Graphics ;
2018-03-15 03:55:24 +08:00
using osu.Framework.Input ;
using osu.Framework.Input.Bindings ;
2021-09-16 17:26:12 +08:00
using osu.Framework.Input.Events ;
2018-03-14 05:17:12 +08:00
using osu.Framework.Platform ;
2018-04-13 20:13:09 +08:00
using osu.Framework.Threading ;
2018-03-14 05:17:12 +08:00
using osu.Game.Configuration ;
2018-03-15 03:55:24 +08:00
using osu.Game.Input.Bindings ;
2023-06-01 14:50:37 +08:00
using osu.Game.Online.Multiplayer ;
2018-03-17 02:25:00 +08:00
using osu.Game.Overlays ;
using osu.Game.Overlays.Notifications ;
2018-08-17 13:30:44 +08:00
using SixLabors.ImageSharp ;
2020-07-24 14:00:18 +08:00
using SixLabors.ImageSharp.Formats.Jpeg ;
2024-01-25 13:22:27 +08:00
using SixLabors.ImageSharp.PixelFormats ;
2024-01-25 09:27:10 +08:00
using SixLabors.ImageSharp.Processing ;
2018-04-13 17:19:50 +08:00
2018-03-14 05:17:12 +08:00
namespace osu.Game.Graphics
{
2020-02-07 04:22:30 +08:00
public partial class ScreenshotManager : Component , IKeyBindingHandler < GlobalAction > , IHandleGlobalKeyboardInput
2018-03-14 05:17:12 +08:00
{
2018-04-13 20:13:09 +08:00
private readonly BindableBool cursorVisibility = new BindableBool ( true ) ;
/// <summary>
2018-04-13 20:15:08 +08:00
/// Changed when screenshots are being or have finished being taken, to control whether cursors should be visible.
2018-04-13 20:13:09 +08:00
/// If cursors should not be visible, cursors have 3 frames to hide themselves.
/// </summary>
public IBindable < bool > CursorVisibility = > cursorVisibility ;
2024-01-25 13:18:20 +08:00
[Resolved]
private GameHost host { get ; set ; } = null ! ;
2018-04-13 20:13:09 +08:00
2020-02-14 21:14:00 +08:00
[Resolved]
2024-01-25 13:18:20 +08:00
private Clipboard clipboard { get ; set ; } = null ! ;
2020-02-14 21:14:00 +08:00
2023-07-11 17:42:31 +08:00
[Resolved]
2024-01-25 13:18:20 +08:00
private INotificationOverlay notificationOverlay { get ; set ; } = null ! ;
2023-07-11 17:42:31 +08:00
2024-01-25 13:22:27 +08:00
[Resolved]
private OsuConfigManager config { get ; set ; } = null ! ;
2024-01-25 13:18:20 +08:00
private Storage storage = null ! ;
2020-02-14 21:14:00 +08:00
2024-01-25 13:18:20 +08:00
private Sample ? shutter ;
2018-04-13 17:19:50 +08:00
2018-03-14 05:17:12 +08:00
[BackgroundDependencyLoader]
2024-01-25 13:22:27 +08:00
private void load ( Storage storage , AudioManager audio )
2018-03-14 05:17:12 +08:00
{
this . storage = storage . GetStorageForDirectory ( @"screenshots" ) ;
2019-05-28 16:06:01 +08:00
shutter = audio . Samples . Get ( "UI/shutter" ) ;
2018-03-14 05:17:12 +08:00
}
2018-04-13 17:19:50 +08:00
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < GlobalAction > e )
2018-03-15 03:55:24 +08:00
{
2021-11-18 11:35:47 +08:00
if ( e . Repeat )
return false ;
2021-09-16 17:26:12 +08:00
switch ( e . Action )
2018-03-15 03:55:24 +08:00
{
case GlobalAction . TakeScreenshot :
2024-01-25 13:18:20 +08:00
shutter ? . Play ( ) ;
2023-06-01 14:50:37 +08:00
TakeScreenshotAsync ( ) . FireAndForget ( ) ;
2018-03-15 03:55:24 +08:00
return true ;
}
2018-04-13 17:19:50 +08:00
2018-03-15 03:55:24 +08:00
return false ;
}
2018-04-13 17:19:50 +08:00
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < GlobalAction > e )
2020-01-22 12:22:34 +08:00
{
}
2018-04-13 17:19:50 +08:00
2018-04-13 20:13:09 +08:00
private volatile int screenShotTasks ;
2018-08-29 19:57:48 +08:00
public Task TakeScreenshotAsync ( ) = > Task . Run ( async ( ) = >
2018-03-14 05:17:12 +08:00
{
2018-04-13 20:13:09 +08:00
Interlocked . Increment ( ref screenShotTasks ) ;
2024-01-25 13:22:27 +08:00
ScreenshotFormat screenshotFormat = config . Get < ScreenshotFormat > ( OsuSetting . ScreenshotFormat ) ;
bool captureMenuCursor = config . Get < bool > ( OsuSetting . ScreenshotCaptureMenuCursor ) ;
2023-06-01 14:50:10 +08:00
try
2018-04-13 20:13:09 +08:00
{
2024-01-25 13:22:27 +08:00
if ( ! captureMenuCursor )
2023-06-01 14:50:10 +08:00
{
cursorVisibility . Value = false ;
2018-04-13 20:13:09 +08:00
2023-06-01 14:50:10 +08:00
// We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
const int frames_to_wait = 3 ;
2018-04-13 20:13:09 +08:00
2023-06-01 14:50:10 +08:00
int framesWaited = 0 ;
2018-04-13 20:13:09 +08:00
2024-01-25 13:22:27 +08:00
using ( ManualResetEventSlim framesWaitedEvent = new ManualResetEventSlim ( false ) )
2019-09-14 22:08:56 +08:00
{
2023-06-01 14:50:10 +08:00
ScheduledDelegate waitDelegate = host . DrawThread . Scheduler . AddDelayed ( ( ) = >
{
if ( framesWaited + + > = frames_to_wait )
// ReSharper disable once AccessToDisposedClosure
framesWaitedEvent . Set ( ) ;
} , 10 , true ) ;
2019-09-14 22:08:56 +08:00
2023-06-01 14:50:10 +08:00
if ( ! framesWaitedEvent . Wait ( 1000 ) )
throw new TimeoutException ( "Screenshot data did not arrive in a timely fashion" ) ;
2022-06-23 14:28:20 +08:00
2023-06-01 14:50:10 +08:00
waitDelegate . Cancel ( ) ;
}
2019-09-14 22:08:56 +08:00
}
2018-04-13 20:13:09 +08:00
2024-01-25 13:22:27 +08:00
using ( Image < Rgba32 > ? image = await host . TakeScreenshotAsync ( ) . ConfigureAwait ( false ) )
2023-06-01 14:50:10 +08:00
{
2024-01-25 13:30:26 +08:00
if ( config . Get < ScalingMode > ( OsuSetting . Scaling ) = = ScalingMode . Everything )
2024-01-25 09:27:10 +08:00
{
2024-01-25 13:30:26 +08:00
float posX = config . Get < float > ( OsuSetting . ScalingPositionX ) ;
float posY = config . Get < float > ( OsuSetting . ScalingPositionY ) ;
float sizeX = config . Get < float > ( OsuSetting . ScalingSizeX ) ;
float sizeY = config . Get < float > ( OsuSetting . ScalingSizeY ) ;
2024-01-25 09:27:10 +08:00
image . Mutate ( m = >
{
2024-01-25 13:30:26 +08:00
Rectangle rect = new Rectangle ( Point . Empty , m . GetCurrentSize ( ) ) ;
// Reduce size by user scale settings...
int sx = ( rect . Width - ( int ) ( rect . Width * sizeX ) ) / 2 ;
int sy = ( rect . Height - ( int ) ( rect . Height * sizeY ) ) / 2 ;
2024-01-25 09:27:10 +08:00
rect . Inflate ( - sx , - sy ) ;
2024-01-25 13:30:26 +08:00
// ...then adjust the region based on their positional offset.
2024-01-25 13:22:27 +08:00
rect . X = ( int ) ( rect . X * posX ) * 2 ;
rect . Y = ( int ) ( rect . Y * posY ) * 2 ;
2024-01-25 13:30:26 +08:00
2024-01-25 09:27:10 +08:00
m . Crop ( rect ) ;
} ) ;
}
2023-07-11 17:42:31 +08:00
clipboard . SetImage ( image ) ;
2022-02-18 01:43:36 +08:00
2024-01-25 13:22:27 +08:00
( string? filename , Stream ? stream ) = getWritableStream ( screenshotFormat ) ;
2018-04-13 17:19:50 +08:00
2023-06-01 14:50:10 +08:00
if ( filename = = null ) return ;
2018-04-13 17:19:50 +08:00
2023-06-01 14:50:10 +08:00
using ( stream )
2021-10-26 13:05:07 +08:00
{
2024-01-25 13:22:27 +08:00
switch ( screenshotFormat )
2023-06-01 14:50:10 +08:00
{
case ScreenshotFormat . Png :
await image . SaveAsPngAsync ( stream ) . ConfigureAwait ( false ) ;
break ;
2019-04-01 11:44:46 +08:00
2023-06-01 14:50:10 +08:00
case ScreenshotFormat . Jpg :
const int jpeg_quality = 92 ;
2020-07-24 14:26:45 +08:00
2023-06-01 14:50:10 +08:00
await image . SaveAsJpegAsync ( stream , new JpegEncoder { Quality = jpeg_quality } ) . ConfigureAwait ( false ) ;
break ;
2019-04-01 11:44:46 +08:00
2023-06-01 14:50:10 +08:00
default :
2024-01-25 13:22:27 +08:00
throw new InvalidOperationException ( $"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat}." ) ;
2023-06-01 14:50:10 +08:00
}
2021-10-26 13:05:07 +08:00
}
2018-04-13 17:19:50 +08:00
2023-06-01 14:50:10 +08:00
notificationOverlay . Post ( new SimpleNotification
2018-03-22 19:44:00 +08:00
{
2023-06-01 14:50:10 +08:00
Text = $"Screenshot {filename} saved!" ,
Activated = ( ) = >
{
storage . PresentFileExternally ( filename ) ;
return true ;
}
} ) ;
}
}
finally
{
if ( Interlocked . Decrement ( ref screenShotTasks ) = = 0 )
cursorVisibility . Value = true ;
2018-03-20 03:39:00 +08:00
}
2018-04-13 20:13:09 +08:00
} ) ;
2023-01-10 00:37:16 +08:00
private static readonly object filename_reservation_lock = new object ( ) ;
2024-01-25 13:22:27 +08:00
private ( string? filename , Stream ? stream ) getWritableStream ( ScreenshotFormat format )
2018-03-17 02:05:25 +08:00
{
2023-01-10 00:37:16 +08:00
lock ( filename_reservation_lock )
{
2024-01-25 13:22:27 +08:00
DateTime dt = DateTime . Now ;
string fileExt = format . ToString ( ) . ToLowerInvariant ( ) ;
2018-04-13 17:19:50 +08:00
2023-01-10 00:37:16 +08:00
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}" ;
if ( ! storage . Exists ( withoutIndex ) )
return ( withoutIndex , storage . GetStream ( withoutIndex , FileAccess . Write , FileMode . Create ) ) ;
2018-04-13 17:19:50 +08:00
2023-01-10 00:37:16 +08:00
for ( ulong i = 1 ; i < ulong . MaxValue ; i + + )
{
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}" ;
if ( ! storage . Exists ( indexedName ) )
return ( indexedName , storage . GetStream ( indexedName , FileAccess . Write , FileMode . Create ) ) ;
}
2018-04-13 17:19:50 +08:00
2023-01-10 00:37:16 +08:00
return ( null , null ) ;
}
2018-03-17 02:05:25 +08:00
}
2018-03-14 05:17:12 +08:00
}
}