Skip to content

Commit 9a5656e

Browse files
committed
Improve NewWindow open mode (#4)
1 parent ff0088b commit 9a5656e

File tree

7 files changed

+183
-30
lines changed

7 files changed

+183
-30
lines changed

ObsidianShell.CLI/Program.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal class Program
1414
{
1515
static Settings _settings;
1616

17-
static void Main(string[] args)
17+
static async Task Main(string[] args)
1818
{
1919
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
2020
{
@@ -30,7 +30,7 @@ static void Main(string[] args)
3030
_settings = Settings.Load();
3131

3232
Obsidian obsidian = new Obsidian(_settings);
33-
obsidian.OpenFile(args[0]);
33+
await obsidian.OpenFile(args[0]);
3434
}
3535
}
3636
}

ObsidianShell.GUI/MainViewModel.cs

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public MainViewModel()
6666
ObsidianOpenMode.CurrentTab,
6767
ObsidianOpenMode.NewTab,
6868
ObsidianOpenMode.NewWindow,
69+
ObsidianOpenMode.VaultAndNewWindow,
6970
ObsidianOpenMode.NewPane,
7071
ObsidianOpenMode.HoverPopover
7172
};

ObsidianShell/NativeMethods.txt

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BringWindowToTop
2+
EnumThreadWindows
3+
GetAsyncKeyState
4+
GetClassName
5+
GetWindow
6+
IsIconic
7+
IsWindowVisible
8+
SendMessage
9+
SetWindowPos
10+
ShowWindow
11+
WM_GETTEXT

ObsidianShell/Obsidian.cs

+63-19
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
using System.IO;
66
using System.Linq;
77
using System.Runtime;
8+
using System.Runtime.InteropServices;
89
using System.Text;
10+
using System.Threading;
911
using System.Threading.Tasks;
1012
using System.Windows.Forms;
13+
using Windows.Win32;
14+
using Windows.Win32.Foundation;
1115

1216
namespace ObsidianShell
1317
{
@@ -20,15 +24,15 @@ public Obsidian(Settings settings)
2024
_settings = settings;
2125
}
2226

23-
public void OpenFile(string path)
27+
public async Task OpenFile(string path)
2428
{
2529
switch (_settings.OpenMode)
2630
{
2731
case OpenMode.VaultFallback:
2832
{
2933
if (IsFileInVault(path))
3034
{
31-
OpenFileInVault(path);
35+
await OpenFileInVault(path);
3236
}
3337
else
3438
{
@@ -40,17 +44,17 @@ public void OpenFile(string path)
4044
{
4145
if (IsFileInVault(path))
4246
{
43-
OpenFileInVault(path);
47+
await OpenFileInVault(path);
4448
}
4549
else
4650
{
47-
OpenFileInRecent(path);
51+
await OpenFileInRecent(path);
4852
}
4953
break;
5054
}
5155
case OpenMode.Recent:
5256
{
53-
OpenFileInRecent(path);
57+
await OpenFileInRecent(path);
5458
break;
5559
}
5660
}
@@ -63,7 +67,7 @@ private static string PercentEncode(string text)
6367
return Uri.EscapeDataString(text);
6468
}
6569

66-
private void OpenFileInVault(string path, string vaultPath = null)
70+
private async Task OpenFileInVault(string path, string vaultPath = null)
6771
{
6872
if (_settings.EnableAdvancedURI is false)
6973
{
@@ -73,7 +77,7 @@ private void OpenFileInVault(string path, string vaultPath = null)
7377
{
7478
vaultPath = vaultPath ?? GetFileVaultPath(path);
7579
string vault = GetVaultName(vaultPath);
76-
string filename = Utils.GetRelativePath(vaultPath, path);
80+
string filepath = Utils.GetRelativePath(vaultPath, path);
7781

7882
ObsidianOpenMode obsidianOpenMode = _settings.ObsidianDefaultOpenMode;
7983
if (Utils.IsKeyPressed(Keys.ControlKey))
@@ -85,20 +89,60 @@ private void OpenFileInVault(string path, string vaultPath = null)
8589
if (Utils.IsKeyPressed(Keys.LWin) || Utils.IsKeyPressed(Keys.RWin))
8690
obsidianOpenMode = _settings.ObsidianWinOpenMode;
8791

88-
string openmode = obsidianOpenMode switch
92+
if (obsidianOpenMode is ObsidianOpenMode.NewWindow)
8993
{
90-
ObsidianOpenMode.CurrentTab => "false",
91-
ObsidianOpenMode.NewTab => "tab",
92-
ObsidianOpenMode.NewWindow => "window",
93-
ObsidianOpenMode.NewPane => "split",
94-
ObsidianOpenMode.HoverPopover => "popover",
95-
_ => throw new ArgumentException()
96-
};
97-
98-
Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filename)}&openmode={openmode}");
94+
await OpenFileInNewWindow(vault, filepath);
95+
}
96+
else
97+
{
98+
string openmode = obsidianOpenMode switch
99+
{
100+
ObsidianOpenMode.CurrentTab => "false",
101+
ObsidianOpenMode.NewTab => "tab",
102+
//ObsidianOpenMode.NewWindow => "window",
103+
ObsidianOpenMode.NewPane => "split",
104+
ObsidianOpenMode.HoverPopover => "popover",
105+
ObsidianOpenMode.VaultAndNewWindow => "window",
106+
_ => throw new ArgumentException()
107+
};
108+
Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filepath)}&openmode={openmode}");
109+
}
99110
}
100111
}
101112

113+
private async Task OpenFileInNewWindow(string vault, string filepath)
114+
{
115+
List<WindowVisualState> states = Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Select(w => new WindowVisualState(w)).ToList();
116+
Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filepath)}&openmode=window");
117+
118+
Stopwatch stopwach = Stopwatch.StartNew();
119+
do
120+
{
121+
await Task.Delay(50);
122+
// This is not precise enough. If the vault hasn't been opend before, Obsidian will create two or more windows.
123+
// But that case is hard to detect.
124+
if (Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Count() > states.Count)
125+
{
126+
// 250~477ms
127+
Debug.WriteLine($"Found new window after {stopwach.ElapsedMilliseconds}ms");
128+
break;
129+
}
130+
} while (stopwach.ElapsedMilliseconds < 10000);
131+
132+
foreach (WindowVisualState state in states)
133+
{
134+
state.Restore();
135+
}
136+
137+
/*
138+
// If not specify Chrome_WidgetWin_1, we will get Chrome_WidgetWin_0.
139+
HWND newWindow = Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Where(x => !states.Select(x => x.Handle).Contains(x)).First();
140+
Debug.WriteLine($"New window: {new WindowVisualState(newWindow)}");
141+
142+
PInvoke.BringWindowToTop(newWindow);
143+
*/
144+
}
145+
102146
private static string GetVaultName(string path)
103147
{
104148
return Path.GetFileName(path);
@@ -142,7 +186,7 @@ private void OpenFileByFallback(string path)
142186
Process.Start(editor, String.Format(_settings.FallbackMarkdownEditorArguments, $@"""{path}"""));
143187
}
144188

145-
private void OpenFileInRecent(string path)
189+
private async Task OpenFileInRecent(string path)
146190
{
147191
// hard link stays valid when the source file is deleted;
148192
// symbolic link requires SeCreateSymbolicLinkPrivilege;
@@ -177,7 +221,7 @@ private void OpenFileInRecent(string path)
177221
path_in_recent = CreateLinkInRecent(directory.Parent, false) + '\\' + directory.Name;
178222
}
179223

180-
OpenFileInVault(path_in_recent, _settings.RecentVault);
224+
await OpenFileInVault(path_in_recent, _settings.RecentVault);
181225
}
182226

183227
private static string FormatLinkName(string prefixed_name, bool explicitDirectory)

ObsidianShell/ObsidianShell.csproj

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66
</PropertyGroup>
77

88
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
9-
<DebugType>none</DebugType>
9+
<DebugType>full</DebugType>
1010
</PropertyGroup>
1111

1212
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
13-
<DebugType>none</DebugType>
13+
<DebugType>full</DebugType>
1414
</PropertyGroup>
1515

1616
<ItemGroup>
1717
<PackageReference Include="Fastenshtein" Version="1.0.0.8" />
18+
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
1822
<PackageReference Include="NCode.ReparsePoints" Version="1.0.2" />
1923
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
2024
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />

ObsidianShell/Settings.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ public enum ObsidianOpenMode
1818
CurrentTab = 0,
1919
NewTab = 1,
2020
NewWindow = 2,
21+
VaultAndNewWindow = 5,
2122
NewPane = 3,
22-
HoverPopover = 4
23+
HoverPopover = 4,
2324
}
2425

2526
public class Settings : INotifyPropertyChanged
@@ -35,10 +36,10 @@ public class Settings : INotifyPropertyChanged
3536
public int RecentVaultSubdirectoriesLimit { get; set; } = 10;
3637

3738
public bool EnableAdvancedURI { get; set; } = false;
38-
public ObsidianOpenMode ObsidianDefaultOpenMode { get; set; } = ObsidianOpenMode.NewWindow;
39+
public ObsidianOpenMode ObsidianDefaultOpenMode { get; set; } = ObsidianOpenMode.NewTab;
3940
public ObsidianOpenMode ObsidianCtrlOpenMode { get; set; } = ObsidianOpenMode.NewPane;
40-
public ObsidianOpenMode ObsidianShiftOpenMode { get; set; } = ObsidianOpenMode.CurrentTab;
41-
public ObsidianOpenMode ObsidianAltOpenMode { get; set; } = ObsidianOpenMode.NewTab;
41+
public ObsidianOpenMode ObsidianShiftOpenMode { get; set; } = ObsidianOpenMode.NewWindow;
42+
public ObsidianOpenMode ObsidianAltOpenMode { get; set; } = ObsidianOpenMode.CurrentTab;
4243
public ObsidianOpenMode ObsidianWinOpenMode { get; set; } = ObsidianOpenMode.HoverPopover;
4344

4445
private static string GetPath()

ObsidianShell/Utils.cs

+95-3
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,114 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.IO;
45
using System.Linq;
56
using System.Runtime.InteropServices;
67
using System.Text;
78
using System.Threading.Tasks;
89
using System.Windows.Forms;
10+
using Windows.Win32;
11+
using Windows.Win32.Foundation;
12+
using Windows.Win32.UI.WindowsAndMessaging;
913

1014
namespace ObsidianShell
1115
{
16+
internal class WindowVisualState
17+
{
18+
public HWND Handle { get; }
19+
//private bool _visibility;
20+
private bool _minimized;
21+
private HWND _prevWindow;
22+
23+
public WindowVisualState(HWND handle)
24+
{
25+
Handle = handle;
26+
//_visibility = PInvoke.IsWindowVisible(handle);
27+
_minimized = PInvoke.IsIconic(handle);
28+
_prevWindow = GetPrevVisibleWindow(handle);
29+
}
30+
31+
private static HWND GetPrevVisibleWindow(HWND handle)
32+
{
33+
HWND nextWindow = handle;
34+
while ((nextWindow = PInvoke.GetWindow(nextWindow, GET_WINDOW_CMD.GW_HWNDPREV)) != default)
35+
{
36+
if (PInvoke.IsWindowVisible(nextWindow))
37+
return nextWindow;
38+
}
39+
return default;
40+
}
41+
42+
public void Restore()
43+
{
44+
WindowVisualState newState = new(Handle);
45+
if (//newState._visibility != _visibility ||
46+
newState._minimized != _minimized ||
47+
newState._prevWindow != _prevWindow
48+
)
49+
{
50+
SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;
51+
//flags |= _visibility ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
52+
// SetWindowPos will move hWnd below hWndInsertAfter
53+
PInvoke.SetWindowPos(Handle, _prevWindow, 0, 0, 0, 0, flags);
54+
55+
if (_minimized)
56+
PInvoke.ShowWindow(Handle, SHOW_WINDOW_CMD.SW_MINIMIZE);
57+
58+
Debug.WriteLine($"{this}\n-> {newState}\n-> {new WindowVisualState(Handle)}");
59+
}
60+
}
61+
62+
public override string ToString()
63+
{
64+
return @$"{Handle.Value}({Utils.GetClassName(Handle)}:{Utils.GetWindowText(Handle)}) {""/*_visibility*/} {_minimized} {_prevWindow.Value}({Utils.GetClassName(_prevWindow)}:{Utils.GetWindowText(_prevWindow)})";
65+
}
66+
}
67+
1268
internal class Utils
1369
{
14-
[DllImport("user32.dll")]
15-
private static extern short GetAsyncKeyState(Keys vKey);
70+
internal static string GetClassName(HWND hWnd)
71+
{
72+
unsafe
73+
{
74+
char* className = stackalloc char[256];
75+
PInvoke.GetClassName(hWnd, className, 256);
76+
return new string(className);
77+
}
78+
}
79+
80+
internal static string GetWindowText(HWND hWnd)
81+
{
82+
unsafe
83+
{
84+
char* buffer = stackalloc char[256];
85+
PInvoke.SendMessage(hWnd, PInvoke.WM_GETTEXT, 256, (nint)buffer);
86+
return new(buffer);
87+
}
88+
}
89+
90+
public static IEnumerable<HWND> EnumerateProcessWindowHandles(string friendlyProcessName, string className = null)
91+
{
92+
List<HWND> handles = new();
93+
foreach (Process process in Process.GetProcessesByName(friendlyProcessName))
94+
{
95+
foreach (ProcessThread thread in process.Threads)
96+
{
97+
PInvoke.EnumThreadWindows((uint)thread.Id, (hWnd, lParam) => {
98+
if (className is null || GetClassName(hWnd) == className)
99+
{
100+
handles.Add(hWnd);
101+
}
102+
return true;
103+
}, IntPtr.Zero);
104+
}
105+
}
106+
return handles;
107+
}
16108

17109
public static bool IsKeyPressed(Keys key)
18110
{
19-
return (GetAsyncKeyState(key) & 0x8000) != 0;
111+
return (PInvoke.GetAsyncKeyState((int)key) & 0x8000) != 0;
20112
}
21113

22114
/// <summary>

0 commit comments

Comments
 (0)