FileManager.hx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. package hide.tools;
  2. enum abstract GenToManagerCommand(String) {
  3. var success;
  4. }
  5. enum abstract ManagerToGenCommand(String) {
  6. var queue;
  7. var prio;
  8. var clear;
  9. }
  10. typedef GenToManagerSuccessMessage = {
  11. var originalPath : String;
  12. var thumbnailPath : String;
  13. }
  14. enum FileKind {
  15. Dir;
  16. File;
  17. }
  18. enum VCSStatus {
  19. /**
  20. Pending or no vsc available on system
  21. **/
  22. None;
  23. /**
  24. The file is up to date and not modified
  25. **/
  26. UpToDate;
  27. /**
  28. The file is modified locally
  29. **/
  30. Modified;
  31. }
  32. @:access(hide.tools.FileManager)
  33. @:allow(hide.tools.FileManager)
  34. class FileEntry {
  35. public var name: String;
  36. public var path: String;
  37. public var relPath: String;
  38. public var children: Array<FileEntry>;
  39. public var kind: FileKind;
  40. public var parent: FileEntry;
  41. public var iconPath: String;
  42. public var disposed: Bool = false;
  43. public var vcsStatus: VCSStatus = None;
  44. public var ignored: Bool = false;
  45. var registeredWatcher : hide.tools.FileWatcher.FileWatchEvent = null;
  46. public function new(name: String, parent: FileEntry, kind: FileKind) {
  47. this.name = name;
  48. this.parent = parent;
  49. this.kind = kind;
  50. this.relPath = computeRelPath();
  51. this.path = computePath();
  52. this.ignored = computeIgnore();
  53. watch();
  54. FileManager.inst.fileIndex.set(this.getRelPath(), this);
  55. }
  56. public final function toString() : String{
  57. return name;
  58. }
  59. public function dispose() {
  60. if (children != null) {
  61. for (child in children) {
  62. child.dispose();
  63. }
  64. }
  65. disposed = true;
  66. children = null;
  67. if (registeredWatcher != null) {
  68. hide.Ide.inst.fileWatcher.unregister(this.getPath(), registeredWatcher.fun);
  69. registeredWatcher = null;
  70. }
  71. FileManager.inst.fileIndex.remove(this.getRelPath());
  72. }
  73. function refreshChildren() {
  74. if (kind != Dir)
  75. return;
  76. var fullPath = getPath();
  77. var oldChildren : Map<String, FileEntry> = [for (file in (children ?? [])) file.name => file];
  78. if (children == null)
  79. children = [];
  80. else
  81. children.resize(0);
  82. if (js.node.Fs.existsSync(fullPath)) {
  83. var paths = js.node.Fs.readdirSync(fullPath);
  84. for (path in paths) {
  85. if (StringTools.startsWith(path, "."))
  86. continue;
  87. var prev = oldChildren.get(path);
  88. if (prev != null) {
  89. children.push(prev);
  90. oldChildren.remove(path);
  91. } else {
  92. var info = js.node.Fs.statSync(fullPath + "/" + path);
  93. var newEntry = new FileEntry(path, this, info.isDirectory() ? Dir : File);
  94. newEntry.refreshChildren();
  95. children.push(newEntry);
  96. }
  97. }
  98. }
  99. for (child in oldChildren) {
  100. child.dispose();
  101. }
  102. children.sort(compareFile);
  103. }
  104. function watch() {
  105. if (registeredWatcher != null)
  106. throw "already watching";
  107. var rel = this.getRelPath();
  108. registeredWatcher = hide.Ide.inst.fileWatcher.register(rel, FileManager.inst.fileChangeInternal.bind(this), true);
  109. }
  110. public inline function getRelPath() {
  111. return this.relPath;
  112. }
  113. public inline function getPath() {
  114. return this.path;
  115. }
  116. function computePath() {
  117. if (this.parent == null) return hide.Ide.inst.resourceDir;
  118. return this.parent.computePath() + "/" + this.name;
  119. }
  120. function computeRelPath() {
  121. if (this.parent == null) return "";
  122. if (this.parent.parent == null) return this.name;
  123. return this.parent.computeRelPath() + "/" + this.name;
  124. }
  125. function computeIgnore() {
  126. for (excl in FileManager.inst.ignorePatterns) {
  127. if (excl.match(getRelPath()))
  128. return true;
  129. }
  130. return return false;
  131. }
  132. // sort directories before files, and then dirs and files alphabetically
  133. static public function compareFile(a: FileEntry, b: FileEntry) {
  134. if (a.kind != b.kind) {
  135. if (a.kind == Dir) {
  136. return -1;
  137. }
  138. return 1;
  139. }
  140. return Reflect.compare(a.name.toLowerCase(), b.name.toLowerCase());
  141. }
  142. }
  143. typedef MiniatureReadyCallback = (miniaturePath: String) -> Void;
  144. /**
  145. Class that handle parsing and maintaining the state of the project files, and generate miniatures for them on demand
  146. **/
  147. class FileManager {
  148. public var fileRoot: FileEntry;
  149. var fileIndex : Map<String, FileEntry> = [];
  150. public static final thumbnailGeneratorPort = 9669;
  151. public static final thumbnailGeneratorUrl = "localhost";
  152. public static var inst(get, default) : FileManager;
  153. public var onFileChangeHandlers: Array<(entry: FileEntry) -> Void> = [];
  154. public var onVCSStatusUpdateHandlers: Array<() -> Void> = [];
  155. var svnEnabled = false;
  156. var windowManager : RenderWindowManager = null;
  157. var onReadyCallbacks : Map<String, MiniatureReadyCallback> = [];
  158. var serverSocket : hxd.net.Socket = null;
  159. var generatorSocket : hxd.net.Socket = null;
  160. var pendingMessages : Array<String> = [];
  161. var ignorePatterns: Array<EReg> = [];
  162. var fileEntryRefreshDelay : Delayer<FileEntry>;
  163. var retries = 0;
  164. static final maxRetries = 5;
  165. static function get_inst() {
  166. if (inst == null) {
  167. inst = new FileManager();
  168. inst.init();
  169. }
  170. return inst;
  171. }
  172. function new() {
  173. }
  174. public static function onBeforeReload() {
  175. if (inst != null) {
  176. inst.cleanupGenerator();
  177. inst.cleanupServer();
  178. }
  179. }
  180. var pendingMessageQueued = false;
  181. function queueProcessPendingMessages() {
  182. if (!pendingMessageQueued) {
  183. haxe.Timer.delay(processPendingMessages, 10);
  184. pendingMessageQueued = true;
  185. }
  186. }
  187. function processPendingMessages() {
  188. pendingMessageQueued = false;
  189. if (!checkWindowReady()) {
  190. return;
  191. }
  192. var len = hxd.Math.imin(300, pendingMessages.length);
  193. for (i in 0 ... len) {
  194. generatorSocket.out.writeString(pendingMessages[i]);
  195. }
  196. pendingMessages.splice(0, len);
  197. if (pendingMessages.length > 0) {
  198. queueProcessPendingMessages();
  199. }
  200. }
  201. public function deleteFiles(files : Array<FileEntry>) {
  202. //trace(fullPaths);
  203. var roots = getRoots(files);
  204. for (file in roots) {
  205. if( file.kind == Dir ) {
  206. file.dispose(); // kill watchers
  207. untyped js.node.Fs.rmSync(file.getPath(), {force: true, recursive: true});
  208. } else {
  209. file.dispose(); // kill watchers
  210. untyped js.node.Fs.rmSync(file.getPath(), {force: true, recursive: false});
  211. }
  212. }
  213. }
  214. public function getFileEntry(path: String) {
  215. var relPath = hide.Ide.inst.makeRelative(path);
  216. return fileIndex.get(relPath);
  217. }
  218. // Deduplicate paths if they are contained in a directory
  219. // also present in paths, to simplify bulk operations
  220. public function getRoots(files: Array<FileEntry>) : Array<FileEntry> {
  221. var dirs : Array<FileEntry> = [];
  222. for (file in files) {
  223. if(file.kind == Dir) {
  224. dirs.push(file);
  225. }
  226. }
  227. // Find the minimum ammount of files that need to be moved
  228. var roots: Array<FileEntry> = [];
  229. for (file in files) {
  230. var isContainedInAnotherDir = false;
  231. for (dir2 in dirs) {
  232. if (file == dir2)
  233. continue;
  234. if (StringTools.contains(file.getPath(), dir2.getPath())) {
  235. isContainedInAnotherDir = true;
  236. continue;
  237. }
  238. }
  239. if (!isContainedInAnotherDir) {
  240. roots.push(file);
  241. }
  242. }
  243. return roots;
  244. }
  245. function onSVNFileModified(modifiedFiles: Array<String>) {
  246. for (file in fileIndex) {
  247. file.vcsStatus = UpToDate;
  248. }
  249. for (modifiedFile in modifiedFiles) {
  250. var relPath = hide.Ide.inst.getRelPath(modifiedFile);
  251. var file = fileIndex.get(relPath);
  252. while(file != null && file.vcsStatus != Modified) {
  253. file.vcsStatus = Modified;
  254. file = file.parent;
  255. }
  256. }
  257. for (handler in onVCSStatusUpdateHandlers) {
  258. handler();
  259. }
  260. }
  261. /**
  262. Return the path to a temporary file with all the paths in the files array inside
  263. **/
  264. public function createSVNFileList(files: Array<FileEntry>) : String {
  265. var tmpdir = js.node.Os.tmpdir();
  266. var name = 'hidefiles${Std.int(hxd.Math.random(100000000))}.txt';
  267. var path = tmpdir + "/" + name;
  268. var str = [for(f in files) f.getPath()].join("\n");
  269. // Encode paths as utf-16 because tortoiseproc want the file encoded that way
  270. var bytes = haxe.io.Bytes.alloc(str.length * 2);
  271. var pos = 0;
  272. for (char in 0...str.length) {
  273. bytes.setUInt16(pos, str.charCodeAt(char));
  274. pos += 2;
  275. }
  276. sys.io.File.saveBytes(path, bytes);
  277. return path;
  278. }
  279. function setupServer() {
  280. if (serverSocket != null)
  281. throw "Server already exists";
  282. serverSocket = new hxd.net.Socket();
  283. serverSocket.onError = (msg) -> {
  284. hide.Ide.inst.quickError("FileManager socket error : " + msg);
  285. cleanupGenerator();
  286. cleanupServer();
  287. }
  288. serverSocket.bind(thumbnailGeneratorUrl, thumbnailGeneratorPort, (remoteSocket) -> {
  289. if (generatorSocket != null) {
  290. generatorSocket.close();
  291. }
  292. generatorSocket = remoteSocket;
  293. generatorSocket.onError = (msg) -> {
  294. hide.Ide.inst.quickError("Generator socket error : " + msg);
  295. cleanupGenerator();
  296. }
  297. var handler = new hide.tools.ThumbnailGenerator.MessageHandler(generatorSocket, processThumbnailGeneratorMessage);
  298. trace("Thumbnail generator connected");
  299. // resend command that weren't completed
  300. for (path => _ in onReadyCallbacks) {
  301. sendGenerateCommand(path);
  302. }
  303. });
  304. }
  305. function cleanupServer() {
  306. if (serverSocket != null) {
  307. serverSocket.close();
  308. serverSocket = null;
  309. }
  310. }
  311. function cleanupGenerator() {
  312. if (generatorSocket != null) {
  313. generatorSocket.close();
  314. generatorSocket = null;
  315. }
  316. if (windowManager != null && windowManager.generatorWindow != null) {
  317. windowManager.generatorWindow.close(true);
  318. }
  319. windowManager = null;
  320. untyped nw.Window.getAll((win:nw.Window) -> {
  321. if (win.title == "HideThumbnailGenerator") {
  322. win.close(true);
  323. }
  324. });
  325. }
  326. function init() {
  327. // kill server when page is reloaded
  328. js.Browser.window.addEventListener('beforeunload', () -> { cleanupGenerator(); cleanupServer(); });
  329. svnEnabled = hide.Ide.inst.isSVNAvailable();
  330. var exclPatterns : Array<String> = hide.Ide.inst.currentConfig.get("filetree.excludes", []);
  331. ignorePatterns = [];
  332. for(pat in exclPatterns)
  333. ignorePatterns.push(new EReg(pat, "i"));
  334. setupServer();
  335. checkWindowReady();
  336. initFileSystem();
  337. }
  338. function initFileSystem() {
  339. fileEntryRefreshDelay = new Delayer((entry: FileEntry) -> {
  340. entry.refreshChildren();
  341. });
  342. fileRoot = new FileEntry("res", null, Dir);
  343. fileRoot.refreshChildren();
  344. queueRefreshSVN();
  345. }
  346. function fileChangeInternal(entry: FileEntry) {
  347. // invalidate thumbnail
  348. entry.iconPath = null;
  349. if (!js.node.Fs.existsSync(entry.getPath()) && entry.parent != null) {
  350. fileEntryRefreshDelay.queue(entry.parent);
  351. return;
  352. }
  353. if (entry.kind == Dir) {
  354. fileEntryRefreshDelay.queue(entry);
  355. }
  356. queueRefreshSVN();
  357. for (handler in onFileChangeHandlers) {
  358. handler(entry);
  359. }
  360. }
  361. public function queueRefreshSVN() {
  362. if (svnEnabled) {
  363. hide.Ide.inst.getSVNModifiedFiles(onSVNFileModified);
  364. }
  365. }
  366. public function cloneFile(entry: FileEntry) {
  367. var sourcePath = entry.getPath();
  368. var nameNewFile = hide.Ide.inst.ask("New filename:", new haxe.io.Path(sourcePath).file);
  369. if (nameNewFile == null || nameNewFile.length == 0) {
  370. return false;
  371. }
  372. var targetPath = new haxe.io.Path(sourcePath).dir + "/" + nameNewFile;
  373. if ( sys.FileSystem.exists(targetPath) ) {
  374. throw "File already exists";
  375. }
  376. if( sys.FileSystem.isDirectory(sourcePath) ) {
  377. sys.FileSystem.createDirectory(targetPath + "/");
  378. for( f in sys.FileSystem.readDirectory(sourcePath) ) {
  379. sys.io.File.saveBytes(targetPath + "/" + f, sys.io.File.getBytes(sourcePath + "/" + f));
  380. }
  381. } else {
  382. if (targetPath.indexOf(".") == -1) {
  383. var oldExt = sourcePath.split(".").pop();
  384. targetPath += "." + oldExt;
  385. }
  386. sys.io.File.saveBytes(targetPath, sys.io.File.getBytes(sourcePath));
  387. }
  388. return true;
  389. }
  390. function processThumbnailGeneratorMessage(message: String) {
  391. try {
  392. var message = haxe.Json.parse(message);
  393. switch(message.type) {
  394. case success:
  395. var message : GenToManagerSuccessMessage = message.data;
  396. var cb = onReadyCallbacks.get(message.originalPath);
  397. if (cb == null) {
  398. return;
  399. //throw "Generated a thumbnail for a file not registered";
  400. }
  401. cb(message.thumbnailPath);
  402. onReadyCallbacks.remove(message.originalPath);
  403. default:
  404. throw "Unknown message type " + message.type;
  405. }
  406. } catch(e) {
  407. hide.Ide.inst.quickError("Thumb Generator invalid message : " + e + "\n" + message);
  408. }
  409. }
  410. var queued = false;
  411. /**
  412. Asyncrhonusly generates a miniature.
  413. onReady is called back with the path of the loaded miniature, or null if the miniature couldn't be loaded
  414. **/
  415. public function renderMiniature(path: String, onReady: MiniatureReadyCallback) {
  416. if (retries >= maxRetries) {
  417. onReady(null);
  418. return;
  419. }
  420. var ext = path.split(".").pop().toLowerCase();
  421. switch(ext) {
  422. case "prefab" | "fbx" | "l3d" | "fx" | "shgraph" | "jpg" | "jpeg" | "png":
  423. if (!onReadyCallbacks.exists(path)) {
  424. onReadyCallbacks.set(path, onReady);
  425. sendGenerateCommand(path);
  426. }
  427. default:
  428. onReady(null);
  429. }
  430. }
  431. public function invalidateMiniature(file: FileEntry) {
  432. if (file.children != null) {
  433. for (child in file.children) {
  434. invalidateMiniature(child);
  435. }
  436. return;
  437. }
  438. var thumbnail = ThumbnailGenerator.getThumbPath(file.getPath());
  439. try {
  440. sys.FileSystem.deleteFile(thumbnail.toString());
  441. } catch (e) {};
  442. file.iconPath = null;
  443. }
  444. public function checkWindowReady() {
  445. if (serverSocket == null)
  446. return false;
  447. if (windowManager == null) {
  448. if (retries < maxRetries) {
  449. retries ++;
  450. windowManager = new RenderWindowManager();
  451. }
  452. if (retries == maxRetries) {
  453. js.Browser.window.alert("Max retries for thumbnail render window reached");
  454. retries++;
  455. }
  456. return false;
  457. }
  458. if (windowManager.state == Pending) {
  459. return false;
  460. }
  461. if (windowManager.state == Ready && generatorSocket != null) {
  462. return true;
  463. }
  464. return false;
  465. }
  466. public function clearRenderQueue() {
  467. onReadyCallbacks.clear();
  468. if (!checkWindowReady()) {
  469. return;
  470. }
  471. var message = {
  472. type: ManagerToGenCommand.clear,
  473. };
  474. var cmd = haxe.Json.stringify(message) + "\n";
  475. generatorSocket.out.writeString(cmd);
  476. pendingMessages = [];
  477. }
  478. public function setPriority(path: String, newPriority: Int) {
  479. if (!onReadyCallbacks.exists(path)) {
  480. return;
  481. }
  482. if (retries >= maxRetries)
  483. return;
  484. var message = {
  485. type: ManagerToGenCommand.prio,
  486. path: path,
  487. prio: newPriority
  488. };
  489. var cmd = haxe.Json.stringify(message) + "\n";
  490. pendingMessages.push(cmd);
  491. queueProcessPendingMessages();
  492. }
  493. function sendGenerateCommand(path: String) {
  494. if (!checkWindowReady()) {
  495. return;
  496. }
  497. var message = {
  498. type: ManagerToGenCommand.queue,
  499. path: path,
  500. };
  501. var cmd = haxe.Json.stringify(message) + "\n";
  502. pendingMessages.push(cmd);
  503. queueProcessPendingMessages();
  504. }
  505. public static function doRename(path:String, name:String) {
  506. var isDir = sys.FileSystem.isDirectory(hide.Ide.inst.getPath(path));
  507. if( isDir ) hide.Ide.inst.fileWatcher.pause();
  508. var ret = onRenameRec(path, name);
  509. if( isDir ) hide.Ide.inst.fileWatcher.resume();
  510. return ret;
  511. }
  512. public static function onRenameRec(path:String, name:String) {
  513. var ide = hide.Ide.inst;
  514. var parts = path.split("/");
  515. parts.pop();
  516. for( n in name.split("/") ) {
  517. if( n == ".." )
  518. parts.pop();
  519. else
  520. parts.push(n);
  521. }
  522. var newPath = name.charAt(0) == "/" ? name.substr(1) : parts.join("/");
  523. if( newPath == path )
  524. return false;
  525. if( sys.FileSystem.exists(ide.getPath(newPath)) ) {
  526. function addPath(path:String,rand:String) {
  527. var p = path.split(".");
  528. if( p.length > 1 )
  529. p[p.length-2] += rand;
  530. else
  531. p[p.length-1] += rand;
  532. return p.join(".");
  533. }
  534. if( path.toLowerCase() == newPath.toLowerCase() ) {
  535. // case change
  536. var rand = "__tmp"+Std.random(10000);
  537. onRenameRec(path, "/"+addPath(path,rand));
  538. onRenameRec(addPath(path,rand), name);
  539. } else {
  540. if( !ide.confirm(newPath+" already exists, invert files?") )
  541. return false;
  542. var rand = "__tmp"+Std.random(10000);
  543. onRenameRec(path, "/"+addPath(path,rand));
  544. onRenameRec(newPath, "/"+path);
  545. onRenameRec(addPath(path,rand), name);
  546. }
  547. return false;
  548. }
  549. var isDir = sys.FileSystem.isDirectory(ide.getPath(path));
  550. var wasRenamed = false;
  551. var isSVNRepo = sys.FileSystem.exists(ide.projectDir+"/.svn") || js.node.ChildProcess.spawnSync("svn",["info"], { cwd : ide.resourceDir }).status == 0; // handle not root dirs
  552. if( isSVNRepo ) {
  553. if( js.node.ChildProcess.spawnSync("svn",["--version"]).status != 0 ) {
  554. if( isDir && !ide.confirm("Renaming a SVN directory, but 'svn' system command was not found. Continue ?") )
  555. return false;
  556. } else {
  557. // Check if origin file and target directory are versioned
  558. var isFileVersioned = js.node.ChildProcess.spawnSync("svn",["info", ide.getPath(path)]).status == 0;
  559. var newAbsPath = ide.getPath(newPath);
  560. var parentFolder = newAbsPath.substring(0, newAbsPath.lastIndexOf('/'));
  561. var isDirVersioned = js.node.ChildProcess.spawnSync("svn",["info", parentFolder]).status == 0;
  562. if (isFileVersioned && isDirVersioned) {
  563. var cwd = Sys.getCwd();
  564. Sys.setCwd(ide.resourceDir);
  565. var code = Sys.command("svn",["rename", path, newPath]);
  566. Sys.setCwd(cwd);
  567. if( code == 0 )
  568. wasRenamed = true;
  569. else {
  570. if( !ide.confirm("SVN rename failure, perform file rename ?") )
  571. return false;
  572. }
  573. }
  574. }
  575. }
  576. if( !wasRenamed )
  577. sys.FileSystem.rename(ide.getPath(path), ide.getPath(newPath));
  578. replacePathInFiles(path, newPath, isDir);
  579. var dataDir = new haxe.io.Path(path);
  580. if( dataDir.ext != "dat" ) {
  581. dataDir.ext = "dat";
  582. var dataPath = dataDir.toString();
  583. if( sys.FileSystem.isDirectory(ide.getPath(dataPath)) ) {
  584. var destPath = new haxe.io.Path(name);
  585. destPath.ext = "dat";
  586. onRenameRec(dataPath, destPath.toString());
  587. }
  588. }
  589. // update Materials.props if an FBX is moved/renamed
  590. var newSysPath = new haxe.io.Path(name);
  591. var oldSysPath = new haxe.io.Path(path);
  592. if (newSysPath.dir == null) {
  593. newSysPath.dir = oldSysPath.dir;
  594. }
  595. if (newSysPath.ext?.toLowerCase() == "fbx" && oldSysPath.ext?.toLowerCase() == "fbx") {
  596. function remLeadingSlash(s:String) {
  597. if(StringTools.startsWith(s,"/")) {
  598. return s.length > 1 ? s.substr(1) : "";
  599. }
  600. return s;
  601. }
  602. var oldMatPropsPath = ide.getPath(remLeadingSlash((oldSysPath.dir ?? "") + "/materials.props"));
  603. if (sys.FileSystem.exists(oldMatPropsPath)) {
  604. var newMatPropsPath = ide.getPath(remLeadingSlash((newSysPath.dir ?? "") + "/materials.props"));
  605. var oldMatProps = haxe.Json.parse(sys.io.File.getContent(oldMatPropsPath));
  606. var newMatProps : Dynamic =
  607. if (sys.FileSystem.exists(newMatPropsPath))
  608. haxe.Json.parse(sys.io.File.getContent(newMatPropsPath))
  609. else {};
  610. var oldNameExt = oldSysPath.file + "." + oldSysPath.ext;
  611. var newNameExt = newSysPath.file + "." + newSysPath.ext;
  612. function moveRec(originalData: Dynamic, oldData:Dynamic, newData: Dynamic) {
  613. for (field in Reflect.fields(originalData)) {
  614. if (StringTools.endsWith(field, oldNameExt)) {
  615. var innerData = Reflect.getProperty(originalData, field);
  616. var newField = StringTools.replace(field, oldNameExt, newNameExt);
  617. Reflect.setProperty(newData, newField, innerData);
  618. Reflect.deleteField(oldData, field);
  619. }
  620. else {
  621. var originalInner = Reflect.getProperty(originalData, field);
  622. if (Type.typeof(originalInner) != TObject)
  623. continue;
  624. var oldInner = Reflect.getProperty(oldData, field);
  625. var newInner = Reflect.getProperty(newData, field) ?? {};
  626. moveRec(originalInner, oldInner, newInner);
  627. // Avoid creating empty fields
  628. if (Reflect.fields(newInner).length > 0) {
  629. Reflect.setProperty(newData, field, newInner);
  630. }
  631. // Cleanup removed fields in old props
  632. if (Reflect.fields(oldInner).length == 0) {
  633. Reflect.deleteField(oldData, field);
  634. }
  635. }
  636. }
  637. }
  638. var sourceData = oldMatProps;
  639. var oldDataToSave = oldMatPropsPath == newMatPropsPath ? newMatProps : haxe.Json.parse(haxe.Json.stringify(oldMatProps));
  640. moveRec(oldMatProps, oldDataToSave, newMatProps);
  641. sys.io.File.saveContent(newMatPropsPath, haxe.Json.stringify(newMatProps, null, "\t"));
  642. if (oldMatPropsPath != newMatPropsPath) {
  643. if (Reflect.fields(oldMatProps).length > 0) {
  644. sys.io.File.saveContent(oldMatPropsPath, haxe.Json.stringify(oldDataToSave, null, "\t"));
  645. } else {
  646. sys.FileSystem.deleteFile(oldMatPropsPath);
  647. }
  648. }
  649. // Clear caches
  650. @:privateAccess
  651. {
  652. if (h3d.mat.MaterialSetup.current != null) {
  653. h3d.mat.MaterialSetup.current.database.db.remove(ide.makeRelative(oldMatPropsPath));
  654. h3d.mat.MaterialSetup.current.database.db.remove(ide.makeRelative(newMatPropsPath));
  655. }
  656. hxd.res.Loader.currentInstance.cache.remove(ide.makeRelative(oldMatPropsPath));
  657. hxd.res.Loader.currentInstance.cache.remove(ide.makeRelative(newMatPropsPath));
  658. }
  659. }
  660. }
  661. return true;
  662. }
  663. public static function replacePathInFiles(oldPath: String, newPath: String, isDir: Bool = false) {
  664. function filter(ctx: hide.Ide.FilterPathContext) {
  665. var p = ctx.valueCurrent;
  666. if( p == null )
  667. return;
  668. if( p == oldPath ) {
  669. ctx.change(newPath);
  670. return;
  671. }
  672. if( p == "/"+oldPath ) {
  673. ctx.change(newPath);
  674. return;
  675. }
  676. if( isDir ) {
  677. if( StringTools.startsWith(p,oldPath+"/") ) {
  678. ctx.change(newPath + p.substr(oldPath.length));
  679. return;
  680. }
  681. if( StringTools.startsWith(p,"/"+oldPath+"/") ) {
  682. ctx.change("/"+newPath + p.substr(oldPath.length+1));
  683. return;
  684. }
  685. }
  686. }
  687. hide.Ide.inst.filterPaths(filter);
  688. }
  689. }
  690. enum RenderWindowState {
  691. Pending;
  692. Ready;
  693. }
  694. @:allow(hide.tools.FileManager)
  695. @:access(hide.tools.FileManager)
  696. class RenderWindowManager {
  697. var state : RenderWindowState = Pending;
  698. var generatorWindow : nw.Window;
  699. function new() {
  700. state = Pending;
  701. // wait that the browser is idle before creating the rendering window, so
  702. // the generator socket is properly initialised
  703. untyped js.Browser.window.requestIdleCallback(() -> {
  704. state = Ready;
  705. nw.Window.open('app.html?thumbnail=true', cast {
  706. new_instance: true,
  707. show: false,
  708. title: "HideThumbnailGenerator"
  709. }, (win: nw.Window) -> {
  710. generatorWindow = win;
  711. win.on("close", () -> {
  712. hide.Tools.FileManager.cleanupGenerator();
  713. });
  714. });
  715. }, {timeout: 1000});
  716. }
  717. }