Skip to content

Commit 88f142c

Browse files
Add companion window for PackageImport dialog (closes #6)
Add a Package2Folder companion utility window that auto-appears whenever Unity's PackageImport dialog opens, allowing users to redirect any .unitypackage import to a specific folder. Works with Package Manager "My Assets" imports, drag-and-drop, and Assets menu. - Watch for PackageImport windows via EditorApplication.update - Companion window with "Import to Folder..." button - Modify import item paths in-place via reflection and refresh dialog - Cache original paths to allow re-selecting without path stacking - Survive domain reloads via SerializeField and OnEnable re-registration - Auto-close companion when PackageImport dialog closes - Bump version to 1.3.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c26847 commit 88f142c

4 files changed

Lines changed: 243 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.
1111

1212
💡 _Always remove previous plugin version before updating_
1313

14+
## [1.3.0] - 2026-02-13
15+
16+
### Added
17+
- Add companion window that auto-appears alongside Unity's import dialog, allowing you to redirect any .unitypackage import to a specific folder — works with Package Manager "My Assets" imports, drag-and-drop, and the Assets menu (closes #6)
18+
1419
## [1.2.1] - 2025-08-15
1520

1621
### Fixed

Editor/Package2Folder.cs

Lines changed: 221 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// new argument was added in 19.1.4
1+
// new argument was added in 19.1.4
22

33
#if UNITY_2019_3_OR_NEWER
44
#define CS_P2F_NEW_ARGUMENT_2
@@ -11,9 +11,11 @@
1111
#endif
1212

1313
using System;
14+
using System.Collections.Generic;
1415
using System.IO;
1516
using System.Reflection;
1617
using UnityEditor;
18+
using UnityEngine;
1719

1820
namespace CodeStage.PackageToFolder
1921
{
@@ -64,7 +66,7 @@ private static ExtractAndPrepareAssetListDelegate ExtractAndPrepareAssetList
6466
return extractAndPrepareAssetList;
6567
}
6668
}
67-
69+
6870
private static FieldInfo destinationAssetPathFieldInfo;
6971
private static FieldInfo DestinationAssetPathFieldInfo
7072
{
@@ -118,8 +120,65 @@ private static MethodInfo ShowImportPackageMethodInfo
118120
}
119121
}
120122

123+
private static Type packageImportType;
124+
private static Type PackageImportType
125+
{
126+
get
127+
{
128+
if (packageImportType == null)
129+
packageImportType = typeof(MenuItem).Assembly.GetType("UnityEditor.PackageImport");
130+
return packageImportType;
131+
}
132+
}
133+
134+
private static FieldInfo importPackageItemsFieldInfo;
135+
private static FieldInfo ImportPackageItemsFieldInfo
136+
{
137+
get
138+
{
139+
if (importPackageItemsFieldInfo == null)
140+
importPackageItemsFieldInfo = PackageImportType.GetField("m_ImportPackageItems", BindingFlags.NonPublic | BindingFlags.Instance);
141+
return importPackageItemsFieldInfo;
142+
}
143+
}
144+
145+
private static FieldInfo treeFieldInfo;
146+
private static FieldInfo TreeFieldInfo
147+
{
148+
get
149+
{
150+
if (treeFieldInfo == null)
151+
treeFieldInfo = PackageImportType.GetField("m_Tree", BindingFlags.NonPublic | BindingFlags.Instance);
152+
return treeFieldInfo;
153+
}
154+
}
155+
121156
#endregion reflection stuff
122157

158+
///////////////////////////////////////////////////////////////
159+
// PackageImport window watcher
160+
///////////////////////////////////////////////////////////////
161+
162+
[InitializeOnLoadMethod]
163+
private static void SetupPackageImportWatcher()
164+
{
165+
EditorApplication.update -= WatchForPackageImportWindows;
166+
EditorApplication.update += WatchForPackageImportWindows;
167+
}
168+
169+
private static void WatchForPackageImportWindows()
170+
{
171+
var windows = Resources.FindObjectsOfTypeAll(PackageImportType);
172+
if (windows == null || windows.Length == 0) return;
173+
174+
foreach (var window in windows)
175+
{
176+
var editorWindow = window as EditorWindow;
177+
if (editorWindow != null)
178+
Package2FolderCompanion.ShowForImportWindow(editorWindow);
179+
}
180+
}
181+
123182
///////////////////////////////////////////////////////////////
124183
// Unity Editor menus integration
125184
///////////////////////////////////////////////////////////////
@@ -137,7 +196,7 @@ private static void Package2FolderCommand()
137196
var packagePath = EditorUtility.OpenFilePanel("Import package ...", "", "unitypackage");
138197
if (string.IsNullOrEmpty(packagePath)) return;
139198
if (!File.Exists(packagePath)) return;
140-
199+
141200
var selectedFolderPath = GetSelectedFolderPath();
142201
ImportPackageToFolder(packagePath, selectedFolderPath, true);
143202
}
@@ -180,7 +239,7 @@ public static void ImportPackageToFolder(string packagePath, string selectedFold
180239
{
181240
#if CS_P2F_NEW_ARGUMENT_2
182241
ShowImportPackageWindow(packagePath, assetsItems, packageIconPath, assetOrigin);
183-
#else
242+
#else
184243
ShowImportPackageWindow(packagePath, assetsItems, packageIconPath, allowReInstall);
185244
#endif
186245

@@ -192,11 +251,11 @@ public static void ImportPackageToFolder(string packagePath, string selectedFold
192251
}
193252
}
194253

195-
private static void ChangeAssetItemPath(object assetItem, string selectedFolderPath)
254+
public static void ChangeAssetItemPath(object assetItem, string selectedFolderPath)
196255
{
197256
string destinationPath = (string)DestinationAssetPathFieldInfo.GetValue(assetItem);
198257
if (destinationPath.StartsWith("Packages/")) return;
199-
258+
200259
int firstSlashIndex = destinationPath.IndexOf('/');
201260
if (firstSlashIndex >= 0)
202261
{
@@ -207,10 +266,10 @@ private static void ChangeAssetItemPath(object assetItem, string selectedFolderP
207266
{
208267
destinationPath = selectedFolderPath + "/" + destinationPath;
209268
}
210-
269+
211270
DestinationAssetPathFieldInfo.SetValue(assetItem, destinationPath);
212271
}
213-
272+
214273
#if CS_P2F_NEW_ARGUMENT_2
215274
public static void ShowImportPackageWindow(string path, object[] array, string packageIconPath, object assetOrigin = null)
216275
{
@@ -268,6 +327,53 @@ public static void ImportPackageSilently(string packageName, object[] assetsItem
268327
#endif
269328
}
270329

330+
///////////////////////////////////////////////////////////////
331+
// PackageImport window helpers
332+
///////////////////////////////////////////////////////////////
333+
334+
internal static object[] GetImportPackageItems(EditorWindow importWindow)
335+
{
336+
return ImportPackageItemsFieldInfo.GetValue(importWindow) as object[];
337+
}
338+
339+
internal static string[] GetImportItemPaths(EditorWindow importWindow)
340+
{
341+
var items = GetImportPackageItems(importWindow);
342+
if (items == null) return null;
343+
344+
var paths = new string[items.Length];
345+
for (int i = 0; i < items.Length; i++)
346+
{
347+
paths[i] = (string)DestinationAssetPathFieldInfo.GetValue(items[i]);
348+
}
349+
return paths;
350+
}
351+
352+
internal static void SetImportWindowFolder(EditorWindow importWindow, string selectedFolderPath, string[] originalPaths)
353+
{
354+
var items = GetImportPackageItems(importWindow);
355+
if (items == null) return;
356+
357+
// Restore original paths first to avoid stacking folder prefixes
358+
if (originalPaths != null)
359+
{
360+
for (int i = 0; i < items.Length && i < originalPaths.Length; i++)
361+
{
362+
DestinationAssetPathFieldInfo.SetValue(items[i], originalPaths[i]);
363+
}
364+
}
365+
366+
// Apply new folder
367+
foreach (var item in items)
368+
{
369+
ChangeAssetItemPath(item, selectedFolderPath);
370+
}
371+
372+
// Reset tree view to force rebuild
373+
TreeFieldInfo.SetValue(importWindow, null);
374+
importWindow.Repaint();
375+
}
376+
271377
///////////////////////////////////////////////////////////////
272378
// Utility methods
273379
///////////////////////////////////////////////////////////////
@@ -282,4 +388,110 @@ private static string GetSelectedFolderPath()
282388
return !Directory.Exists(path) ? null : path;
283389
}
284390
}
285-
}
391+
392+
internal class Package2FolderCompanion : EditorWindow
393+
{
394+
private static readonly Dictionary<int, Package2FolderCompanion> activeCompanions = new Dictionary<int, Package2FolderCompanion>();
395+
396+
[SerializeField] private EditorWindow importWindow;
397+
[SerializeField] private string[] originalPaths;
398+
[SerializeField] private string selectedFolder;
399+
400+
internal static void ShowForImportWindow(EditorWindow importWindow)
401+
{
402+
var id = importWindow.GetInstanceID();
403+
404+
Package2FolderCompanion existing;
405+
if (activeCompanions.TryGetValue(id, out existing) && existing != null)
406+
return;
407+
408+
var companion = CreateInstance<Package2FolderCompanion>();
409+
companion.importWindow = importWindow;
410+
companion.titleContent = new GUIContent("Package2Folder");
411+
companion.CacheOriginalPaths();
412+
companion.ShowUtility();
413+
companion.PositionNearImportWindow();
414+
activeCompanions[id] = companion;
415+
}
416+
417+
private void CacheOriginalPaths()
418+
{
419+
originalPaths = Package2Folder.GetImportItemPaths(importWindow);
420+
}
421+
422+
private void PositionNearImportWindow()
423+
{
424+
if (importWindow == null) return;
425+
426+
var importPos = importWindow.position;
427+
position = new Rect(
428+
importPos.x + importPos.width + 10,
429+
importPos.y,
430+
220,
431+
60
432+
);
433+
}
434+
435+
private void OnEnable()
436+
{
437+
if (importWindow != null)
438+
activeCompanions[importWindow.GetInstanceID()] = this;
439+
}
440+
441+
private void Update()
442+
{
443+
if (importWindow == null)
444+
{
445+
Close();
446+
}
447+
}
448+
449+
private void OnGUI()
450+
{
451+
if (GUILayout.Button("Import to Folder...", GUILayout.Height(30)))
452+
{
453+
SelectFolderAndModifyPaths();
454+
}
455+
456+
if (!string.IsNullOrEmpty(selectedFolder))
457+
{
458+
EditorGUILayout.LabelField("Target: " + selectedFolder, EditorStyles.miniLabel);
459+
}
460+
}
461+
462+
private void SelectFolderAndModifyPaths()
463+
{
464+
var absolutePath = EditorUtility.OpenFolderPanel("Select target folder", "Assets", "");
465+
if (string.IsNullOrEmpty(absolutePath)) return;
466+
467+
absolutePath = absolutePath.Replace('\\', '/');
468+
var dataPath = Application.dataPath.Replace('\\', '/');
469+
470+
string relativePath;
471+
if (absolutePath == dataPath)
472+
{
473+
relativePath = "Assets";
474+
}
475+
else if (absolutePath.StartsWith(dataPath + "/"))
476+
{
477+
relativePath = "Assets" + absolutePath.Substring(dataPath.Length);
478+
}
479+
else
480+
{
481+
EditorUtility.DisplayDialog("Invalid Folder",
482+
"Please select a folder inside the Assets directory.", "OK");
483+
return;
484+
}
485+
486+
selectedFolder = relativePath;
487+
Package2Folder.SetImportWindowFolder(importWindow, selectedFolder, originalPaths);
488+
Repaint();
489+
}
490+
491+
private void OnDestroy()
492+
{
493+
if (importWindow != null)
494+
activeCompanions.Remove(importWindow.GetInstanceID());
495+
}
496+
}
497+
}

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,22 @@ Download the latest `.unitypackage` file from the [Releases](https://github.com/
3434

3535
## How to use
3636

37-
1. Import the package to your project using one of the installation methods above
38-
2. In the Project window, select the folder where you want to import a package
39-
3. Use the menu item: `Assets > Import Package > Here...`
40-
4. Select the `.unitypackage` file you want to import
41-
5. The package will be imported into the selected folder instead of the project root
37+
### Option A: Via right-click menu (select folder first)
38+
39+
1. In the Project window, select the folder where you want to import a package
40+
2. Use the menu item: `Assets > Import Package > Here...`
41+
3. Select the `.unitypackage` file you want to import
42+
4. The package will be imported into the selected folder instead of the project root
43+
44+
### Option B: Via companion window (any import method)
45+
46+
Whenever Unity's import dialog opens — whether from **Package Manager > My Assets**, drag-and-drop, or the Assets menu — a small **Package2Folder** utility window automatically appears next to it:
47+
48+
1. Trigger a `.unitypackage` import from any source (e.g. Package Manager "My Assets" tab)
49+
2. In the companion window that appears, click **"Import to Folder..."**
50+
3. Select the target folder under Assets/
51+
4. The import dialog updates to show the new destination paths
52+
5. Click **Import** in the import dialog as usual
4253

4354
## Public API
4455

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "net.codestage.package2folder",
3-
"version": "1.2.1",
3+
"version": "1.3.0",
44
"displayName": "Package2Folder",
55
"description": "Unity Editor extension that allows you to import custom packages into the selected Project folder, avoiding your project's root bloating.",
66
"unity": "2021.3",

0 commit comments

Comments
 (0)