Editor window class for the Supershot studio. Manages capturing the scene view, building/editing SuperShotDocument instances, an in-memory gallery of captures, undo/redo for edits, saving/exporting images, and sharing to Discord via webhooks.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Sandbox;
namespace Editor.SuperShot;
public sealed class CapturedShot
{
public Bitmap Raw { get; set; }
public EditSettings Edit { get; set; }
public SuperShotDocument Document { get; set; }
public Pixmap Thumbnail { get; set; }
public string SavedPath { get; set; }
public Vector2Int Size { get; set; }
public DateTime Time { get; set; } = DateTime.Now;
}
[EditorApp( "Supershot Studio", "photo_camera", "Take and edit screenshots from the editor camera" )]
public sealed class SuperShotWindow : DockWindow
{
public SuperShotSettings Settings => SuperShotSettings.Current;
public Bitmap RawCapture { get; private set; }
public bool HasCapture => RawCapture is not null && RawCapture.IsValid;
public SuperShotDocument ActiveDocument => _activeDocument;
public SuperShotLayer SelectedLayer => _activeDocument?.SelectedLayer;
public List<CapturedShot> Gallery { get; } = new();
public event Action Changed;
readonly List<SuperShotDocument> _undo = new();
readonly List<SuperShotDocument> _redo = new();
SuperShotDocument _activeDocument;
SuperShotDocument _editBaseline;
bool _reviewingGalleryShot;
CapturedShot _editingShot;
static SuperShotWindow _instance;
bool _duplicate;
public static SuperShotWindow Open()
{
if ( _instance is not null && _instance.IsValid )
{
_instance.Show();
_instance.Focus();
return _instance;
}
return new SuperShotWindow();
}
public SuperShotWindow()
{
if ( _instance is not null && _instance.IsValid )
{
_duplicate = true;
return;
}
_instance = this;
DeleteOnClose = true;
Title = "Supershot Studio";
Size = new Vector2( 1280, 820 );
SetWindowIcon( "photo_camera" );
LoadSavedShotsIntoGallery();
BuildDefaultLayout();
Show();
}
public override void Show()
{
if ( _duplicate )
{
if ( _instance is not null && _instance.IsValid )
{
_instance.Show();
_instance.Focus();
}
Destroy();
return;
}
base.Show();
}
public void NotifyChanged() => Changed?.Invoke();
public void CaptureNow()
{
var capture = BuildCaptureForShot();
var raw = SuperShotService.CaptureRaw( capture );
if ( raw is null )
{
Log.Warning( "[Supershot] Capture produced no image. Move the scene view (activate the Supershot framing tool) or pick a scene camera." );
return;
}
RawCapture?.Dispose();
RawCapture = raw;
_activeDocument = CreateDocumentForCapture( raw, capture );
_reviewingGalleryShot = false;
_editingShot = null;
_editBaseline = _activeDocument.Clone();
AddToGallery( raw, _activeDocument );
NotifyChanged();
}
public CaptureSettings BuildCaptureForShot()
{
var capture = CloneCapture( Settings.Capture );
var preset = Settings.FindFilterPreset( capture.DefaultFilterPresetId );
if ( preset?.UseForCapture == true && preset.PostFx is not null )
capture.PostFx = preset.PostFx.Clone();
return capture;
}
public string Capture()
{
CaptureNow();
if ( !HasCapture )
return null;
var path = SaveCurrent();
if ( path is not null && Gallery.Count > 0 )
_editingShot = Gallery[^1];
return path;
}
public void QuickCapture( ShotResolution resolution )
{
var previous = Settings.Capture.Resolution;
Settings.Capture.Resolution = resolution;
CaptureNow();
SaveCurrent();
Settings.Capture.Resolution = previous;
NotifyChanged();
}
public void CaptureAt( ShotResolution resolution )
{
var previous = Settings.Capture.Resolution;
Settings.Capture.Resolution = resolution;
CaptureNow();
Settings.Capture.Resolution = previous;
NotifyChanged();
}
public Bitmap BuildFinished()
{
if ( !HasCapture )
return null;
return SuperShotEdit.RenderDocument( RawCapture, _activeDocument, Settings );
}
public void PushEditUndo()
{
if ( _activeDocument is null )
return;
_undo.Add( _activeDocument.Clone() );
if ( _undo.Count > 64 ) _undo.RemoveAt( 0 );
_redo.Clear();
}
public void Undo()
{
if ( _undo.Count == 0 ) return;
_redo.Add( _activeDocument?.Clone() );
var prev = _undo[^1];
_undo.RemoveAt( _undo.Count - 1 );
SetActiveDocument( prev );
Settings.Save();
NotifyChanged();
}
public void Redo()
{
if ( _redo.Count == 0 ) return;
_undo.Add( _activeDocument?.Clone() );
var next = _redo[^1];
_redo.RemoveAt( _redo.Count - 1 );
SetActiveDocument( next );
Settings.Save();
NotifyChanged();
}
public void ResetEdit()
{
PushEditUndo();
if ( _activeDocument is not null )
{
_activeDocument.SelectedFilterPresetId = "";
_activeDocument.BaseEdit = new EditSettings();
_activeDocument.Layers.Clear();
_activeDocument.SelectedLayerId = "";
CopyEdit( _activeDocument.BaseEdit, Settings.Edit );
}
else
{
CopyEdit( new EditSettings(), Settings.Edit );
}
Settings.Save();
NotifyChanged();
}
static void CopyEdit( EditSettings from, EditSettings to )
{
to.Brightness = from.Brightness; to.Contrast = from.Contrast; to.Saturation = from.Saturation;
to.Exposure = from.Exposure; to.Hue = from.Hue; to.Filter = from.Filter;
to.Sharpen = from.Sharpen; to.Blur = from.Blur; to.Vignette = from.Vignette; to.Grain = from.Grain;
to.Rotate = from.Rotate; to.FlipH = from.FlipH; to.FlipV = from.FlipV;
to.Border = from.Border; to.BorderColor = from.BorderColor;
to.Watermark = from.Watermark; to.WatermarkText = from.WatermarkText;
to.WatermarkAnchor = from.WatermarkAnchor; to.WatermarkSize = from.WatermarkSize; to.WatermarkColor = from.WatermarkColor;
}
SuperShotDocument CreateDocumentForCapture( Bitmap raw, CaptureSettings capture )
{
Settings.EnsureBuiltinFilterPresets();
var document = new SuperShotDocument
{
Name = "Captured Shot",
Size = raw?.Size ?? default,
Capture = CloneCapture( capture ?? Settings.Capture ),
BaseEdit = new EditSettings(),
SelectedFilterPresetId = Settings.Capture.DefaultFilterPresetId ?? ""
};
if ( Settings.FindFilterPreset( document.SelectedFilterPresetId ) is null )
document.SelectedFilterPresetId = "";
CopyEdit( document.BaseEdit, Settings.Edit );
return document;
}
static CaptureSettings CloneCapture( CaptureSettings source )
{
if ( source is null )
return new CaptureSettings();
return new CaptureSettings
{
Source = source.Source,
Resolution = source.Resolution,
CustomWidth = source.CustomWidth,
CustomHeight = source.CustomHeight,
SuperSampling = source.SuperSampling,
OverrideFov = source.OverrideFov,
Fov = source.Fov,
TransparentBackground = source.TransparentBackground,
ShowGameUI = source.ShowGameUI,
HideUI = source.HideUI,
HideTags = source.HideTags,
Delay = source.Delay,
DefaultFilterPresetId = source.DefaultFilterPresetId,
PostFx = source.PostFx?.Clone() ?? new PostFxSettings()
};
}
void SetActiveDocument( SuperShotDocument document )
{
_activeDocument = document ?? new SuperShotDocument();
_activeDocument.Layers ??= new List<SuperShotLayer>();
_activeDocument.BaseEdit ??= new EditSettings();
if ( _editingShot is not null )
_editingShot.Document = _activeDocument;
CopyEdit( _activeDocument.BaseEdit, Settings.Edit );
}
public void ApplyFilterPreset( FilterPreset preset )
{
if ( _activeDocument is null || preset is null )
return;
PushEditUndo();
_activeDocument.SelectedFilterPresetId = preset.Id;
Settings.Save();
NotifyChanged();
}
public void CreateFilterPresetFromCurrent( string name )
{
if ( string.IsNullOrWhiteSpace( name ) )
return;
Settings.EnsureBuiltinFilterPresets();
Settings.FilterPresets.Add( new FilterPreset
{
Name = name.Trim(),
PhotoEditSettings = (_activeDocument?.BaseEdit ?? Settings.Edit).Clone(),
PostFx = new PostFxSettings()
} );
Settings.Save();
NotifyChanged();
}
public void DuplicateFilterPreset( FilterPreset preset )
{
if ( preset is null )
return;
var copy = preset.Clone();
copy.Id = Guid.NewGuid().ToString( "N" );
copy.Name = $"{preset.Name} Copy";
copy.BuiltIn = false;
Settings.FilterPresets.Add( copy );
Settings.Save();
NotifyChanged();
}
public void RenameFilterPreset( FilterPreset preset, string name )
{
if ( preset is null || preset.BuiltIn || string.IsNullOrWhiteSpace( name ) )
return;
preset.Name = name.Trim();
Settings.Save();
NotifyChanged();
}
public void DeleteFilterPreset( FilterPreset preset )
{
if ( preset is null || preset.BuiltIn )
return;
Settings.FilterPresets.Remove( preset );
if ( _activeDocument?.SelectedFilterPresetId == preset.Id )
_activeDocument.SelectedFilterPresetId = "";
if ( Settings.Capture.DefaultFilterPresetId == preset.Id )
Settings.Capture.DefaultFilterPresetId = "";
Settings.Save();
NotifyChanged();
}
public void SetDefaultFilterPreset( FilterPreset preset )
{
if ( preset is not null )
preset.UseForCapture = true;
Settings.Capture.DefaultFilterPresetId = preset?.Id ?? "";
Settings.Save();
NotifyChanged();
}
public void CopyFilterGameCameraCode( FilterPreset preset )
{
EditorUtility.Clipboard.Copy( SuperShotFilterExport.BuildCameraSnippet( preset ) );
Log.Info( $"[Supershot] Copied game camera code for filter '{preset?.Name ?? "None"}'." );
}
public void ResetBuiltinFilterPresets()
{
Settings.FilterPresets.RemoveAll( p => p?.BuiltIn == true );
Settings.EnsureBuiltinFilterPresets();
Settings.Save();
NotifyChanged();
}
public void AddTextLayer()
{
AddLayer( new SuperShotLayer { Name = "Text", Kind = SuperShotLayerKind.Text } );
}
public void AddAdjustmentLayer()
{
AddLayer( new SuperShotLayer { Name = "Adjustment", Kind = SuperShotLayerKind.Adjustment } );
}
public void AddImageLayer( string path )
{
if ( string.IsNullOrWhiteSpace( path ) )
return;
path = path.Trim( '"', ' ' );
var size = new Vector2( 0.4f, 0.4f );
try
{
if ( File.Exists( path ) )
{
using var probe = Bitmap.CreateFromBytes( File.ReadAllBytes( path ) );
if ( probe is not null && probe.IsValid && probe.Width > 0 && probe.Height > 0 )
{
var docSize = _activeDocument?.Size ?? default;
float canvasAspect = docSize.x > 0 && docSize.y > 0
? docSize.x / (float)docSize.y
: 1f;
float imageAspect = probe.Width / (float)probe.Height;
float h = 0.4f;
float w = MathX.Clamp( h * imageAspect / canvasAspect, 0.05f, 1f );
size = new Vector2( w, h );
}
}
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Couldn't read image layer dimensions '{path}': {e.Message}" );
}
AddLayer( new SuperShotLayer
{
Name = Path.GetFileNameWithoutExtension( path ),
Kind = SuperShotLayerKind.Image,
Size = size,
Image = new ImageLayerSettings { Path = path }
} );
}
void AddLayer( SuperShotLayer layer )
{
if ( _activeDocument is null || layer is null )
return;
PushEditUndo();
_activeDocument.Layers ??= new List<SuperShotLayer>();
_activeDocument.Layers.Add( layer );
_activeDocument.SelectedLayerId = layer.Id;
NotifyChanged();
}
public void SelectLayer( SuperShotLayer layer )
{
if ( _activeDocument is null || layer is null )
return;
_activeDocument.SelectedLayerId = layer.Id;
NotifyChanged();
}
public void SetSelectedLayerSilent( SuperShotLayer layer )
{
if ( _activeDocument is null )
return;
_activeDocument.SelectedLayerId = layer?.Id ?? "";
}
public void ToggleLayerVisibility( SuperShotLayer layer )
{
if ( layer is null )
return;
PushEditUndo();
layer.Visible = !layer.Visible;
NotifyChanged();
}
public void AlignSelectedLayer( int? horizontal, int? vertical )
{
var layer = SelectedLayer;
if ( layer is null || !layer.HasVisualBox )
return;
PushEditUndo();
var pos = layer.Position;
var s = layer.Size;
if ( horizontal == -1 ) pos.x = s.x * 0.5f;
else if ( horizontal == 0 ) pos.x = 0.5f;
else if ( horizontal == 1 ) pos.x = 1f - s.x * 0.5f;
if ( vertical == -1 ) pos.y = s.y * 0.5f;
else if ( vertical == 0 ) pos.y = 0.5f;
else if ( vertical == 1 ) pos.y = 1f - s.y * 0.5f;
layer.Position = pos;
NotifyChanged();
}
public void FillCanvasWithSelectedLayer()
{
var layer = SelectedLayer;
if ( layer is null || !layer.HasVisualBox )
return;
PushEditUndo();
layer.Position = new Vector2( 0.5f, 0.5f );
layer.Size = new Vector2( 1f, 1f );
NotifyChanged();
}
public void DuplicateSelectedLayer()
{
var layer = SelectedLayer;
if ( layer is null )
return;
PushEditUndo();
var copy = layer.Clone( true );
_activeDocument.Layers.Add( copy );
_activeDocument.SelectedLayerId = copy.Id;
NotifyChanged();
}
public void DeleteSelectedLayer()
{
var layer = SelectedLayer;
if ( layer is null )
return;
PushEditUndo();
_activeDocument.Layers.Remove( layer );
_activeDocument.SelectedLayerId = _activeDocument.Layers.Count > 0 ? _activeDocument.Layers[^1].Id : "";
NotifyChanged();
}
public void MoveSelectedLayer( int direction )
{
var layer = SelectedLayer;
if ( layer is null )
return;
var index = _activeDocument.Layers.IndexOf( layer );
var next = Math.Clamp( index + direction, 0, _activeDocument.Layers.Count - 1 );
if ( next == index )
return;
PushEditUndo();
_activeDocument.Layers.RemoveAt( index );
_activeDocument.Layers.Insert( next, layer );
NotifyChanged();
}
public string SaveCurrentAsNew()
{
using var finished = BuildFinished();
if ( finished is null )
{
Log.Warning( "[Supershot] Nothing captured to save." );
return null;
}
var path = SuperShotService.Save( finished, Settings.Output, Settings.Capture );
if ( path is null )
return null;
if ( _activeDocument is not null )
{
var previousEditingShot = _editingShot;
_editingShot = null;
SetActiveDocument( _activeDocument.Clone() );
_editingShot = previousEditingShot;
}
if ( _activeDocument is not null )
{
_activeDocument.SourcePath = SuperShotService.SaveSourceImage( RawCapture, path ) ?? _activeDocument.SourcePath;
_activeDocument.ExportPath = path;
SuperShotService.SaveDocumentSidecar( _activeDocument, path );
}
var shot = new CapturedShot
{
Raw = RawCapture?.Clone(),
Edit = _activeDocument?.BaseEdit?.Clone() ?? Settings.Edit.Clone(),
Document = _activeDocument,
Size = finished.Size,
Thumbnail = MakeThumbnail( finished ),
SavedPath = path,
Time = DateTime.Now
};
Gallery.Add( shot );
_editingShot = shot;
_editBaseline = _activeDocument?.Clone();
NotifyChanged();
return path;
}
public string SaveCurrent()
{
using var finished = BuildFinished();
if ( finished is null )
{
Log.Warning( "[Supershot] Nothing captured to save." );
return null;
}
if ( _editingShot is not null && !string.IsNullOrEmpty( _editingShot.SavedPath ) )
{
var saved = SuperShotService.SaveOver( finished, _editingShot.SavedPath, Settings.Output.Quality );
if ( saved is not null )
{
if ( _activeDocument is not null )
{
_activeDocument.SourcePath = SuperShotService.SaveSourceImage( RawCapture, saved ) ?? _activeDocument.SourcePath;
_activeDocument.ExportPath = saved;
SuperShotService.SaveDocumentSidecar( _activeDocument, saved );
}
_editingShot.Edit = _activeDocument?.BaseEdit?.Clone() ?? Settings.Edit.Clone();
_editingShot.Document = _activeDocument;
_editingShot.Size = finished.Size;
_editingShot.Thumbnail = MakeThumbnail( finished );
_editBaseline = _activeDocument?.Clone();
}
NotifyChanged();
return saved;
}
var path = SuperShotService.Save( finished, Settings.Output, Settings.Capture );
if ( path is not null && Gallery.Count > 0 )
{
if ( _activeDocument is not null )
{
_activeDocument.SourcePath = SuperShotService.SaveSourceImage( RawCapture, path ) ?? _activeDocument.SourcePath;
_activeDocument.ExportPath = path;
SuperShotService.SaveDocumentSidecar( _activeDocument, path );
}
Gallery[^1].SavedPath = path;
Gallery[^1].Document = _activeDocument;
Gallery[^1].Edit = _activeDocument?.BaseEdit?.Clone() ?? Settings.Edit.Clone();
_editBaseline = _activeDocument?.Clone();
}
NotifyChanged();
return path;
}
public void UploadToArtPage()
{
var path = SaveCurrent();
SuperShotService.UploadToArtPage( path );
}
public async Task PostCurrentToAll()
{
var targets = SuperShotService.EnabledWebhooks( Settings.Share );
if ( targets.Count == 0 )
{
Log.Warning( "[Supershot] No enabled Discord channels configured (Discord tab)." );
return;
}
if ( !TryEncodeCurrent( out var bytes, out var name ) )
return;
SaveCurrent();
int sent = await DiscordWebhook.PostImageToMany( targets, bytes, name, Settings.Share.Message, SuperShotService.MimeType( Settings.Output.Format ) );
Log.Info( $"[Supershot] Posted to {sent}/{targets.Count} Discord channel(s)." );
}
public async Task<bool> PostCurrentToWebhook( DiscordWebhookConfig webhook )
{
if ( webhook is null || !webhook.IsConfigured )
{
Log.Warning( "[Supershot] That channel has no webhook URL set." );
return false;
}
if ( !TryEncodeCurrent( out var bytes, out var name ) )
return false;
SaveCurrent();
return await DiscordWebhook.PostImage( webhook.Url, bytes, name, Settings.Share.Message, SuperShotService.MimeType( Settings.Output.Format ) );
}
bool TryEncodeCurrent( out byte[] bytes, out string filename )
{
bytes = null;
filename = null;
using var finished = BuildFinished();
if ( finished is null )
{
Log.Warning( "[Supershot] Nothing captured to post." );
return false;
}
bytes = SuperShotService.Encode( finished, Settings.Output.Format, Settings.Output.Quality );
filename = $"supershot.{SuperShotService.Extension( Settings.Output.Format )}";
return true;
}
void AddToGallery( Bitmap raw, SuperShotDocument document )
{
var shot = new CapturedShot
{
Raw = raw.Clone(),
Edit = document?.BaseEdit?.Clone() ?? Settings.Edit.Clone(),
Document = document,
Size = raw.Size,
Thumbnail = MakeThumbnail( raw )
};
Gallery.Add( shot );
if ( Gallery.Count > 50 )
{
Gallery[0].Raw?.Dispose();
Gallery.RemoveAt( 0 );
}
}
static Pixmap MakeThumbnail( Bitmap source )
{
try
{
int w = 256;
int h = Math.Max( 1, (int)(w * (source.Height / (float)source.Width)) );
using var small = source.Resize( w, h );
var pm = new Pixmap( w, h );
pm.UpdateFromPixels( small );
return pm;
}
catch
{
return null;
}
}
public void RefreshGalleryFromDisk()
{
LoadSavedShotsIntoGallery();
NotifyChanged();
}
void LoadSavedShotsIntoGallery()
{
try
{
var folder = Settings.Output.ResolveFolder();
if ( string.IsNullOrEmpty( folder ) || !Directory.Exists( folder ) )
return;
var matches = new List<string>();
foreach ( var file in Directory.GetFiles( folder ) )
{
var ext = Path.GetExtension( file ).ToLowerInvariant();
if ( ext is ".png" or ".jpg" or ".jpeg" or ".webp" && !SuperShotService.IsSupershotCompanionFile( file ) )
matches.Add( file );
}
matches.Sort( ( a, b ) => File.GetLastWriteTime( a ).CompareTo( File.GetLastWriteTime( b ) ) );
const int max = 30;
int start = Math.Max( 0, matches.Count - max );
for ( int i = start; i < matches.Count; i++ )
{
var file = matches[i];
if ( GalleryContainsPath( file ) )
continue;
try
{
using var bmp = Bitmap.CreateFromBytes( File.ReadAllBytes( file ) );
if ( bmp is null || !bmp.IsValid )
continue;
var document = SuperShotService.LoadDocumentSidecar( file );
document ??= new SuperShotDocument
{
Name = Path.GetFileNameWithoutExtension( file ),
ExportPath = file,
SourcePath = file,
Size = bmp.Size
};
Gallery.Add( new CapturedShot
{
Raw = null,
Edit = document.BaseEdit?.Clone() ?? new EditSettings(),
Document = document,
Size = bmp.Size,
Thumbnail = MakeThumbnail( bmp ),
SavedPath = file,
Time = File.GetLastWriteTime( file )
} );
}
catch
{
}
}
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Couldn't scan the gallery folder: {e.Message}" );
}
}
bool GalleryContainsPath( string path )
{
foreach ( var shot in Gallery )
{
if ( !string.IsNullOrEmpty( shot.SavedPath ) && string.Equals( shot.SavedPath, path, StringComparison.OrdinalIgnoreCase ) )
return true;
}
return false;
}
public void LoadFromGallery( CapturedShot shot )
{
if ( shot is null )
return;
Bitmap raw = null;
var document = shot.Document;
if ( document is null && !string.IsNullOrEmpty( shot.SavedPath ) )
document = SuperShotService.LoadDocumentSidecar( shot.SavedPath );
var sourcePath = document?.SourcePath;
if ( !string.IsNullOrEmpty( sourcePath ) && File.Exists( sourcePath ) )
{
try { raw = Bitmap.CreateFromBytes( File.ReadAllBytes( sourcePath ) ); }
catch ( Exception e ) { Log.Warning( $"[Supershot] Couldn't open editable source '{sourcePath}': {e.Message}" ); }
}
if ( shot.Raw is not null && shot.Raw.IsValid )
{
raw ??= shot.Raw.Clone();
}
else if ( !string.IsNullOrEmpty( shot.SavedPath ) && File.Exists( shot.SavedPath ) )
{
if ( raw is null )
{
try { raw = Bitmap.CreateFromBytes( File.ReadAllBytes( shot.SavedPath ) ); }
catch ( Exception e ) { Log.Warning( $"[Supershot] Couldn't open '{shot.SavedPath}': {e.Message}" ); }
}
if ( raw is not null && raw.IsValid && shot.Raw is null )
shot.Raw = raw.Clone();
}
if ( raw is null || !raw.IsValid )
return;
RawCapture?.Dispose();
RawCapture = raw;
_editingShot = shot;
if ( document is null )
{
document = new SuperShotDocument
{
Name = Path.GetFileNameWithoutExtension( shot.SavedPath ) ?? "Flat Image",
ExportPath = shot.SavedPath,
SourcePath = shot.SavedPath,
Size = raw.Size,
BaseEdit = shot.Edit?.Clone() ?? new EditSettings()
};
}
document.Size = raw.Size;
shot.Document = document;
SetActiveDocument( document );
_editBaseline = _activeDocument.Clone();
Settings.Save();
NotifyChanged();
}
public void OpenInEditor( CapturedShot shot )
{
LoadFromGallery( shot );
if ( !HasCapture )
return;
_reviewingGalleryShot = true;
_editBaseline = _activeDocument?.Clone();
DockManager.RaiseDock( "Edit" );
DockManager.Update();
}
public void ReturnToLivePreview()
{
_reviewingGalleryShot = false;
_editingShot = null;
_activeDocument = null;
RawCapture?.Dispose();
RawCapture = null;
NotifyChanged();
}
public void LeaveEditReview()
{
if ( !_reviewingGalleryShot )
return;
_reviewingGalleryShot = false;
if ( HasCapture && HasUnsavedEditChanges() )
{
Dialog.AskConfirm(
() => { SaveCurrent(); ReturnToLivePreview(); },
() => ReturnToLivePreview(),
"Save your edits to this shot before returning to the live preview?",
"Unsaved changes", "Save", "Discard" );
}
else
{
ReturnToLivePreview();
}
}
public bool HasUnsavedEditChanges()
{
return _editBaseline is not null && !DocumentEquals( _editBaseline, _activeDocument );
}
static bool DocumentEquals( SuperShotDocument a, SuperShotDocument b )
{
if ( a is null || b is null )
return a == b;
return Json.Serialize( a ) == Json.Serialize( b );
}
static bool EditEquals( EditSettings a, EditSettings b )
{
return a.Brightness == b.Brightness && a.Contrast == b.Contrast && a.Saturation == b.Saturation
&& a.Exposure == b.Exposure && a.Hue == b.Hue && a.Filter == b.Filter
&& a.Sharpen == b.Sharpen && a.Blur == b.Blur && a.Vignette == b.Vignette && a.Grain == b.Grain
&& a.Rotate == b.Rotate && a.FlipH == b.FlipH && a.FlipV == b.FlipV
&& a.Border == b.Border && a.BorderColor == b.BorderColor
&& a.Watermark == b.Watermark && a.WatermarkText == b.WatermarkText
&& a.WatermarkAnchor == b.WatermarkAnchor && a.WatermarkSize == b.WatermarkSize && a.WatermarkColor == b.WatermarkColor;
}
public void RemoveFromGallery( CapturedShot shot )
{
if ( shot is null ) return;
shot.Raw?.Dispose();
if ( !string.IsNullOrEmpty( shot.SavedPath ) )
{
try { if ( File.Exists( shot.SavedPath ) ) File.Delete( shot.SavedPath ); }
catch ( Exception e ) { Log.Warning( $"[Supershot] Couldn't delete '{shot.SavedPath}': {e.Message}" ); }
}
Gallery.Remove( shot );
NotifyChanged();
}
public void RestoreStudioLayout()
{
BuildDefaultLayout();
}
protected override void BuildDefaultLayout()
{
DockManager.RegisterDock( new() { Title = "Home", Icon = "home", Area = DockArea.Center, CreateAction = () => new CapturePanel( this ) } );
DockManager.RegisterDock( new() { Title = "Preview", Icon = "image", Area = DockArea.Center, CreateAction = () => new PreviewPanel( this ) } );
DockManager.RegisterDock( new() { Title = "Edit", Icon = "tune", Area = DockArea.Center, CreateAction = () => new EditPanel( this ) } );
DockManager.RegisterDock( new() { Title = "Gallery", Icon = "collections", Area = DockArea.Center, CreateAction = () => new GalleryPanel( this ) } );
DockManager.RegisterDock( new() { Title = "Discord", Icon = "forum", Area = DockArea.Center, CreateAction = () => new SharePanel( this ) } );
DockManager.RegisterDock( new() { Title = "Settings", Icon = "settings", Area = DockArea.Center, CreateAction = () => new SettingsPanel( this ) } );
var home = DockManager.OpenDock( "Home", DockArea.Center );
DockManager.OpenDock( "Edit", DockArea.Center, home );
DockManager.OpenDock( "Gallery", DockArea.Center, home );
DockManager.OpenDock( "Discord", DockArea.Center, home );
DockManager.OpenDock( "Settings", DockArea.Center, home );
DockManager.RaiseDock( "Home" );
RebuildMenuBar();
}
void RebuildMenuBar()
{
MenuBar.Clear();
var file = MenuBar.AddMenu( "File" );
file.AddOption( "Capture", "photo_camera", () => Capture() );
file.AddOption( "Capture All Package Thumbnails", "burst_mode", () => SuperShotService.CaptureAllPackageThumbnails() );
file.AddSeparator();
file.AddOption( "Save", "save", () => SaveCurrent() );
file.AddOption( "Copy Path", "content_copy", () => SuperShotService.CopyPathToClipboard( SaveCurrent() ) );
file.AddOption( "Open Output Folder", "folder", () => SuperShotService.RevealInExplorer( Settings.Output.ResolveFolder() ) );
file.AddSeparator();
file.AddOption( new Option( "Close" ) { Triggered = Close } );
var appMenu = MenuBar.AddMenu( "Home" );
appMenu.AddOption( "Open Home", "home", () => DockManager.RaiseDock( "Home" ) );
appMenu.AddOption( "Open Editor", "tune", () => DockManager.RaiseDock( "Edit" ) );
appMenu.AddOption( "Open Gallery", "collections", () => DockManager.RaiseDock( "Gallery" ) );
appMenu.AddOption( "Open Discord", "forum", () => DockManager.RaiseDock( "Discord" ) );
appMenu.AddSeparator();
appMenu.AddOption( "Undo", "undo", Undo );
appMenu.AddOption( "Redo", "redo", Redo );
appMenu.AddOption( "Reset Edits", "restart_alt", ResetEdit );
var discord = MenuBar.AddMenu( "Discord" );
discord.AboutToShow += () => OnDiscordMenu( discord );
var view = MenuBar.AddMenu( "View" );
view.AboutToShow += () => OnViewMenu( view );
var help = MenuBar.AddMenu( "Help" );
help.AddOption( "Upload to s&box Art Page", "image", UploadToArtPage );
help.AddOption( "About Supershot", "info", () => EditorUtility.DisplayDialog( "Supershot",
"Editor-only screenshot studio.\n\nCapture the scene-view freecam, edit, then save or share to Discord. Includes presets for Steam and s&box package thumbnails (Square 512x512, Wide 910x512, Tall 512x910)." ) );
}
void OnDiscordMenu( Menu menu )
{
menu.Clear();
var webhooks = Settings.Share.Webhooks;
var configured = webhooks.FindAll( w => w is not null && w.IsConfigured );
if ( configured.Count == 0 )
{
var none = menu.AddOption( "No channels configured", "link_off" );
none.Enabled = false;
menu.AddSeparator();
menu.AddOption( "Open Discord Tab", "forum", () => DockManager.RaiseDock( "Discord" ) );
return;
}
menu.AddOption( "Post current shot to all Discord Channels", "share", () => _ = PostCurrentToAll() );
menu.AddOption( "Capture + Post to all Discord Channels", "burst_mode", () => _ = SuperShotService.CaptureAndPostAll() );
var favorites = configured.FindAll( w => w.Favorite );
if ( favorites.Count > 0 )
{
menu.AddSeparator();
var heading = menu.AddOption( "Favorites", "star" );
heading.Enabled = false;
foreach ( var wh in favorites )
{
var target = wh;
menu.AddOption( $"Post to {wh.Name}", "send", () => _ = PostCurrentToWebhook( target ) );
}
}
menu.AddSeparator();
foreach ( var wh in configured )
{
var sub = menu.AddMenu( wh.Name );
var target = wh;
sub.AddOption( "Post Current", "send", () => _ = PostCurrentToWebhook( target ) );
sub.AddOption( "Capture + Post", "photo_camera", () => _ = SuperShotService.CaptureAndPostTo( target ) );
sub.AddSeparator();
var favOpt = sub.AddOption( "Favorite" );
favOpt.Checkable = true;
favOpt.Checked = target.Favorite;
favOpt.Toggled += ( b ) => { target.Favorite = b; Settings.Save(); };
}
}
void OnViewMenu( Menu view )
{
view.Clear();
view.AddOption( "Restore To Default", "settings_backup_restore", BuildDefaultLayout );
view.AddSeparator();
var live = view.AddOption( "Live Preview" );
live.Checkable = true;
live.Checked = Settings.LivePreview;
live.Toggled += ( b ) =>
{
Settings.LivePreview = b;
Settings.Save();
NotifyChanged();
};
view.AddSeparator();
foreach ( var dock in DockManager.DockTypes )
{
var o = view.AddOption( dock.Title, dock.Icon );
o.Checkable = true;
o.Checked = DockManager.IsDockOpen( dock.Title );
o.Toggled += ( b ) => DockManager.SetDockState( dock.Title, b );
}
}
public override void OnDestroyed()
{
base.OnDestroyed();
if ( _duplicate )
return;
if ( _instance == this )
_instance = null;
Settings.Save();
RawCapture?.Dispose();
foreach ( var s in Gallery )
s.Raw?.Dispose();
}
}