From c6d2a566a4622acf9fd0386f1f4f3769ea7b037c Mon Sep 17 00:00:00 2001
From: ArthurHeitmann <37270165+ArthurHeitmann@users.noreply.github.com>
Date: Thu, 5 Sep 2024 19:59:51 +0200
Subject: [PATCH] drag & drop files onto supported text fields

---
 .../openFiles/types/SmdFileData.dart          |  3 +-
 .../openFiles/types/TmdFileData.dart          |  3 +-
 lib/widgets/filesView/fileTabView.dart        |  2 +-
 .../filesView/types/SaveSlotDataEditor.dart   |  6 +-
 .../types/genericTable/tableEditor.dart       | 12 +---
 lib/widgets/filesView/types/wtaWtpEditor.dart |  3 +-
 lib/widgets/layout/searchPanel.dart           |  6 +-
 lib/widgets/misc/dropTargetBuilder.dart       | 18 +++++-
 lib/widgets/misc/imagePreviewBuilder.dart     |  5 +-
 lib/widgets/misc/preferencesEditor.dart       |  8 +--
 .../propEditors/UnderlinePropTextField.dart   |  2 +-
 .../propEditors/primaryPropTextField.dart     |  2 +-
 lib/widgets/propEditors/propTextField.dart    | 61 +++++++++++++++++++
 .../propEditors/transparentPropTextField.dart |  2 +-
 14 files changed, 104 insertions(+), 29 deletions(-)

diff --git a/lib/stateManagement/openFiles/types/SmdFileData.dart b/lib/stateManagement/openFiles/types/SmdFileData.dart
index 7082276b..29b9e9d5 100644
--- a/lib/stateManagement/openFiles/types/SmdFileData.dart
+++ b/lib/stateManagement/openFiles/types/SmdFileData.dart
@@ -7,6 +7,7 @@ import '../../../fileTypeUtils/smd/smdReader.dart';
 import '../../../fileTypeUtils/smd/smdWriter.dart';
 import '../../../widgets/filesView/FileType.dart';
 import '../../../widgets/filesView/types/genericTable/tableEditor.dart';
+import '../../../widgets/propEditors/propTextField.dart';
 import '../../Property.dart';
 import '../../changesExporter.dart';
 import '../../hasUuid.dart';
@@ -165,7 +166,7 @@ class SmdData extends ListNotifier<SmdEntryData> with CustomTableConfig, Undoabl
       key: Key(entry.uuid),
       cells: [
         PropCellConfig(prop: entry.id),
-        PropCellConfig(prop: entry.text, allowMultiline: true),
+        PropCellConfig(prop: entry.text, options: const PropTFOptions(isMultiline: true)),
       ],
     );
   }
diff --git a/lib/stateManagement/openFiles/types/TmdFileData.dart b/lib/stateManagement/openFiles/types/TmdFileData.dart
index f8f87865..bd0b70ab 100644
--- a/lib/stateManagement/openFiles/types/TmdFileData.dart
+++ b/lib/stateManagement/openFiles/types/TmdFileData.dart
@@ -7,6 +7,7 @@ import '../../../fileTypeUtils/tmd/tmdReader.dart';
 import '../../../fileTypeUtils/tmd/tmdWriter.dart';
 import '../../../widgets/filesView/FileType.dart';
 import '../../../widgets/filesView/types/genericTable/tableEditor.dart';
+import '../../../widgets/propEditors/propTextField.dart';
 import '../../Property.dart';
 import '../../changesExporter.dart';
 import '../../hasUuid.dart';
@@ -165,7 +166,7 @@ class TmdData extends ListNotifier<TmdEntryData> with CustomTableConfig, Undoabl
       key: Key(entry.uuid),
       cells: [
         PropCellConfig(prop: entry.id),
-        PropCellConfig(prop: entry.text, allowMultiline: true),
+        PropCellConfig(prop: entry.text, options: const PropTFOptions(isMultiline: true)),
       ],
     );
   }
diff --git a/lib/widgets/filesView/fileTabView.dart b/lib/widgets/filesView/fileTabView.dart
index 05065890..4a8a503c 100644
--- a/lib/widgets/filesView/fileTabView.dart
+++ b/lib/widgets/filesView/fileTabView.dart
@@ -131,9 +131,9 @@ class _FileTabViewState extends ChangeNotifierState<FileTabView> {
       children: [
         for (var (i, file) in widget.viewArea.files.indexed)
           IndexedStackIsVisible(
+            key: Key(file.uuid),
             isVisible: i == currentFileIndex,
             child: ConstrainedBox(
-              key: Key(file.uuid),
               constraints: const BoxConstraints.expand(),
               child: FocusTraversalGroup(
                 child: makeFileEditor(file),
diff --git a/lib/widgets/filesView/types/SaveSlotDataEditor.dart b/lib/widgets/filesView/types/SaveSlotDataEditor.dart
index 4864f328..c3de265c 100644
--- a/lib/widgets/filesView/types/SaveSlotDataEditor.dart
+++ b/lib/widgets/filesView/types/SaveSlotDataEditor.dart
@@ -200,7 +200,7 @@ class _InventoryTableConfig with CustomTableConfig {
     key: Key(items[i].uuid),
     cells: [
       TextCellConfig(i.toString()),
-      PropCellConfig(prop: items[i].id, autocompleteOptions: _getItemIdAutocomplete),
+      PropCellConfig(prop: items[i].id, options: const PropTFOptions(autocompleteOptions: _getItemIdAutocomplete)),
       PropCellConfig(prop: items[i].count),
       PropCellConfig(prop: items[i].isActive),
     ]
@@ -279,7 +279,7 @@ class _WeaponTableConfig with CustomTableConfig {
     key: Key(weapons[i].uuid),
     cells: [
       TextCellConfig(weapons[i].index.toString().padLeft(2, " ") + (i < _weaponsByIndex.length ? " (${_weaponsByIndex[i].item2})" : "")),
-      PropCellConfig(prop: weapons[i].id, autocompleteOptions: _getWeaponIdAutocomplete),
+      PropCellConfig(prop: weapons[i].id, options: const PropTFOptions(autocompleteOptions: _getWeaponIdAutocomplete)),
       PropCellConfig(prop: weapons[i].level),
       PropCellConfig(prop: weapons[i].isNew),
       PropCellConfig(prop: weapons[i].hasNewStory),
@@ -394,7 +394,7 @@ class _StringListTableConfig with CustomTableConfig {
       cells: [
         PropCellConfig(
           prop: strings[index].text,
-          autocompleteOptions: autocompleteOptions,
+          options: PropTFOptions(autocompleteOptions: autocompleteOptions),
         ),
         CustomWidgetCellConfig(
           IconButton(
diff --git a/lib/widgets/filesView/types/genericTable/tableEditor.dart b/lib/widgets/filesView/types/genericTable/tableEditor.dart
index 06b7f601..df2a9a08 100644
--- a/lib/widgets/filesView/types/genericTable/tableEditor.dart
+++ b/lib/widgets/filesView/types/genericTable/tableEditor.dart
@@ -1,6 +1,4 @@
 
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 
 import '../../../../stateManagement/Property.dart';
@@ -11,7 +9,6 @@ import '../../../misc/nestedContextMenu.dart';
 import '../../../propEditors/UnderlinePropTextField.dart';
 import '../../../propEditors/propEditorFactory.dart';
 import '../../../propEditors/propTextField.dart';
-import '../../../propEditors/textFieldAutocomplete.dart';
 import '../../../propEditors/transparentPropTextField.dart';
 import '../../../theme/customTheme.dart';
 import 'tableExporter.dart';
@@ -23,19 +20,16 @@ abstract class CellConfig {
 
 class PropCellConfig extends CellConfig {
   final Prop prop;
-  final bool allowMultiline;
-  final FutureOr<Iterable<AutocompleteConfig>> Function()? autocompleteOptions;
+  final PropTFOptions options;
 
-  PropCellConfig({ required this.prop, this.allowMultiline = false, this.autocompleteOptions });
+  PropCellConfig({ required this.prop, this.options = const PropTFOptions() });
 
   @override
   Widget makeWidget() => makePropEditor<TransparentPropTextField>(
     prop,
-    PropTFOptions(
+    options.copyWith(
       constraints: const BoxConstraints(minWidth: double.infinity, minHeight: 30),
-      isMultiline: allowMultiline,
       useIntrinsicWidth: false,
-      autocompleteOptions: autocompleteOptions,
     ),
   );
 
diff --git a/lib/widgets/filesView/types/wtaWtpEditor.dart b/lib/widgets/filesView/types/wtaWtpEditor.dart
index 2261d451..95df309b 100644
--- a/lib/widgets/filesView/types/wtaWtpEditor.dart
+++ b/lib/widgets/filesView/types/wtaWtpEditor.dart
@@ -9,6 +9,7 @@ import '../../../stateManagement/Property.dart';
 import '../../../stateManagement/listNotifier.dart';
 import '../../../stateManagement/openFiles/types/WtaWtpData.dart';
 import '../../../utils/utils.dart';
+import '../../propEditors/propTextField.dart';
 import '../../theme/customTheme.dart';
 import 'genericTable/tableEditor.dart';
 
@@ -53,7 +54,7 @@ class _TexturesTableConfig with CustomTableConfig {
       key: Key(textures[index].uuid),
       cells: [
         PropCellConfig(prop: textures[index].id),
-        PropCellConfig(prop: textures[index].path),
+        PropCellConfig(prop: textures[index].path, options: const PropTFOptions(isFilePath: true)),
         CustomWidgetCellConfig(IconButton(
           icon: const Icon(Icons.folder, size: 20),
           onPressed: () => _selectTexture(index),
diff --git a/lib/widgets/layout/searchPanel.dart b/lib/widgets/layout/searchPanel.dart
index 6c2d8abc..336d7c81 100644
--- a/lib/widgets/layout/searchPanel.dart
+++ b/lib/widgets/layout/searchPanel.dart
@@ -315,7 +315,7 @@ class _SearchPanelState extends State<SearchPanel> {
             const SizedBox()
           ],
         ),
-        makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false)),
+        makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false, isFolderPath: true)),
         makePropEditor(extensions, const PropTFOptions(hintText: "Extensions (.xml, .rb, ...)", useIntrinsicWidth: false)),
       ].map((e) => Padding(
         padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
@@ -346,7 +346,7 @@ class _SearchPanelState extends State<SearchPanel> {
               ],
             ),
             if (!useIndexedData.value) ...[
-              makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false)),
+              makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false, isFolderPath: true)),
             ]
           ].map((e) => Padding(
             padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
@@ -360,7 +360,7 @@ class _SearchPanelState extends State<SearchPanel> {
   Widget _makeEstSearchOptions() {
     return Column(
       children: [
-        makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false)),
+        makePropEditor(path, const PropTFOptions(hintText: "Path", useIntrinsicWidth: false, isFolderPath: true)),
         ...estOptionsNamed.map((opt) => RowSeparated(
           crossAxisAlignment: CrossAxisAlignment.center,
           separatorWidth: 5,
diff --git a/lib/widgets/misc/dropTargetBuilder.dart b/lib/widgets/misc/dropTargetBuilder.dart
index 13ea35d4..0313a4c4 100644
--- a/lib/widgets/misc/dropTargetBuilder.dart
+++ b/lib/widgets/misc/dropTargetBuilder.dart
@@ -1,4 +1,6 @@
 
+import 'dart:math';
+
 import 'package:desktop_drop/desktop_drop.dart';
 import 'package:flutter/material.dart';
 
@@ -15,15 +17,27 @@ class DropTargetBuilder extends StatefulWidget {
 }
 
 class _DropTargetBuilderState extends State<DropTargetBuilder> {
+  static int _dropping = 0;
+
   bool isDropping = false;
 
   @override
   Widget build(BuildContext context) {
     return DropTarget(
       enable: ModalRoute.of(context)!.isCurrent && IndexedStackIsVisible.of(context) != false,
-      onDragEntered: (_) => setState(() => isDropping = true),
-      onDragExited: (_) => setState(() => isDropping = false),
+      onDragEntered: (_) {
+        _dropping += 1;
+        isDropping = true;
+        setState(() {});
+      },
+      onDragExited: (_) {
+        _dropping = max(0, _dropping - 1);
+        isDropping = false;
+        setState(() {});
+      },
       onDragDone: (details) {
+        if (_dropping > 0)
+          return;
         widget.onDrop(details.files.map((f) => f.path).toList());
       },
       child: widget.builder(context, isDropping),
diff --git a/lib/widgets/misc/imagePreviewBuilder.dart b/lib/widgets/misc/imagePreviewBuilder.dart
index c4ddc5ec..cd7b9691 100644
--- a/lib/widgets/misc/imagePreviewBuilder.dart
+++ b/lib/widgets/misc/imagePreviewBuilder.dart
@@ -5,6 +5,7 @@ import 'dart:typed_data';
 import 'package:flutter/material.dart';
 
 import '../../fileTypeUtils/textures/ddsConverter.dart';
+import '../../utils/utils.dart';
 
 enum ImagePreviewState {
   loading,
@@ -57,7 +58,9 @@ class _ImagePreviewBuilderState extends State<ImagePreviewBuilder> {
     exists = true;
     var sw = Stopwatch()..start();
     image = texToPng(widget.path, maxHeight: widget.maxHeight, verbose: false)
-      ..then((_) => print("Loaded image in ${sw.elapsedMilliseconds}ms"));
+      ..then((_) {
+        debugOnly(() => print("Loaded image in ${sw.elapsedMilliseconds}ms"));
+      });
     setState(() {});
   }
 
diff --git a/lib/widgets/misc/preferencesEditor.dart b/lib/widgets/misc/preferencesEditor.dart
index ea7f4ddc..5b6b0edf 100644
--- a/lib/widgets/misc/preferencesEditor.dart
+++ b/lib/widgets/misc/preferencesEditor.dart
@@ -90,7 +90,7 @@ class _PreferencesEditorState extends ChangeNotifierState<PreferencesEditor> {
               prop: exportPathProp,
               onValid: (value) => exportPathProp.value = value,
               validatorOnChange: (value) => value.isEmpty || Directory(value).existsSync() ? null : "Directory does not exist",
-              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30)),
+              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30), isFolderPath: true),
             ),
           ),
           makeFilePickerButton(
@@ -162,7 +162,7 @@ class _PreferencesEditorState extends ChangeNotifierState<PreferencesEditor> {
                 prop: path,
                 onValid: (value) => widget.prefs.indexingPaths!.setPath(path, value),
                 validatorOnChange: (value) => Directory(value).existsSync() ? null : "Directory does not exist",
-                options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30)),
+                options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30), isFolderPath: true),
               ),
             ),
             makeFilePickerButton(
@@ -263,7 +263,7 @@ class _PreferencesEditorState extends ChangeNotifierState<PreferencesEditor> {
               prop: widget.prefs.waiExtractDir!,
               onValid: (value) => widget.prefs.waiExtractDir!.value = value,
               validatorOnChange: (value) => value.isEmpty || Directory(value).existsSync() ? null : "Directory does not exist",
-              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30)),
+              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30), isFolderPath: true),
             ),
           ),
           makeFilePickerButton(
@@ -282,7 +282,7 @@ class _PreferencesEditorState extends ChangeNotifierState<PreferencesEditor> {
               prop: widget.prefs.wwiseCliPath!,
               onValid: (value) => widget.prefs.wwiseCliPath!.value = value.isNotEmpty ? findWwiseCliExe(value)! : "",
               validatorOnChange: (value) => value.isEmpty || findWwiseCliExe(value) != null ? null : "Directory does not exist",
-              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30)),
+              options: const PropTFOptions(constraints: BoxConstraints(minHeight: 30), isFolderPath: true, isFilePath: true),
             ),
           ),
           makeFilePickerButton(
diff --git a/lib/widgets/propEditors/UnderlinePropTextField.dart b/lib/widgets/propEditors/UnderlinePropTextField.dart
index a8bae3bb..a90b4c03 100644
--- a/lib/widgets/propEditors/UnderlinePropTextField.dart
+++ b/lib/widgets/propEditors/UnderlinePropTextField.dart
@@ -33,7 +33,7 @@ class _UnderlinePropTextFieldState extends PropTextFieldState {
           decoration: BoxDecoration(
             border: Border(bottom: BorderSide(color: getTheme(context).propBorderColor!, width: 2)),
           ),
-          child: intrinsicWidthWrapper(
+          child: applyWrappers(
             child: ConstrainedBox(
               constraints: widget.options.constraints,
               child: Row(
diff --git a/lib/widgets/propEditors/primaryPropTextField.dart b/lib/widgets/propEditors/primaryPropTextField.dart
index bf2f9150..68d75bab 100644
--- a/lib/widgets/propEditors/primaryPropTextField.dart
+++ b/lib/widgets/propEditors/primaryPropTextField.dart
@@ -33,7 +33,7 @@ class _PrimaryPropTextFieldState extends PropTextFieldState {
         child: Material(
           color: getTheme(context).formElementBgColor,
           borderRadius: BorderRadius.circular(8.0),
-          child: intrinsicWidthWrapper(
+          child: applyWrappers(
             child: ConstrainedBox(
               constraints: widget.options.constraints,
               child: Row(
diff --git a/lib/widgets/propEditors/propTextField.dart b/lib/widgets/propEditors/propTextField.dart
index 4f793cdb..761e8720 100644
--- a/lib/widgets/propEditors/propTextField.dart
+++ b/lib/widgets/propEditors/propTextField.dart
@@ -1,5 +1,6 @@
 
 import 'dart:async';
+import 'dart:io';
 
 import 'package:flutter/material.dart';
 
@@ -7,6 +8,8 @@ import '../../stateManagement/Property.dart';
 import '../../utils/utils.dart';
 import '../misc/ChangeNotifierWidget.dart';
 import '../misc/TextFieldFocusNode.dart';
+import '../misc/dropTargetBuilder.dart';
+import '../theme/customTheme.dart';
 import 'DoubleClickablePropTextField.dart';
 import 'UnderlinePropTextField.dart';
 import 'primaryPropTextField.dart';
@@ -18,6 +21,8 @@ class PropTFOptions {
   final BoxConstraints constraints;
   final bool isMultiline;
   final bool useIntrinsicWidth;
+  final bool isFolderPath;
+  final bool isFilePath;
   final String? hintText;
   final FutureOr<Iterable<AutocompleteConfig>> Function()? autocompleteOptions;
 
@@ -26,6 +31,8 @@ class PropTFOptions {
     this.constraints = const BoxConstraints(minWidth: 50),
     this.isMultiline = false,
     this.useIntrinsicWidth = true,
+    this.isFolderPath = false,
+    this.isFilePath = false,
     this.hintText,
     this.autocompleteOptions,
   });
@@ -35,6 +42,8 @@ class PropTFOptions {
     BoxConstraints? constraints,
     bool? isMultiline,
     bool? useIntrinsicWidth,
+    bool? isFolderPath,
+    bool? isFilePath,
     String? hintText,
     FutureOr<Iterable<AutocompleteConfig>> Function()? autocompleteOptions,
   }) {
@@ -43,6 +52,8 @@ class PropTFOptions {
       constraints: constraints ?? this.constraints,
       isMultiline: isMultiline ?? this.isMultiline,
       useIntrinsicWidth: useIntrinsicWidth ?? this.useIntrinsicWidth,
+      isFolderPath: isFolderPath ?? this.isFolderPath,
+      isFilePath: isFilePath ?? this.isFilePath,
       hintText: hintText ?? this.hintText,
       autocompleteOptions: autocompleteOptions ?? this.autocompleteOptions,
     );
@@ -178,7 +189,57 @@ abstract class PropTextFieldState<P extends Prop> extends ChangeNotifierState<Pr
     onValid(text);
   }
 
+  Widget applyWrappers({ required Widget child }) {
+    return pathDropTargetWrapper(
+      child: intrinsicWidthWrapper(
+        child: child,
+      ),
+    );
+  }
+  
   Widget intrinsicWidthWrapper({ required Widget child }) => widget.options.useIntrinsicWidth
     ? IntrinsicWidth(child: child)
     : child;
+  
+  Widget pathDropTargetWrapper({ required Widget child }) => widget.options.isFolderPath || widget.options.isFilePath
+    ? DropTargetBuilder(
+        onDrop: (files) async {
+          var file = files.first;
+          if (!widget.options.isFolderPath && await Directory(file).exists()) {
+            showToast("Expected a file, not a folder");
+            return;
+          }
+          if (!widget.options.isFilePath && await File(file).exists()) {
+            showToast("Expected a folder, not a file");
+            return;
+          }
+            
+          controller.text = file;
+          onValid(file);
+        },
+        builder: (context, isDropping) => Stack(
+          children: [
+            child,
+            if (isDropping)
+              Positioned.fill(
+                child: Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: DecoratedBox(
+                    decoration: BoxDecoration(
+                      boxShadow: [
+                        BoxShadow(
+                          color: getTheme(context).editorBackgroundColor!.withOpacity(0.75),
+                          blurRadius: 10,
+                          spreadRadius: 5,
+                        ),
+                      ],
+                    ),
+                  ),
+                ),
+              ),
+          ],
+        ),
+      )
+    : child;
+
 }
diff --git a/lib/widgets/propEditors/transparentPropTextField.dart b/lib/widgets/propEditors/transparentPropTextField.dart
index 4c0822e7..48f39248 100644
--- a/lib/widgets/propEditors/transparentPropTextField.dart
+++ b/lib/widgets/propEditors/transparentPropTextField.dart
@@ -30,7 +30,7 @@ class _TransparentPropTextFieldState extends PropTextFieldState {
       notifier: widget.prop,
       builder: (context) => Padding(
         padding: const EdgeInsets.symmetric(vertical: 3),
-        child: intrinsicWidthWrapper(
+        child: applyWrappers(
           child: ConstrainedBox(
             constraints: widget.options.constraints,
             child: Row(