Skip to content

Commit a7e9e8b

Browse files
Single Instance (#102)
1 parent 6739521 commit a7e9e8b

File tree

5 files changed

+118
-4
lines changed

5 files changed

+118
-4
lines changed

Pixed/App.axaml.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
using Avalonia.Controls.ApplicationLifetimes;
44
using Avalonia.Data.Core.Plugins;
55
using Avalonia.Markup.Xaml;
6+
using Avalonia.Threading;
67
using Microsoft.Extensions.DependencyInjection;
8+
using Newtonsoft.Json;
79
using Pixed.DependencyInjection;
10+
using Pixed.Utils;
811
using Pixed.Windows;
12+
using System;
13+
using System.IO;
14+
using System.Threading;
915
using System.Threading.Tasks;
1016

1117
namespace Pixed;
1218

1319
public partial class App : Application
1420
{
21+
private Mutex _mutex;
1522
internal static IPixedServiceProvider ServiceProvider { get; private set; }
1623
public override void Initialize()
1724
{
@@ -32,6 +39,11 @@ public async override void OnFrameworkInitializationCompleted()
3239

3340
private async Task InitializeMainWindow(IClassicDesktopStyleApplicationLifetime desktop, Window splash)
3441
{
42+
if (!HandleNewInstance(Dispatcher.UIThread, desktop))
43+
{
44+
splash.Close();
45+
return;
46+
}
3547
await Task.Delay(1500);
3648
BindingPlugins.DataValidators.RemoveAt(0);
3749

@@ -47,4 +59,54 @@ private async Task InitializeMainWindow(IClassicDesktopStyleApplicationLifetime
4759
await Task.Delay(100);
4860
splash.Close();
4961
}
62+
63+
private bool HandleNewInstance(Dispatcher? dispatcher, IClassicDesktopStyleApplicationLifetime desktop)
64+
{
65+
_mutex = new(true, "pixed_mutex_name", out bool isOwned);
66+
var handle = new EventWaitHandle(false, EventResetMode.AutoReset, "pixed_mutex_event_name");
67+
68+
GC.KeepAlive(_mutex);
69+
70+
if (dispatcher == null)
71+
return true;
72+
73+
if (isOwned)
74+
{
75+
var thread = new Thread(
76+
async () =>
77+
{
78+
while (handle.WaitOne())
79+
{
80+
var pixed = await desktop.MainWindow.StorageProvider.GetPixedFolder();
81+
var filePath = Path.Combine(pixed.Path.AbsolutePath, "instance.lock");
82+
83+
if (File.Exists(filePath))
84+
{
85+
string[] args = JsonConvert.DeserializeObject<string[]>(File.ReadAllText(filePath)) ?? [];
86+
87+
if (args.Length > 0)
88+
{
89+
Subjects.NewInstanceHandled.OnNext(args);
90+
}
91+
}
92+
}
93+
})
94+
{
95+
IsBackground = true
96+
};
97+
98+
thread.Start();
99+
return true;
100+
}
101+
102+
Task.Run(async () =>
103+
{
104+
var pixed = await desktop.MainWindow.StorageProvider.GetPixedFolder();
105+
File.WriteAllText(Path.Combine(pixed.Path.AbsolutePath, "instance.lock"), JsonConvert.SerializeObject(Environment.GetCommandLineArgs()));
106+
});
107+
handle.Set();
108+
109+
desktop.Shutdown();
110+
return false;
111+
}
50112
}

Pixed/IO/PixedProjectMethods.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Pixed.Utils;
44
using Pixed.Windows;
55
using System.IO;
6+
using System.Linq;
67
using System.Threading.Tasks;
78

89
namespace Pixed.IO;
@@ -103,7 +104,14 @@ public async Task Open(RecentFilesService recentFilesService)
103104

104105
public void Open(string path)
105106
{
107+
string[] formats = [".pixed", ".piskel", ".png"];
106108
FileInfo info = new(path);
109+
110+
if (!formats.Contains(info.Extension))
111+
{
112+
return;
113+
}
114+
107115
IPixedProjectSerializer serializer;
108116
Stream stream = File.OpenRead(path);
109117

Pixed/Subjects.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal static class Subjects
1414
public static Subject<bool> GridChanged { get; } = new Subject<bool>();
1515
public static Subject<BaseToolPair> ToolChanged { get; } = new Subject<BaseToolPair>();
1616
public static Subject<double> ZoomChanged { get; } = new Subject<double>();
17+
public static Subject<string[]> NewInstanceHandled { get; } = new Subject<string[]>();
1718

1819
public static Subject<BaseSelection> ClipboardCopy { get; } = new Subject<BaseSelection>();
1920
public static Subject<BaseSelection> ClipboardCut { get; } = new Subject<BaseSelection>();

Pixed/Utils/StorageProviderUtils.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Avalonia.Platform.Storage;
2+
using System.Threading.Tasks;
3+
4+
namespace Pixed.Utils;
5+
internal static class StorageProviderUtils
6+
{
7+
public async static Task<IStorageFolder?> GetPixedFolder(this IStorageProvider provider)
8+
{
9+
var documentsFolder = await provider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
10+
return await documentsFolder.CreateFolderAsync("Pixed");
11+
}
12+
}

Pixed/Windows/MainWindow.axaml.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Avalonia.Controls;
22
using Avalonia.Input;
3+
using Avalonia.Threading;
34
using Pixed.Controls;
45
using Pixed.Input;
56
using Pixed.IO;
@@ -8,14 +9,16 @@
89
using Pixed.Services;
910
using Pixed.Services.Keyboard;
1011
using Pixed.Tools;
12+
using Pixed.Utils;
1113
using Pixed.ViewModels;
14+
using System;
1215
using System.IO;
1316
using System.Threading.Tasks;
1417
using System.Windows.Input;
1518

1619
namespace Pixed.Windows;
1720

18-
internal partial class MainWindow : PixedWindow<MainViewModel>
21+
internal partial class MainWindow : PixedWindow<MainViewModel>, IDisposable
1922
{
2023
private readonly ApplicationData _applicationData;
2124
private readonly PixedProjectMethods _pixedProjectMethods;
@@ -25,6 +28,9 @@ internal partial class MainWindow : PixedWindow<MainViewModel>
2528
private readonly RecentFilesService _recentFilesService;
2629
private readonly ToolSelector _toolSelector;
2730
private readonly MenuBuilder _menuBuilder;
31+
private IDisposable _newInstanceHandled;
32+
private bool _disposedValue;
33+
2834
public static Window? Handle { get; private set; }
2935
public static ICommand? QuitCommand { get; private set; }
3036
public MainWindow(ApplicationData applicationData, PixedProjectMethods pixedProjectMethods, MenuBuilder builder, MenuItemRegistry menuItemRegistry,
@@ -39,6 +45,13 @@ public MainWindow(ApplicationData applicationData, PixedProjectMethods pixedProj
3945
_recentFilesService = recentFilesService;
4046
_toolSelector = toolSelector;
4147
_menuBuilder = builder;
48+
_newInstanceHandled = Subjects.NewInstanceHandled.Subscribe(args =>
49+
{
50+
foreach (var arg in args)
51+
{
52+
Dispatcher.UIThread.Invoke(() => _pixedProjectMethods.Open(arg));
53+
}
54+
});
4255

4356
InitializeWindow();
4457
}
@@ -72,6 +85,25 @@ protected override void OnInitialized()
7285
_menuBuilder.Build();
7386
}
7487

88+
protected virtual void Dispose(bool disposing)
89+
{
90+
if (!_disposedValue)
91+
{
92+
if (disposing)
93+
{
94+
_newInstanceHandled?.Dispose();
95+
}
96+
97+
_disposedValue = true;
98+
}
99+
}
100+
101+
public void Dispose()
102+
{
103+
Dispose(disposing: true);
104+
GC.SuppressFinalize(this);
105+
}
106+
75107
private void InitializeBeforeUI()
76108
{
77109
Handle = this;
@@ -145,9 +177,8 @@ private void Window_PointerReleased(object? sender, PointerReleasedEventArgs e)
145177

146178
private async Task InitializeDataFolder()
147179
{
148-
var documentsFolder = await StorageProvider.TryGetWellKnownFolderAsync(Avalonia.Platform.Storage.WellKnownFolder.Documents);
149-
var pixedFolder = await documentsFolder.CreateFolderAsync("Pixed");
150-
await documentsFolder.CreateFolderAsync("Pixed/Palettes");
180+
var pixedFolder = await StorageProvider.GetPixedFolder();
181+
await pixedFolder.CreateFolderAsync("Palettes");
151182
_applicationData.Initialize(pixedFolder);
152183
}
153184

0 commit comments

Comments
 (0)