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
2022-06-17 15:37:17 +08:00
#nullable disable
2018-04-13 17:19:50 +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-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
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-04-13 17:19:50 +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-04-13 17:19:50 +08:00
using osu.Framework.Platform ;
2018-04-13 20:13:09 +08:00
using osu.Framework.Threading ;
2018-04-13 17:19:50 +08:00
using osu.Game.Configuration ;
using osu.Game.Input.Bindings ;
2023-06-01 14:50:37 +08:00
using osu.Game.Online.Multiplayer ;
2018-04-13 17:19:50 +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 ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Graphics
{
2022-11-24 13:32:20 +08:00
public partial class ScreenshotManager : Component , IKeyBindingHandler < GlobalAction > , IHandleGlobalKeyboardInput
2018-04-13 17:19:50 +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 ;
2018-04-13 17:19:50 +08:00
private Bindable < ScreenshotFormat > screenshotFormat ;
2018-04-13 20:13:09 +08:00
private Bindable < bool > captureMenuCursor ;
2020-02-14 21:14:00 +08:00
[Resolved]
private GameHost host { get ; set ; }
2023-07-11 17:42:31 +08:00
[Resolved]
private Clipboard clipboard { get ; set ; }
2018-04-13 17:19:50 +08:00
private Storage storage ;
2020-02-14 21:14:00 +08:00
[Resolved]
2022-04-18 18:59:57 +08:00
private INotificationOverlay notificationOverlay { get ; set ; }
2018-04-13 17:19:50 +08:00
2021-01-19 16:11:40 +08:00
private Sample shutter ;
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
2020-02-14 21:14:00 +08:00
private void load ( OsuConfigManager config , Storage storage , AudioManager audio )
2018-04-13 17:19:50 +08:00
{
this . storage = storage . GetStorageForDirectory ( @"screenshots" ) ;
screenshotFormat = config . GetBindable < ScreenshotFormat > ( OsuSetting . ScreenshotFormat ) ;
2018-04-13 20:13:09 +08:00
captureMenuCursor = config . GetBindable < bool > ( OsuSetting . ScreenshotCaptureMenuCursor ) ;
2018-04-13 17:19:50 +08:00
2019-05-28 16:06:01 +08:00
shutter = audio . Samples . Get ( "UI/shutter" ) ;
2018-04-13 17:19:50 +08:00
}
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < GlobalAction > e )
2018-04-13 17:19:50 +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-04-13 17:19:50 +08:00
{
case GlobalAction . TakeScreenshot :
shutter . Play ( ) ;
2023-06-01 14:50:37 +08:00
TakeScreenshotAsync ( ) . FireAndForget ( ) ;
2018-04-13 17:19:50 +08:00
return true ;
}
return false ;
}
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-04-13 17:19:50 +08:00
{
2018-04-13 20:13:09 +08:00
Interlocked . Increment ( ref screenShotTasks ) ;
2023-06-01 14:50:10 +08:00
try
2018-04-13 20:13:09 +08:00
{
2023-06-01 14:50:10 +08:00
if ( ! captureMenuCursor . Value )
{
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
2023-06-01 14:50:10 +08:00
using ( var 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
2023-06-01 14:50:10 +08:00
using ( var image = await host . TakeScreenshotAsync ( ) . ConfigureAwait ( false ) )
{
2023-07-11 17:42:31 +08:00
clipboard . SetImage ( image ) ;
2022-02-18 01:43:36 +08:00
2023-06-01 14:50:10 +08:00
( string filename , var stream ) = getWritableStream ( ) ;
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
{
2023-06-01 14:50:10 +08:00
switch ( screenshotFormat . Value )
{
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 :
throw new InvalidOperationException ( $"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}." ) ;
}
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-04-13 17:19:50 +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-04-13 17:19:50 +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 ( ) ;
private ( string filename , Stream stream ) getWritableStream ( )
2018-04-13 17:19:50 +08:00
{
2023-01-10 00:37:16 +08:00
lock ( filename_reservation_lock )
{
var dt = DateTime . Now ;
string fileExt = screenshotFormat . 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-04-13 17:19:50 +08:00
}
}
}