Skip to content

Commit 0a7a3d5

Browse files
Merge pull request #8 from Code-Stage/feature/issue-6-opus
Add companion window for Package Manager My Assets imports
2 parents cef724a + 9341cef commit 0a7a3d5

4 files changed

Lines changed: 279 additions & 17 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: 257 additions & 11 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
{
@@ -110,16 +112,77 @@ private static MethodInfo ShowImportPackageMethodInfo
110112
{
111113
if (showImportPackageMethodInfo == null)
112114
{
113-
var packageImport = typeof(MenuItem).Assembly.GetType("UnityEditor.PackageImport");
114-
showImportPackageMethodInfo = packageImport.GetMethod("ShowImportPackage");
115+
showImportPackageMethodInfo = PackageImportType.GetMethod("ShowImportPackage");
115116
}
116117

117118
return showImportPackageMethodInfo;
118119
}
119120
}
120121

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

157+
///////////////////////////////////////////////////////////////
158+
// PackageImport window watcher
159+
///////////////////////////////////////////////////////////////
160+
161+
[InitializeOnLoadMethod]
162+
private static void SetupPackageImportWatcher()
163+
{
164+
EditorApplication.update -= WatchForPackageImportWindows;
165+
EditorApplication.update += WatchForPackageImportWindows;
166+
}
167+
168+
private static double nextWatchTime;
169+
170+
private static void WatchForPackageImportWindows()
171+
{
172+
if (EditorApplication.timeSinceStartup < nextWatchTime) return;
173+
nextWatchTime = EditorApplication.timeSinceStartup + 0.25;
174+
175+
var windows = Resources.FindObjectsOfTypeAll(PackageImportType);
176+
if (windows == null || windows.Length == 0) return;
177+
178+
foreach (var window in windows)
179+
{
180+
var editorWindow = window as EditorWindow;
181+
if (editorWindow != null)
182+
Package2FolderCompanion.ShowForImportWindow(editorWindow);
183+
}
184+
}
185+
123186
///////////////////////////////////////////////////////////////
124187
// Unity Editor menus integration
125188
///////////////////////////////////////////////////////////////
@@ -137,7 +200,7 @@ private static void Package2FolderCommand()
137200
var packagePath = EditorUtility.OpenFilePanel("Import package ...", "", "unitypackage");
138201
if (string.IsNullOrEmpty(packagePath)) return;
139202
if (!File.Exists(packagePath)) return;
140-
203+
141204
var selectedFolderPath = GetSelectedFolderPath();
142205
ImportPackageToFolder(packagePath, selectedFolderPath, true);
143206
}
@@ -180,7 +243,7 @@ public static void ImportPackageToFolder(string packagePath, string selectedFold
180243
{
181244
#if CS_P2F_NEW_ARGUMENT_2
182245
ShowImportPackageWindow(packagePath, assetsItems, packageIconPath, assetOrigin);
183-
#else
246+
#else
184247
ShowImportPackageWindow(packagePath, assetsItems, packageIconPath, allowReInstall);
185248
#endif
186249

@@ -192,11 +255,14 @@ public static void ImportPackageToFolder(string packagePath, string selectedFold
192255
}
193256
}
194257

195-
private static void ChangeAssetItemPath(object assetItem, string selectedFolderPath)
258+
public static void ChangeAssetItemPath(object assetItem, string selectedFolderPath)
196259
{
260+
if (string.IsNullOrEmpty(selectedFolderPath) || !selectedFolderPath.StartsWith("Assets"))
261+
throw new ArgumentException("selectedFolderPath must start with 'Assets'", "selectedFolderPath");
262+
197263
string destinationPath = (string)DestinationAssetPathFieldInfo.GetValue(assetItem);
198264
if (destinationPath.StartsWith("Packages/")) return;
199-
265+
200266
int firstSlashIndex = destinationPath.IndexOf('/');
201267
if (firstSlashIndex >= 0)
202268
{
@@ -207,10 +273,10 @@ private static void ChangeAssetItemPath(object assetItem, string selectedFolderP
207273
{
208274
destinationPath = selectedFolderPath + "/" + destinationPath;
209275
}
210-
276+
211277
DestinationAssetPathFieldInfo.SetValue(assetItem, destinationPath);
212278
}
213-
279+
214280
#if CS_P2F_NEW_ARGUMENT_2
215281
public static void ShowImportPackageWindow(string path, object[] array, string packageIconPath, object assetOrigin = null)
216282
{
@@ -268,6 +334,53 @@ public static void ImportPackageSilently(string packageName, object[] assetsItem
268334
#endif
269335
}
270336

337+
///////////////////////////////////////////////////////////////
338+
// PackageImport window helpers
339+
///////////////////////////////////////////////////////////////
340+
341+
internal static object[] GetImportPackageItems(EditorWindow importWindow)
342+
{
343+
return ImportPackageItemsFieldInfo.GetValue(importWindow) as object[];
344+
}
345+
346+
internal static string[] GetImportItemPaths(EditorWindow importWindow)
347+
{
348+
var items = GetImportPackageItems(importWindow);
349+
if (items == null) return null;
350+
351+
var paths = new string[items.Length];
352+
for (int i = 0; i < items.Length; i++)
353+
{
354+
paths[i] = (string)DestinationAssetPathFieldInfo.GetValue(items[i]);
355+
}
356+
return paths;
357+
}
358+
359+
internal static void SetImportWindowFolder(EditorWindow importWindow, string selectedFolderPath, string[] originalPaths)
360+
{
361+
var items = GetImportPackageItems(importWindow);
362+
if (items == null) return;
363+
364+
// Restore original paths first to avoid stacking folder prefixes
365+
if (originalPaths != null)
366+
{
367+
for (int i = 0; i < items.Length && i < originalPaths.Length; i++)
368+
{
369+
DestinationAssetPathFieldInfo.SetValue(items[i], originalPaths[i]);
370+
}
371+
}
372+
373+
// Apply new folder
374+
foreach (var item in items)
375+
{
376+
ChangeAssetItemPath(item, selectedFolderPath);
377+
}
378+
379+
// Reset tree view to force rebuild
380+
TreeFieldInfo.SetValue(importWindow, null);
381+
importWindow.Repaint();
382+
}
383+
271384
///////////////////////////////////////////////////////////////
272385
// Utility methods
273386
///////////////////////////////////////////////////////////////
@@ -282,4 +395,137 @@ private static string GetSelectedFolderPath()
282395
return !Directory.Exists(path) ? null : path;
283396
}
284397
}
285-
}
398+
399+
internal class Package2FolderCompanion : EditorWindow
400+
{
401+
private static readonly Dictionary<int, Package2FolderCompanion> activeCompanions = new Dictionary<int, Package2FolderCompanion>();
402+
private static readonly HashSet<int> dismissedImportWindows = new HashSet<int>();
403+
404+
[SerializeField] private EditorWindow importWindow;
405+
[SerializeField] private string[] originalPaths;
406+
[SerializeField] private string selectedFolder;
407+
408+
internal static void ShowForImportWindow(EditorWindow importWindow)
409+
{
410+
var id = importWindow.GetInstanceID();
411+
412+
if (dismissedImportWindows.Contains(id))
413+
return;
414+
415+
ClearStaleEntries();
416+
417+
Package2FolderCompanion existing;
418+
if (activeCompanions.TryGetValue(id, out existing) && existing != null)
419+
return;
420+
421+
var companion = CreateInstance<Package2FolderCompanion>();
422+
companion.importWindow = importWindow;
423+
companion.titleContent = new GUIContent("Package2Folder");
424+
companion.CacheOriginalPaths();
425+
companion.ShowUtility();
426+
companion.PositionNearImportWindow();
427+
activeCompanions[id] = companion;
428+
}
429+
430+
private static void ClearStaleEntries()
431+
{
432+
var staleKeys = new List<int>();
433+
foreach (var kvp in activeCompanions)
434+
{
435+
if (kvp.Value == null || kvp.Value.importWindow == null)
436+
staleKeys.Add(kvp.Key);
437+
}
438+
foreach (var key in staleKeys)
439+
{
440+
activeCompanions.Remove(key);
441+
dismissedImportWindows.Remove(key);
442+
}
443+
}
444+
445+
private void CacheOriginalPaths()
446+
{
447+
originalPaths = Package2Folder.GetImportItemPaths(importWindow);
448+
}
449+
450+
private void PositionNearImportWindow()
451+
{
452+
if (importWindow == null) return;
453+
454+
var importPos = importWindow.position;
455+
position = new Rect(
456+
importPos.x + importPos.width + 10,
457+
importPos.y,
458+
220,
459+
60
460+
);
461+
}
462+
463+
private void OnEnable()
464+
{
465+
if (importWindow != null)
466+
activeCompanions[importWindow.GetInstanceID()] = this;
467+
}
468+
469+
private void Update()
470+
{
471+
if (importWindow == null)
472+
{
473+
Close();
474+
}
475+
}
476+
477+
private void OnGUI()
478+
{
479+
if (GUILayout.Button("Import to Folder...", GUILayout.Height(30)))
480+
{
481+
SelectFolderAndModifyPaths();
482+
}
483+
484+
if (!string.IsNullOrEmpty(selectedFolder))
485+
{
486+
EditorGUILayout.LabelField("Target: " + selectedFolder, EditorStyles.miniLabel);
487+
}
488+
}
489+
490+
private void SelectFolderAndModifyPaths()
491+
{
492+
var absolutePath = EditorUtility.OpenFolderPanel("Select target folder", "Assets", "");
493+
if (string.IsNullOrEmpty(absolutePath)) return;
494+
if (importWindow == null) return;
495+
496+
absolutePath = absolutePath.Replace('\\', '/');
497+
var dataPath = Application.dataPath.Replace('\\', '/');
498+
499+
string relativePath;
500+
if (absolutePath == dataPath)
501+
{
502+
relativePath = "Assets";
503+
}
504+
else if (absolutePath.StartsWith(dataPath + "/"))
505+
{
506+
relativePath = "Assets" + absolutePath.Substring(dataPath.Length);
507+
}
508+
else
509+
{
510+
EditorUtility.DisplayDialog("Invalid Folder",
511+
"Please select a folder inside the Assets directory.", "OK");
512+
return;
513+
}
514+
515+
selectedFolder = relativePath;
516+
Package2Folder.SetImportWindowFolder(importWindow, selectedFolder, originalPaths);
517+
Repaint();
518+
}
519+
520+
private void OnDestroy()
521+
{
522+
if (importWindow != null)
523+
{
524+
var id = importWindow.GetInstanceID();
525+
activeCompanions.Remove(id);
526+
// Import window still alive means user dismissed companion manually
527+
dismissedImportWindows.Add(id);
528+
}
529+
}
530+
}
531+
}

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)