Skip to content

Commit

Permalink
Improve NewWindow open mode (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chaoses-Ib committed Mar 12, 2023
1 parent ff0088b commit 9a5656e
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 30 deletions.
4 changes: 2 additions & 2 deletions ObsidianShell.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal class Program
{
static Settings _settings;

static void Main(string[] args)
static async Task Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
Expand All @@ -30,7 +30,7 @@ static void Main(string[] args)
_settings = Settings.Load();

Obsidian obsidian = new Obsidian(_settings);
obsidian.OpenFile(args[0]);
await obsidian.OpenFile(args[0]);
}
}
}
1 change: 1 addition & 0 deletions ObsidianShell.GUI/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public MainViewModel()
ObsidianOpenMode.CurrentTab,
ObsidianOpenMode.NewTab,
ObsidianOpenMode.NewWindow,
ObsidianOpenMode.VaultAndNewWindow,
ObsidianOpenMode.NewPane,
ObsidianOpenMode.HoverPopover
};
Expand Down
11 changes: 11 additions & 0 deletions ObsidianShell/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
BringWindowToTop
EnumThreadWindows
GetAsyncKeyState
GetClassName
GetWindow
IsIconic
IsWindowVisible
SendMessage
SetWindowPos
ShowWindow
WM_GETTEXT
82 changes: 63 additions & 19 deletions ObsidianShell/Obsidian.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
using System.IO;
using System.Linq;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Windows.Win32;
using Windows.Win32.Foundation;

namespace ObsidianShell
{
Expand All @@ -20,15 +24,15 @@ public Obsidian(Settings settings)
_settings = settings;
}

public void OpenFile(string path)
public async Task OpenFile(string path)
{
switch (_settings.OpenMode)
{
case OpenMode.VaultFallback:
{
if (IsFileInVault(path))
{
OpenFileInVault(path);
await OpenFileInVault(path);
}
else
{
Expand All @@ -40,17 +44,17 @@ public void OpenFile(string path)
{
if (IsFileInVault(path))
{
OpenFileInVault(path);
await OpenFileInVault(path);
}
else
{
OpenFileInRecent(path);
await OpenFileInRecent(path);
}
break;
}
case OpenMode.Recent:
{
OpenFileInRecent(path);
await OpenFileInRecent(path);
break;
}
}
Expand All @@ -63,7 +67,7 @@ private static string PercentEncode(string text)
return Uri.EscapeDataString(text);
}

private void OpenFileInVault(string path, string vaultPath = null)
private async Task OpenFileInVault(string path, string vaultPath = null)
{
if (_settings.EnableAdvancedURI is false)
{
Expand All @@ -73,7 +77,7 @@ private void OpenFileInVault(string path, string vaultPath = null)
{
vaultPath = vaultPath ?? GetFileVaultPath(path);
string vault = GetVaultName(vaultPath);
string filename = Utils.GetRelativePath(vaultPath, path);
string filepath = Utils.GetRelativePath(vaultPath, path);

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

string openmode = obsidianOpenMode switch
if (obsidianOpenMode is ObsidianOpenMode.NewWindow)
{
ObsidianOpenMode.CurrentTab => "false",
ObsidianOpenMode.NewTab => "tab",
ObsidianOpenMode.NewWindow => "window",
ObsidianOpenMode.NewPane => "split",
ObsidianOpenMode.HoverPopover => "popover",
_ => throw new ArgumentException()
};

Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filename)}&openmode={openmode}");
await OpenFileInNewWindow(vault, filepath);
}
else
{
string openmode = obsidianOpenMode switch
{
ObsidianOpenMode.CurrentTab => "false",
ObsidianOpenMode.NewTab => "tab",
//ObsidianOpenMode.NewWindow => "window",
ObsidianOpenMode.NewPane => "split",
ObsidianOpenMode.HoverPopover => "popover",
ObsidianOpenMode.VaultAndNewWindow => "window",
_ => throw new ArgumentException()
};
Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filepath)}&openmode={openmode}");
}
}
}

private async Task OpenFileInNewWindow(string vault, string filepath)
{
List<WindowVisualState> states = Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Select(w => new WindowVisualState(w)).ToList();
Process.Start($"obsidian://advanced-uri?vault={PercentEncode(vault)}&filepath={PercentEncode(filepath)}&openmode=window");

Stopwatch stopwach = Stopwatch.StartNew();
do
{
await Task.Delay(50);
// This is not precise enough. If the vault hasn't been opend before, Obsidian will create two or more windows.
// But that case is hard to detect.
if (Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Count() > states.Count)
{
// 250~477ms
Debug.WriteLine($"Found new window after {stopwach.ElapsedMilliseconds}ms");
break;
}
} while (stopwach.ElapsedMilliseconds < 10000);

foreach (WindowVisualState state in states)
{
state.Restore();
}

/*
// If not specify Chrome_WidgetWin_1, we will get Chrome_WidgetWin_0.
HWND newWindow = Utils.EnumerateProcessWindowHandles("Obsidian", "Chrome_WidgetWin_1").Where(x => !states.Select(x => x.Handle).Contains(x)).First();
Debug.WriteLine($"New window: {new WindowVisualState(newWindow)}");
PInvoke.BringWindowToTop(newWindow);
*/
}

private static string GetVaultName(string path)
{
return Path.GetFileName(path);
Expand Down Expand Up @@ -142,7 +186,7 @@ private void OpenFileByFallback(string path)
Process.Start(editor, String.Format(_settings.FallbackMarkdownEditorArguments, $@"""{path}"""));
}

private void OpenFileInRecent(string path)
private async Task OpenFileInRecent(string path)
{
// hard link stays valid when the source file is deleted;
// symbolic link requires SeCreateSymbolicLinkPrivilege;
Expand Down Expand Up @@ -177,7 +221,7 @@ private void OpenFileInRecent(string path)
path_in_recent = CreateLinkInRecent(directory.Parent, false) + '\\' + directory.Name;
}

OpenFileInVault(path_in_recent, _settings.RecentVault);
await OpenFileInVault(path_in_recent, _settings.RecentVault);
}

private static string FormatLinkName(string prefixed_name, bool explicitDirectory)
Expand Down
8 changes: 6 additions & 2 deletions ObsidianShell/ObsidianShell.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>none</DebugType>
<DebugType>full</DebugType>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>none</DebugType>
<DebugType>full</DebugType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Fastenshtein" Version="1.0.0.8" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NCode.ReparsePoints" Version="1.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
Expand Down
9 changes: 5 additions & 4 deletions ObsidianShell/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ public enum ObsidianOpenMode
CurrentTab = 0,
NewTab = 1,
NewWindow = 2,
VaultAndNewWindow = 5,
NewPane = 3,
HoverPopover = 4
HoverPopover = 4,
}

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

public bool EnableAdvancedURI { get; set; } = false;
public ObsidianOpenMode ObsidianDefaultOpenMode { get; set; } = ObsidianOpenMode.NewWindow;
public ObsidianOpenMode ObsidianDefaultOpenMode { get; set; } = ObsidianOpenMode.NewTab;
public ObsidianOpenMode ObsidianCtrlOpenMode { get; set; } = ObsidianOpenMode.NewPane;
public ObsidianOpenMode ObsidianShiftOpenMode { get; set; } = ObsidianOpenMode.CurrentTab;
public ObsidianOpenMode ObsidianAltOpenMode { get; set; } = ObsidianOpenMode.NewTab;
public ObsidianOpenMode ObsidianShiftOpenMode { get; set; } = ObsidianOpenMode.NewWindow;
public ObsidianOpenMode ObsidianAltOpenMode { get; set; } = ObsidianOpenMode.CurrentTab;
public ObsidianOpenMode ObsidianWinOpenMode { get; set; } = ObsidianOpenMode.HoverPopover;

private static string GetPath()
Expand Down
98 changes: 95 additions & 3 deletions ObsidianShell/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,114 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;

namespace ObsidianShell
{
internal class WindowVisualState
{
public HWND Handle { get; }
//private bool _visibility;
private bool _minimized;
private HWND _prevWindow;

public WindowVisualState(HWND handle)
{
Handle = handle;
//_visibility = PInvoke.IsWindowVisible(handle);
_minimized = PInvoke.IsIconic(handle);
_prevWindow = GetPrevVisibleWindow(handle);
}

private static HWND GetPrevVisibleWindow(HWND handle)
{
HWND nextWindow = handle;
while ((nextWindow = PInvoke.GetWindow(nextWindow, GET_WINDOW_CMD.GW_HWNDPREV)) != default)
{
if (PInvoke.IsWindowVisible(nextWindow))
return nextWindow;
}
return default;
}

public void Restore()
{
WindowVisualState newState = new(Handle);
if (//newState._visibility != _visibility ||
newState._minimized != _minimized ||
newState._prevWindow != _prevWindow
)
{
SET_WINDOW_POS_FLAGS flags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE;
//flags |= _visibility ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
// SetWindowPos will move hWnd below hWndInsertAfter
PInvoke.SetWindowPos(Handle, _prevWindow, 0, 0, 0, 0, flags);

if (_minimized)
PInvoke.ShowWindow(Handle, SHOW_WINDOW_CMD.SW_MINIMIZE);

Debug.WriteLine($"{this}\n-> {newState}\n-> {new WindowVisualState(Handle)}");
}
}

public override string ToString()
{
return @$"{Handle.Value}({Utils.GetClassName(Handle)}:{Utils.GetWindowText(Handle)}) {""/*_visibility*/} {_minimized} {_prevWindow.Value}({Utils.GetClassName(_prevWindow)}:{Utils.GetWindowText(_prevWindow)})";
}
}

internal class Utils
{
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(Keys vKey);
internal static string GetClassName(HWND hWnd)
{
unsafe
{
char* className = stackalloc char[256];
PInvoke.GetClassName(hWnd, className, 256);
return new string(className);
}
}

internal static string GetWindowText(HWND hWnd)
{
unsafe
{
char* buffer = stackalloc char[256];
PInvoke.SendMessage(hWnd, PInvoke.WM_GETTEXT, 256, (nint)buffer);
return new(buffer);
}
}

public static IEnumerable<HWND> EnumerateProcessWindowHandles(string friendlyProcessName, string className = null)
{
List<HWND> handles = new();
foreach (Process process in Process.GetProcessesByName(friendlyProcessName))
{
foreach (ProcessThread thread in process.Threads)
{
PInvoke.EnumThreadWindows((uint)thread.Id, (hWnd, lParam) => {
if (className is null || GetClassName(hWnd) == className)
{
handles.Add(hWnd);
}
return true;
}, IntPtr.Zero);
}
}
return handles;
}

public static bool IsKeyPressed(Keys key)
{
return (GetAsyncKeyState(key) & 0x8000) != 0;
return (PInvoke.GetAsyncKeyState((int)key) & 0x8000) != 0;
}

/// <summary>
Expand Down

0 comments on commit 9a5656e

Please sign in to comment.