Skip to content

Commit 60219c7

Browse files
authored
Merge pull request OpenKH#1234 from Kite2810/master
Added various features to Openkh.Tools.LayoutEditor.
2 parents 3d3d6f2 + c9fae08 commit 60219c7

File tree

9 files changed

+2027
-71
lines changed

9 files changed

+2027
-71
lines changed

OpenKh.Tools.LayoutEditor/AnimatedGifEncoder.cs

Lines changed: 1033 additions & 0 deletions
Large diffs are not rendered by default.

OpenKh.Tools.LayoutEditor/App.cs

Lines changed: 182 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,27 @@ public string TextureName
116116
}
117117

118118
public bool CheckerboardBackground { get; set; }
119+
120+
public bool UseBlankBackground
121+
{
122+
get => Settings.Default.UseBlankBackground;
123+
set
124+
{
125+
Settings.Default.UseBlankBackground = value;
126+
Settings.Default.Save();
127+
}
128+
}
129+
119130
public ColorF EditorBackground
120131
{
121-
get => new ColorF(Settings.Default.BgColorR, Settings.Default.BgColorG,
122-
Settings.Default.BgColorB, 1f);
132+
get
133+
{
134+
if (UseBlankBackground)
135+
return new ColorF(0f, 0f, 0f, 0f); // Fully transparent
136+
137+
return new ColorF(Settings.Default.BgColorR, Settings.Default.BgColorG,
138+
Settings.Default.BgColorB, 1f);
139+
}
123140
set
124141
{
125142
Settings.Default.BgColorR = value.R;
@@ -291,10 +308,25 @@ void MainMenu()
291308
ForMenuItem($"{LinkToPcsx2ActionName}...", "CTRL+L", MenuFileOpenPcsx2);
292309
ForMenuItem("Save", "CTRL+S", MenuFileSave, CurrentEditor != null);
293310
ForMenuItem("Save as...", MenuFileSaveAs, CurrentEditor != null);
311+
ImGui.Separator();
312+
ForMenuItem("Export Animation/Layout...", MenuFileExportAnimation, CurrentEditor != null);
313+
ForMenuItem("Export Texture(s)...", MenuFileExportTexture, CurrentEditor != null);
314+
ForMenuItem("Export Layout as Animated GIF...", MenuFileExportLayoutAsGif, CurrentEditor is AppLayoutEditor);
315+
ForMenuItem("Export Layout as PNG Sequence...", MenuFileExportLayoutAsPngSequence, CurrentEditor is AppLayoutEditor);
316+
ImGui.Separator();
294317
ForMenuItem("Preview In-Game...", MenuFilePreview, CurrentEditor != null);
295318
ImGui.Separator();
296319
ForMenu("Preferences", () =>
297320
{
321+
// Add the blank background toggle FIRST
322+
ForMenuCheck("Use blank background", () => UseBlankBackground, x =>
323+
{
324+
UseBlankBackground = x;
325+
OnChangeBackground?.Invoke(this, this);
326+
});
327+
328+
// Then the color picker (disabled when blank is on)
329+
ImGui.BeginDisabled(UseBlankBackground);
298330
var editorBackground = new Vector3(EditorBackground.R,
299331
EditorBackground.G, EditorBackground.B);
300332
if (ImGui.ColorEdit3("Background color", ref editorBackground))
@@ -303,6 +335,7 @@ void MainMenu()
303335
editorBackground.Y, editorBackground.Z, 1f);
304336
OnChangeBackground?.Invoke(this, this);
305337
}
338+
ImGui.EndDisabled();
306339

307340
ForMenuCheck("Show PS2 viewport", () => ShowViewportOriginal, x => ShowViewportOriginal = x);
308341
ForMenuCheck("Show ReMIX viewport", () => ShowViewportRemix, x => ShowViewportRemix = x);
@@ -330,7 +363,7 @@ private void MenuFileOpen()
330363

331364
private void MenuFilePreview()
332365
{
333-
OpenPreviewDialog();
366+
OpenPreviewDialog();
334367
}
335368

336369
private void MenuFileOpenWithoutPcsx2()
@@ -371,6 +404,70 @@ private void MenuFileSaveAs()
371404
}, Filters);
372405
}
373406

407+
private void MenuFileExportAnimation()
408+
{
409+
if (CurrentEditor == null)
410+
return;
411+
412+
var animationEntry = CurrentEditor.SaveAnimation(AnimationName ?? "export");
413+
414+
// Determine file extension based on Type (EntryType)
415+
string extension = animationEntry.Type == Bar.EntryType.Layout ? "lad" : "sed";
416+
string filterName = animationEntry.Type == Bar.EntryType.Layout ? "Layout file" : "Sequence file";
417+
418+
var exportFilter = FileDialogFilterComposer
419+
.Compose()
420+
.AddExtensions(filterName, extension)
421+
.AddAllFiles();
422+
423+
FileDialog.OnSave(fileName =>
424+
{
425+
try
426+
{
427+
using var stream = File.Create(fileName);
428+
animationEntry.Stream.SetPosition(0).CopyTo(stream);
429+
MessageBox.Show($"Animation/Layout exported successfully to:\n{fileName}",
430+
"Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
431+
}
432+
catch (Exception ex)
433+
{
434+
ShowError($"Failed to export animation/layout:\n{ex.Message}");
435+
}
436+
}, exportFilter);
437+
}
438+
439+
private void MenuFileExportTexture()
440+
{
441+
if (CurrentEditor == null)
442+
return;
443+
444+
var textureEntry = CurrentEditor.SaveTexture(TextureName ?? "export");
445+
446+
// Determine file extension based on Type (EntryType)
447+
string extension = textureEntry.Type == Bar.EntryType.Imgz ? "imz" : "imd";
448+
string filterName = textureEntry.Type == Bar.EntryType.Imgz ? "Image container IMGZ" : "Image IMGD";
449+
450+
var exportFilter = FileDialogFilterComposer
451+
.Compose()
452+
.AddExtensions(filterName, extension)
453+
.AddAllFiles();
454+
455+
FileDialog.OnSave(fileName =>
456+
{
457+
try
458+
{
459+
using var stream = File.Create(fileName);
460+
textureEntry.Stream.SetPosition(0).CopyTo(stream);
461+
MessageBox.Show($"Texture(s) exported successfully to:\n{fileName}",
462+
"Export Successful", MessageBoxButton.OK, MessageBoxImage.Information);
463+
}
464+
catch (Exception ex)
465+
{
466+
ShowError($"Failed to export texture(s):\n{ex.Message}");
467+
}
468+
}, exportFilter);
469+
}
470+
374471
private void MenuFileExit() => _exitFlag = true;
375472

376473
public void OpenToolDesc(ToolInvokeDesc toolInvokeDesc)
@@ -601,38 +698,22 @@ private void AddKeyMapping(Keys key, Action action)
601698
private void ProcessKeyMapping()
602699
{
603700
var k = Keyboard.GetState();
604-
if (k.IsKeyDown(Keys.LeftControl))
701+
702+
// Check if Ctrl is pressed
703+
if (k.IsKeyDown(Keys.LeftControl) || k.IsKeyDown(Keys.RightControl))
605704
{
606705
var keys = k.GetPressedKeys();
607706
foreach (var key in keys)
608707
{
609708
if (_keyMapping.TryGetValue(key, out var action))
709+
{
710+
// Execute the action regardless of modal state
610711
action();
712+
}
611713
}
612714
}
613715
}
614716

615-
//private void OpenLayout(LayoutEntryModel layoutEntryModel)
616-
//{
617-
// AnimationName = layoutEntryModel.Layout.Name;
618-
// SpriteName = layoutEntryModel.Images.Name;
619-
620-
// var texturesViewModel = new TexturesViewModel(layoutEntryModel.Images.Value);
621-
622-
// var layoutEditorViewModel = new LayoutEditorViewModel(this, this, EditorDebugRenderingService)
623-
// {
624-
// SequenceGroups = new SequenceGroupsViewModel(layoutEntryModel.Layout.Value, texturesViewModel, EditorDebugRenderingService),
625-
// Layout = layoutEntryModel.Layout.Value,
626-
// Images = layoutEntryModel.Images.Value
627-
// };
628-
629-
// CurrentEditor = layoutEditorViewModel;
630-
// OnControlChanged?.Invoke(new LayoutEditorView()
631-
// {
632-
// DataContext = layoutEditorViewModel
633-
// });
634-
//}
635-
636717
private bool LinkLaydToPcs2(Stream stream) =>
637718
LinkToPcs2(stream, Layout.MagicCodeValidator, 0x1c);
638719

@@ -677,6 +758,82 @@ private void CloseProcessStream()
677758
_processStream = null;
678759
}
679760

761+
private void MenuFileExportLayoutAsPngSequence()
762+
{
763+
if (!(CurrentEditor is AppLayoutEditor layoutEditor))
764+
{
765+
ShowError("This feature is only available for Layout files (2LD).");
766+
return;
767+
}
768+
769+
// Let user select a folder
770+
using (var folderDialog = new System.Windows.Forms.FolderBrowserDialog())
771+
{
772+
folderDialog.Description = "Select folder to save PNG sequence";
773+
folderDialog.ShowNewFolderButton = true;
774+
775+
if (folderDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
776+
{
777+
try
778+
{
779+
int frameCount = layoutEditor.ExportCurrentLayoutAsPngSequence(folderDialog.SelectedPath);
780+
781+
MessageBox.Show(
782+
$"PNG sequence exported successfully!\n\n" +
783+
$"Location: {folderDialog.SelectedPath}\n" +
784+
$"Frames: {frameCount} files (frame_0000.png to frame_{frameCount - 1:D4}.png)\n\n",
785+
"Export Successful",
786+
MessageBoxButton.OK,
787+
MessageBoxImage.Information);
788+
}
789+
catch (Exception ex)
790+
{
791+
ShowError($"Failed to export PNG sequence:\n{ex.Message}");
792+
}
793+
}
794+
}
795+
}
796+
797+
private void MenuFileExportLayoutAsGif()
798+
{
799+
if (!(CurrentEditor is AppLayoutEditor layoutEditor))
800+
{
801+
ShowError("This feature is only available for Layout files (2LD).");
802+
return;
803+
}
804+
805+
var gifFilter = FileDialogFilterComposer
806+
.Compose()
807+
.AddExtensions("Animated GIF", "gif")
808+
.AddAllFiles();
809+
810+
FileDialog.OnSave(fileName =>
811+
{
812+
try
813+
{
814+
// Default settings optimized for quality and reasonable speed
815+
// frameDelay: 33ms = ~30 FPS, 50ms = 20 FPS (smoother, less flicker)
816+
// quality: 1 = best quality (slower), 10 = balanced, 20 = fastest (lower quality)
817+
int frameDelay = 33;
818+
int quality = 10;
819+
820+
layoutEditor.ExportCurrentLayoutAsGif(fileName, frameDelay, quality);
821+
822+
MessageBox.Show(
823+
$"Animated GIF exported successfully to:\n{fileName}\n\n" +
824+
$"Settings: {1000 / frameDelay} FPS, Quality: {quality}/20\n\n" +
825+
$"Note: GIF format has inherent flickering on fast color transitions due to 256-color palette limitation per frame.",
826+
"Export Successful",
827+
MessageBoxButton.OK,
828+
MessageBoxImage.Information);
829+
}
830+
catch (Exception ex)
831+
{
832+
ShowError($"Failed to export animated GIF:\n{ex.Message}");
833+
}
834+
}, gifFilter);
835+
}
836+
680837
private static bool DoesContainSequenceAnimations(IEnumerable<Bar.Entry> entries) =>
681838
entries.Any(x => x.Type == Bar.EntryType.Seqd);
682839

0 commit comments

Comments
 (0)