This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new b123f5d852 Add option to open files as text, make clear which files 
can be opened, fixes #6529 (#6534)
b123f5d852 is described below

commit b123f5d8523e2e8d17eb730742e9c7bbd92da362
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Feb 10 12:30:12 2026 +0100

    Add option to open files as text, make clear which files can be opened, 
fixes #6529 (#6534)
---
 .../apache/hop/core/util/BinaryDetectionUtil.java  |  66 +++++++++
 .../main/java/org/apache/hop/git/GitGuiPlugin.java |  33 ++++-
 .../perspective/explorer/file/ParquetFileType.java |   5 +
 .../perspective/explorer/file/ExcelFileType.java   |   5 +
 .../transforms/types/HtmlOpenAsTextPlugin.java     |  48 +++----
 .../main/java/org/apache/hop/ui/core/PropsUi.java  |   7 +
 .../org/apache/hop/ui/core/gui/GuiMenuWidgets.java |  26 +++-
 .../org/apache/hop/ui/core/gui/GuiResource.java    |  15 +++
 .../apache/hop/ui/core/gui/GuiToolbarWidgets.java  |  25 +++-
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java |  61 ++++++---
 .../ui/hopgui/delegates/HopGuiFileDelegate.java    |   6 +-
 .../apache/hop/ui/hopgui/file/IHopFileType.java    |  10 ++
 .../hop/ui/hopgui/file/IHopFileTypeHandler.java    |  11 ++
 .../hop/ui/hopgui/file/empty/EmptyFileType.java    |   5 +
 .../perspective/explorer/ExplorerPerspective.java  |  35 ++++-
 .../explorer/file/types/ArchiveFileType.java       |   5 +
 .../explorer/file/types/FolderFileType.java        |   5 +
 .../explorer/file/types/GenericFileType.java       |   5 +
 .../types/base/BaseExplorerFileTypeHandler.java    |   3 +-
 .../RawExplorerFileType.java}                      |  94 ++++---------
 .../RawExplorerFileTypeHandler.java}               | 150 +++++++++++----------
 .../text/BaseTextExplorerFileTypeHandler.java      |   2 +-
 22 files changed, 416 insertions(+), 206 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/util/BinaryDetectionUtil.java 
b/core/src/main/java/org/apache/hop/core/util/BinaryDetectionUtil.java
new file mode 100644
index 0000000000..39c49ed5ae
--- /dev/null
+++ b/core/src/main/java/org/apache/hop/core/util/BinaryDetectionUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Simple heuristic to detect if file content looks like text (Option A: 
presence of null byte
+ * indicates binary).
+ */
+public final class BinaryDetectionUtil {
+
+  private static final int DEFAULT_MAX_BYTES = 8192;
+
+  private BinaryDetectionUtil() {}
+
+  /**
+   * Returns true if the content appears to be text (no null byte in the 
sampled bytes). Returns
+   * false if a null byte is found or on I/O error (treat as binary to be 
safe).
+   *
+   * @param inputStream stream to sample (will be read from; caller is 
responsible for closing)
+   * @param maxBytesToSample maximum number of bytes to read (e.g. 8192)
+   * @return true if no null byte was found in the first maxBytesToSample 
bytes, false otherwise
+   */
+  public static boolean looksLikeText(InputStream inputStream, int 
maxBytesToSample) {
+    if (inputStream == null || maxBytesToSample <= 0) {
+      return false;
+    }
+    byte[] buf = new byte[Math.min(maxBytesToSample, 65536)];
+    try {
+      int n = inputStream.read(buf, 0, buf.length);
+      if (n <= 0) {
+        return true;
+      }
+      for (int i = 0; i < n; i++) {
+        if (buf[i] == 0) {
+          return false;
+        }
+      }
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  /** Same as {@link #looksLikeText(InputStream, int)} with a default sample 
size of 8192 bytes. */
+  public static boolean looksLikeText(InputStream inputStream) {
+    return looksLikeText(inputStream, DEFAULT_MAX_BYTES);
+  }
+}
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
index 72c437050a..1f5434bfac 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitGuiPlugin.java
@@ -124,6 +124,14 @@ public class GitGuiPlugin
   private final Color colorStagedModify;
   private final Color colorUnstaged;
 
+  /** Muted variants when the explorer has already grayed the item 
(non-openable file). */
+  private final Color colorStagedAddGray;
+
+  private final Color colorStagedModifyGray;
+  private final Color colorUnstagedGray;
+  private final Color colorIgnoredGray;
+  private final Color colorStagedUnchangedGray;
+
   public static GitGuiPlugin getInstance() {
     if (instance == null) {
       instance = new GitGuiPlugin();
@@ -146,6 +154,12 @@ public class GitGuiPlugin
     colorStagedUnchanged = GuiResource.getInstance().getColorBlack();
     colorStagedAdd = GuiResource.getInstance().getColorDarkGreen();
 
+    colorStagedAddGray = GuiResource.getInstance().getColorDarkGreenMuted();
+    colorStagedModifyGray = GuiResource.getInstance().getColorLightBlueMuted();
+    colorUnstagedGray = GuiResource.getInstance().getColorRedMuted();
+    colorIgnoredGray = GuiResource.getInstance().getColorDarkGrayMuted();
+    colorStagedUnchangedGray = GuiResource.getInstance().getColorBlackMuted();
+
     refreshChangedFiles();
   }
 
@@ -866,6 +880,11 @@ public class GitGuiPlugin
     // Normalize path
     String absolutePath = getAbsoluteFilename(path);
 
+    // Use gray variants when the explorer has already grayed this item 
(non-openable file)
+    Color systemDarkGray = 
tree.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+    Color currentFg = treeItem.getForeground();
+    boolean useGrayVariants = currentFg != null && 
currentFg.equals(systemDarkGray);
+
     // Changed git file colored blue
     UIFile file = changedFiles.get(absolutePath);
     if (file != null) {
@@ -873,18 +892,24 @@ public class GitGuiPlugin
         case ADD:
         case COPY:
         case RENAME:
-          treeItem.setForeground(file.isStaged() ? colorStagedAdd : 
colorUnstaged);
+          treeItem.setForeground(
+              file.isStaged()
+                  ? (useGrayVariants ? colorStagedAddGray : colorStagedAdd)
+                  : (useGrayVariants ? colorUnstagedGray : colorUnstaged));
           break;
         case MODIFY:
-          treeItem.setForeground(file.isStaged() ? colorStagedModify : 
colorUnstaged);
+          treeItem.setForeground(
+              file.isStaged()
+                  ? (useGrayVariants ? colorStagedModifyGray : 
colorStagedModify)
+                  : (useGrayVariants ? colorUnstagedGray : colorUnstaged));
           break;
         case DELETE:
-          treeItem.setForeground(colorStagedUnchanged);
+          treeItem.setForeground(useGrayVariants ? colorStagedUnchangedGray : 
colorStagedUnchanged);
       }
     }
 
     if (ignoredFiles.containsKey(absolutePath)) {
-      treeItem.setForeground(colorIgnored);
+      treeItem.setForeground(useGrayVariants ? colorIgnoredGray : 
colorIgnored);
     }
   }
 
diff --git 
a/plugins/tech/parquet/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ParquetFileType.java
 
b/plugins/tech/parquet/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ParquetFileType.java
index d56ee868c7..09f7cbe5e2 100644
--- 
a/plugins/tech/parquet/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ParquetFileType.java
+++ 
b/plugins/tech/parquet/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ParquetFileType.java
@@ -125,4 +125,9 @@ public class ParquetFileType implements IHopFileType {
   public String getFileTypeImage() {
     return getClass().getAnnotation(HopFileTypePlugin.class).image();
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/plugins/transforms/excel/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ExcelFileType.java
 
b/plugins/transforms/excel/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ExcelFileType.java
index ddca3764cb..cf0c617caa 100644
--- 
a/plugins/transforms/excel/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ExcelFileType.java
+++ 
b/plugins/transforms/excel/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/ExcelFileType.java
@@ -125,4 +125,9 @@ public class ExcelFileType implements IHopFileType {
   public String getFileTypeImage() {
     return getClass().getAnnotation(HopFileTypePlugin.class).image();
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/types/HtmlOpenAsTextPlugin.java
 
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/types/HtmlOpenAsTextPlugin.java
index 2d952e800b..019d416e6a 100644
--- 
a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/types/HtmlOpenAsTextPlugin.java
+++ 
b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/types/HtmlOpenAsTextPlugin.java
@@ -21,23 +21,20 @@ import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.gui.plugin.menu.GuiMenuElement;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.ui.hopgui.HopGui;
-import org.apache.hop.ui.hopgui.file.HopFileTypeRegistry;
-import org.apache.hop.ui.hopgui.file.IHopFileType;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
-import org.apache.hop.ui.hopgui.perspective.explorer.file.IExplorerFileType;
+import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.raw.RawExplorerFileType;
 
 @GuiPlugin
 public class HtmlOpenAsTextPlugin {
 
   @GuiMenuElement(
       root = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
-      id = "ExplorerPerspective-Html-OpenAsText",
+      id = ExplorerPerspective.CONTEXT_MENU_OPEN_AS_TEXT,
       label =
           
"i18n:org.apache.hop.ui.hopgui.perspective.explorer:ExplorerPerspective.ToolbarElement.OpenAsText.Label",
       image = "textfile.svg",
-      parentId = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
-      separator = true)
+      parentId = ExplorerPerspective.GUI_PLUGIN_CONTEXT_MENU_PARENT_ID)
   public void openAsText() {
     ExplorerPerspective perspective = ExplorerPerspective.getInstance();
     ExplorerFile explorerFile = perspective.getSelectedFile();
@@ -47,38 +44,23 @@ public class HtmlOpenAsTextPlugin {
     }
 
     String filename = explorerFile.getFilename();
-    if (filename == null || !filename.toLowerCase().endsWith(".html")) {
+    if (filename == null) {
       return;
     }
 
     try {
-      HopGui hopGui = HopGui.getInstance();
-
-      // Find the text file type handler (by using a dummy .txt filename)
-      IHopFileType hopFileType = 
HopFileTypeRegistry.getInstance().findHopFileType("dummy.txt");
-
-      if (hopFileType instanceof IExplorerFileType) {
-        IExplorerFileType textFileType = (IExplorerFileType) hopFileType;
-
-        // Create a new ExplorerFile structure but force it to be the Text 
file type
-        ExplorerFile textExplorerFile = new ExplorerFile();
-        // Use the file URI to ensure a unique string key for the tab, but 
pointing to
-        // the same physical file
-        // This avoids the "Duplicate Tab" check in ExplorerPerspective while 
using a
-        // valid file path that won't crash Hop
-        String uniqueName = HopVfs.getFileObject(filename).getName().getURI();
-        textExplorerFile.setFilename(uniqueName);
-        textExplorerFile.setName(explorerFile.getName() + " (Text)");
-        textExplorerFile.setFileType(textFileType);
-
-        // Create the handler directly - BaseTextExplorerFileTypeHandler 
handles VFS
-        // URIs correctly
-        TextExplorerFileTypeHandler handler =
-            new TextExplorerFileTypeHandler(hopGui, perspective, 
textExplorerFile);
-
-        // Add to perspective (this will open a new tab due to unique URI 
string)
-        perspective.addFile(handler);
+      if (HopVfs.getFileObject(filename).isFolder()) {
+        return;
       }
+    } catch (Exception e) {
+      HopGui.getInstance().getLog().logError("Error resolving selected item", 
e);
+      return;
+    }
+
+    try {
+      HopGui hopGui = HopGui.getInstance();
+      RawExplorerFileType rawFileType = new RawExplorerFileType();
+      rawFileType.openFile(hopGui, filename, hopGui.getVariables());
     } catch (Exception e) {
       HopGui.getInstance().getLog().logError("Error opening file as text", e);
     }
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java 
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index 0a352e0726..1288fd77b2 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -1054,6 +1054,13 @@ public class PropsUi extends Props {
     contrastingColors.put(new RGB(100, 100, 100), new RGB(215, 215, 215));
     contrastingColors.put(new RGB(50, 50, 50), new RGB(235, 235, 235));
 
+    // Muted color variants for explorer (non-openable items); dark mode = 
lighter on dark bg
+    contrastingColors.put(new RGB(85, 115, 85), new RGB(120, 155, 120)); // 
DarkGreenMuted
+    contrastingColors.put(new RGB(75, 95, 165), new RGB(130, 150, 215)); // 
LightBlueMuted
+    contrastingColors.put(new RGB(130, 85, 85), new RGB(175, 120, 120)); // 
RedMuted
+    contrastingColors.put(new RGB(105, 105, 105), new RGB(145, 145, 145)); // 
DarkGrayMuted
+    contrastingColors.put(new RGB(90, 90, 90), new RGB(135, 135, 135)); // 
BlackMuted
+
     // Add all the inverse color mappings as well
     //
     Map<RGB, RGB> inverse = new HashMap<>();
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
index 4b7581a8a7..17cdd43d43 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
@@ -31,6 +31,7 @@ import org.apache.hop.core.gui.plugin.menu.GuiMenuItem;
 import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.hopgui.file.IHopFileType;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.widgets.Menu;
@@ -320,7 +321,12 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
    * @return The menu item or null if nothing is found
    */
   public MenuItem enableMenuItem(IHopFileType fileType, String id, String 
permission) {
-    return enableMenuItem(fileType, id, permission, true);
+    return enableMenuItem(fileType, null, id, permission, true);
+  }
+
+  public MenuItem enableMenuItem(
+      IHopFileType fileType, IHopFileTypeHandler handler, String id, String 
permission) {
+    return enableMenuItem(fileType, handler, id, permission, true);
   }
 
   /**
@@ -335,9 +341,23 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
    */
   public MenuItem enableMenuItem(
       IHopFileType fileType, String id, String permission, boolean active) {
-    MenuItem menuItem = menuItemMap.get(id);
+    return enableMenuItem(fileType, null, id, permission, active);
+  }
 
-    boolean hasCapability = fileType.hasCapability(permission);
+  /**
+   * Enable or disable menu item based on capability. When handler is 
non-null, uses handler's
+   * hasCapability (so handlers can disable e.g. Save for binary raw view); 
otherwise uses file
+   * type.
+   */
+  public MenuItem enableMenuItem(
+      IHopFileType fileType,
+      IHopFileTypeHandler handler,
+      String id,
+      String permission,
+      boolean active) {
+    MenuItem menuItem = menuItemMap.get(id);
+    boolean hasCapability =
+        handler != null ? handler.hasCapability(permission) : 
fileType.hasCapability(permission);
     boolean enable = hasCapability && active;
     if (menuItem != null && enable != menuItem.isEnabled()) {
       menuItem.setEnabled(enable);
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 219c31c70c..188dddd6fc 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -107,6 +107,14 @@ public class GuiResource {
   @Getter private Color colorHopTrue;
   @Getter private Color colorDeprecated;
 
+  /** Muted variants for use on grayed (non-openable) explorer items; light 
and dark mode aware. */
+  @Getter private Color colorDarkGreenMuted;
+
+  @Getter private Color colorLightBlueMuted;
+  @Getter private Color colorRedMuted;
+  @Getter private Color colorDarkGrayMuted;
+  @Getter private Color colorBlackMuted;
+
   // Fonts
   //
   private ManagedFont fontDefault;
@@ -384,6 +392,13 @@ public class GuiResource {
     colorHopTrue = new Color(display, props.contrastColor(12, 178, 15));
     colorDeprecated = new Color(display, props.contrastColor(246, 196, 56));
 
+    // Muted variants for grayed (non-openable) explorer items; dark mode 
variants in PropsUi
+    colorDarkGreenMuted = new Color(display, props.contrastColor(new RGB(85, 
115, 85)));
+    colorLightBlueMuted = new Color(display, props.contrastColor(new RGB(75, 
95, 165)));
+    colorRedMuted = new Color(display, props.contrastColor(new RGB(130, 85, 
85)));
+    colorDarkGrayMuted = new Color(display, props.contrastColor(new RGB(105, 
105, 105)));
+    colorBlackMuted = new Color(display, props.contrastColor(new RGB(90, 90, 
90)));
+
     // Load all images from files...
     loadFonts();
     loadCommonImages();
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
index 587978fce6..cde9d9150d 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiToolbarWidgets.java
@@ -42,6 +42,7 @@ import org.apache.hop.ui.core.widget.svg.SvgLabelListener;
 import org.apache.hop.ui.hopgui.TextSizeUtilFacade;
 import org.apache.hop.ui.hopgui.ToolbarFacade;
 import org.apache.hop.ui.hopgui.file.IHopFileType;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.util.EnvironmentUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CLabel;
@@ -702,7 +703,12 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
    * @return The toolbar item or null if nothing is found
    */
   public ToolItem enableToolbarItem(IHopFileType fileType, String id, String 
permission) {
-    return enableToolbarItem(fileType, id, permission, true);
+    return enableToolbarItem(fileType, null, id, permission, true);
+  }
+
+  public ToolItem enableToolbarItem(
+      IHopFileType fileType, IHopFileTypeHandler handler, String id, String 
permission) {
+    return enableToolbarItem(fileType, handler, id, permission, true);
   }
 
   /**
@@ -717,8 +723,23 @@ public class GuiToolbarWidgets extends BaseGuiWidgets 
implements IToolbarWidgetR
    */
   public ToolItem enableToolbarItem(
       IHopFileType fileType, String id, String permission, boolean active) {
+    return enableToolbarItem(fileType, null, id, permission, active);
+  }
+
+  /**
+   * Enable or disable toolbar item based on capability. When handler is 
non-null, uses handler's
+   * hasCapability (so handlers can disable e.g. Save for binary raw view); 
otherwise uses file
+   * type.
+   */
+  public ToolItem enableToolbarItem(
+      IHopFileType fileType,
+      IHopFileTypeHandler handler,
+      String id,
+      String permission,
+      boolean active) {
     ToolItem item = findToolItem(id);
-    boolean hasCapability = fileType.hasCapability(permission);
+    boolean hasCapability =
+        handler != null ? handler.hasCapability(permission) : 
fileType.hasCapability(permission);
     boolean enable = hasCapability && active;
 
     if (item == null) {
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 7916529226..af27e96f05 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -1376,57 +1376,80 @@ public class HopGui
    */
   public void handleFileCapabilities(
       IHopFileType fileType, boolean changed, boolean running, boolean paused) 
{
+    handleFileCapabilities(fileType, null, changed, running, paused);
+  }
+
+  /**
+   * Same as {@link #handleFileCapabilities(IHopFileType, boolean, boolean, 
boolean)} but when
+   * handler is non-null, Save/SaveAs use the handler's capability (e.g. 
disabled for binary raw
+   * view).
+   */
+  public void handleFileCapabilities(
+      IHopFileType fileType,
+      IHopFileTypeHandler handler,
+      boolean changed,
+      boolean running,
+      boolean paused) {
 
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_FILE_SAVE, IHopFileType.CAPABILITY_SAVE, 
changed);
+        fileType, handler, ID_MAIN_MENU_FILE_SAVE, 
IHopFileType.CAPABILITY_SAVE, changed);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_FILE_SAVE_AS, IHopFileType.CAPABILITY_SAVE_AS);
+        fileType, handler, ID_MAIN_MENU_FILE_SAVE_AS, 
IHopFileType.CAPABILITY_SAVE_AS);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_FILE_EXPORT_TO_SVG, 
IHopFileType.CAPABILITY_EXPORT_TO_SVG);
+        fileType, handler, ID_MAIN_MENU_FILE_EXPORT_TO_SVG, 
IHopFileType.CAPABILITY_EXPORT_TO_SVG);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_FILE_CLOSE, IHopFileType.CAPABILITY_CLOSE);
+        fileType, handler, ID_MAIN_MENU_FILE_CLOSE, 
IHopFileType.CAPABILITY_CLOSE);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_FILE_CLOSE_ALL, IHopFileType.CAPABILITY_CLOSE);
+        fileType, handler, ID_MAIN_MENU_FILE_CLOSE_ALL, 
IHopFileType.CAPABILITY_CLOSE);
 
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_EDIT_SELECT_ALL, 
IHopFileType.CAPABILITY_SELECT);
+        fileType, handler, ID_MAIN_MENU_EDIT_SELECT_ALL, 
IHopFileType.CAPABILITY_SELECT);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_EDIT_UNSELECT_ALL, 
IHopFileType.CAPABILITY_SELECT);
+        fileType, handler, ID_MAIN_MENU_EDIT_UNSELECT_ALL, 
IHopFileType.CAPABILITY_SELECT);
 
-    mainMenuWidgets.enableMenuItem(fileType, ID_MAIN_MENU_EDIT_COPY, 
IHopFileType.CAPABILITY_COPY);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_EDIT_PASTE, IHopFileType.CAPABILITY_PASTE);
-    mainMenuWidgets.enableMenuItem(fileType, ID_MAIN_MENU_EDIT_CUT, 
IHopFileType.CAPABILITY_CUT);
+        fileType, handler, ID_MAIN_MENU_EDIT_COPY, 
IHopFileType.CAPABILITY_COPY);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_EDIT_DELETE, IHopFileType.CAPABILITY_DELETE);
+        fileType, handler, ID_MAIN_MENU_EDIT_PASTE, 
IHopFileType.CAPABILITY_PASTE);
+    mainMenuWidgets.enableMenuItem(
+        fileType, handler, ID_MAIN_MENU_EDIT_CUT, IHopFileType.CAPABILITY_CUT);
+    mainMenuWidgets.enableMenuItem(
+        fileType, handler, ID_MAIN_MENU_EDIT_DELETE, 
IHopFileType.CAPABILITY_DELETE);
 
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_RUN_START, IHopFileType.CAPABILITY_START, 
!running);
+        fileType, handler, ID_MAIN_MENU_RUN_START, 
IHopFileType.CAPABILITY_START, !running);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_RUN_STOP, IHopFileType.CAPABILITY_STOP, 
running);
+        fileType, handler, ID_MAIN_MENU_RUN_STOP, 
IHopFileType.CAPABILITY_STOP, running);
+    mainMenuWidgets.enableMenuItem(
+        fileType,
+        handler,
+        ID_MAIN_MENU_RUN_PAUSE,
+        IHopFileType.CAPABILITY_PAUSE,
+        running && !paused);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_RUN_PAUSE, IHopFileType.CAPABILITY_PAUSE, 
running && !paused);
+        fileType, handler, ID_MAIN_MENU_RUN_RESUME, 
IHopFileType.CAPABILITY_PAUSE, paused);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_RUN_RESUME, IHopFileType.CAPABILITY_PAUSE, 
paused);
+        fileType, handler, ID_MAIN_MENU_RUN_PREVIEW, 
IHopFileType.CAPABILITY_PREVIEW);
     mainMenuWidgets.enableMenuItem(
-        fileType, ID_MAIN_MENU_RUN_PREVIEW, IHopFileType.CAPABILITY_PREVIEW);
-    mainMenuWidgets.enableMenuItem(fileType, ID_MAIN_MENU_RUN_DEBUG, 
IHopFileType.CAPABILITY_DEBUG);
+        fileType, handler, ID_MAIN_MENU_RUN_DEBUG, 
IHopFileType.CAPABILITY_DEBUG);
 
     mainMenuWidgets.enableMenuItem(
         fileType,
+        handler,
         ID_MAIN_MENU_EDIT_NAV_PREV,
         IHopFileType.CAPABILITY_FILE_HISTORY,
         getActivePerspective().hasNavigationPreviousFile());
     mainMenuWidgets.enableMenuItem(
         fileType,
+        handler,
         ID_MAIN_MENU_EDIT_NAV_NEXT,
         IHopFileType.CAPABILITY_FILE_HISTORY,
         getActivePerspective().hasNavigationNextFile());
 
     mainToolbarWidgets.enableToolbarItem(
-        fileType, ID_MAIN_TOOLBAR_SAVE, IHopFileType.CAPABILITY_SAVE, changed);
+        fileType, handler, ID_MAIN_TOOLBAR_SAVE, IHopFileType.CAPABILITY_SAVE, 
changed);
     mainToolbarWidgets.enableToolbarItem(
-        fileType, ID_MAIN_TOOLBAR_SAVE_AS, IHopFileType.CAPABILITY_SAVE_AS);
+        fileType, handler, ID_MAIN_TOOLBAR_SAVE_AS, 
IHopFileType.CAPABILITY_SAVE_AS);
   }
 
   public IHopFileTypeHandler getActiveFileTypeHandler() {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
index 80611e0814..94dd7ff987 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
@@ -121,10 +121,12 @@ public class HopGuiFileDelegate {
 
     IHopFileTypeHandler fileTypeHandler = hopFile.openFile(hopGui, filename, 
hopGui.getVariables());
     if (fileTypeHandler != null) {
-      hopGui.handleFileCapabilities(hopFile, fileTypeHandler.hasChanged(), 
false, false);
+      hopGui.handleFileCapabilities(
+          hopFile, fileTypeHandler, fileTypeHandler.hasChanged(), false, 
false);
       if (EnvironmentUtils.getInstance().isWeb()) {
         // Do it again to test
-        hopGui.handleFileCapabilities(hopFile, fileTypeHandler.hasChanged(), 
false, false);
+        hopGui.handleFileCapabilities(
+            hopFile, fileTypeHandler, fileTypeHandler.hasChanged(), false, 
false);
       }
 
       // Also save the state of Hop GUI
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
index be774fb1b8..c1c4c5002d 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileType.java
@@ -136,4 +136,14 @@ public interface IHopFileType {
    * @return The path to the SVG file, a logo for this file type
    */
   String getFileTypeImage();
+
+  /**
+   * Whether this file type supports opening (shows content in an editor). 
When false, the Open
+   * action is disabled for files of this type. Folders are not affected.
+   *
+   * @return true if opening a file of this type shows an editor, false if it 
would do nothing
+   */
+  default boolean supportsOpening() {
+    return true;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
index 1b3a6efd05..eb4acb1a80 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/IHopFileTypeHandler.java
@@ -163,4 +163,15 @@ public interface IHopFileTypeHandler extends 
IActionContextHandlersProvider {
 
   /** mark the file as deleted */
   public default void markDeleted() {}
+
+  /**
+   * Whether this handler supports the given capability (e.g. Save). Defaults 
to the file type's
+   * capability; handlers can override to disable per-instance (e.g. raw view 
of a binary file).
+   *
+   * @param capability the capability constant from {@link IHopFileType}
+   * @return true if the capability is supported
+   */
+  default boolean hasCapability(String capability) {
+    return getFileType() != null && getFileType().hasCapability(capability);
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/empty/EmptyFileType.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/empty/EmptyFileType.java
index f5da7370e6..e89b3c45c3 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/empty/EmptyFileType.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/empty/EmptyFileType.java
@@ -90,4 +90,9 @@ public class EmptyFileType implements IHopFileType {
   public String getFileTypeImage() {
     return "ui/images/file.svg";
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 0eca5f8f81..a8109e4ff5 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -128,6 +128,7 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Text;
 import org.eclipse.swt.widgets.ToolBar;
@@ -177,6 +178,8 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
   public static final String CONTEXT_MENU_COLLAPSE_ALL =
       "ExplorerPerspective-ContextMenu-10070-CollapseAll";
   public static final String CONTEXT_MENU_OPEN = 
"ExplorerPerspective-ContextMenu-10100-Open";
+  public static final String CONTEXT_MENU_OPEN_AS_TEXT =
+      "ExplorerPerspective-ContextMenu-10101-OpenAsText";
   public static final String CONTEXT_MENU_RENAME = 
"ExplorerPerspective-ContextMenu-10300-Rename";
   public static final String CONTEXT_MENU_COPY_NAME =
       "ExplorerPerspective-ContextMenu-10400-CopyName";
@@ -511,7 +514,17 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
           }
 
           TreeItem[] selection = tree.getSelection();
-          
menuWidgets.findMenuItem(CONTEXT_MENU_OPEN).setEnabled(selection.length == 1);
+          TreeItemFolder tif =
+              selection.length == 1 ? (TreeItemFolder) selection[0].getData() 
: null;
+          boolean openSupported = tif != null && (tif.folder || 
tif.fileType.supportsOpening());
+          MenuItem openItem = menuWidgets.findMenuItem(CONTEXT_MENU_OPEN);
+          if (openItem != null) {
+            openItem.setEnabled(openSupported);
+          }
+          MenuItem openAsTextItem = 
menuWidgets.findMenuItem(CONTEXT_MENU_OPEN_AS_TEXT);
+          if (openAsTextItem != null) {
+            openAsTextItem.setEnabled(selection.length == 1 && tif != null && 
!tif.folder);
+          }
           
menuWidgets.findMenuItem(CONTEXT_MENU_RENAME).setEnabled(selection.length == 1);
 
           // Show the menu
@@ -1559,7 +1572,11 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
 
         HopGui.getInstance()
             .handleFileCapabilities(
-                fileTypeHandler.getFileType(), fileTypeHandler.hasChanged(), 
false, false);
+                fileTypeHandler.getFileType(),
+                fileTypeHandler,
+                fileTypeHandler.hasChanged(),
+                false,
+                false);
       }
     }
   }
@@ -1955,6 +1972,15 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
           TreeItem childItem = new TreeItem(item, SWT.NONE);
           childItem.setText(childName);
           setItemImage(childItem, fileType);
+
+          // Apply gray for non-openable files before paint listeners so 
listeners (e.g. git) can
+          // use gray variants when they see this styling
+          if (!folder && !fileType.supportsOpening()) {
+            
childItem.setForeground(hopGui.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY));
+          } else {
+            childItem.setForeground(null);
+          }
+
           callPaintListeners(tree, childItem, childPath, childName);
           setTreeItemData(childItem, childPath, childName, fileType, depth, 
folder, true);
 
@@ -2355,14 +2381,15 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable {
     }
 
     boolean isFolderSelected = tif != null && tif.fileType instanceof 
FolderFileType;
+    boolean openSupported = tif != null && (tif.folder || 
tif.fileType.supportsOpening());
 
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_CREATE_FOLDER, 
isFolderSelected);
-    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_OPEN, tif != null);
+    toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_OPEN, openSupported);
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_DELETE, tif != null);
     toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_RENAME, tif != null);
 
     menuWidgets.enableMenuItem(CONTEXT_MENU_CREATE_FOLDER, isFolderSelected);
-    menuWidgets.enableMenuItem(CONTEXT_MENU_OPEN, tif != null);
+    menuWidgets.enableMenuItem(CONTEXT_MENU_OPEN, openSupported);
     menuWidgets.enableMenuItem(CONTEXT_MENU_DELETE, tif != null);
     menuWidgets.enableMenuItem(CONTEXT_MENU_RENAME, tif != null);
     menuWidgets.enableMenuItem(CONTEXT_MENU_COPY_NAME, tif != null);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/ArchiveFileType.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/ArchiveFileType.java
index a9dcc0d560..07a184728f 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/ArchiveFileType.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/ArchiveFileType.java
@@ -125,4 +125,9 @@ public class ArchiveFileType implements IHopFileType {
   public String getFileTypeImage() {
     return getClass().getAnnotation(HopFileTypePlugin.class).image();
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/FolderFileType.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/FolderFileType.java
index bc1006d3d9..549b66c81d 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/FolderFileType.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/FolderFileType.java
@@ -111,4 +111,9 @@ public class FolderFileType implements IHopFileType {
   public String getFileTypeImage() {
     return getClass().getAnnotation(HopFileTypePlugin.class).image();
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
index 95b574d91a..2c7c005129 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
@@ -107,4 +107,9 @@ public class GenericFileType implements IHopFileType {
   public String getFileTypeImage() {
     return "ui/images/file.svg";
   }
+
+  @Override
+  public boolean supportsOpening() {
+    return false;
+  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/base/BaseExplorerFileTypeHandler.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/base/BaseExplorerFileTypeHandler.java
index fc3b1a406d..5c23fcd164 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/base/BaseExplorerFileTypeHandler.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/base/BaseExplorerFileTypeHandler.java
@@ -169,7 +169,8 @@ public abstract class BaseExplorerFileTypeHandler 
implements IExplorerFileTypeHa
         .getDisplay()
         .asyncExec(
             () -> {
-              hopGui.handleFileCapabilities(this.getFileType(), 
this.hasChanged(), false, false);
+              hopGui.handleFileCapabilities(
+                  this.getFileType(), this, this.hasChanged(), false, false);
               perspective.updateTabItem(this);
               perspective.updateTreeItem(this);
             });
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileType.java
similarity index 51%
copy from 
ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
copy to 
ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileType.java
index 95b574d91a..bb0090cbcb 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/GenericFileType.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileType.java
@@ -13,61 +13,57 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
  */
 
-package org.apache.hop.ui.hopgui.perspective.explorer.file.types;
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.raw;
 
-import java.util.Collections;
-import java.util.List;
 import java.util.Properties;
 import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.file.IHasFilename;
 import org.apache.hop.core.variables.IVariables;
-import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.ui.hopgui.HopGui;
-import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
 import org.apache.hop.ui.hopgui.file.IHopFileType;
 import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
 import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import 
org.apache.hop.ui.hopgui.perspective.explorer.file.capabilities.FileTypeCapabilities;
+import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.text.BaseTextExplorerFileType;
 
-// TODO: implement as plugin, move to text transform plugin
-//
-public class GenericFileType implements IHopFileType {
-  @Override
-  public String getName() {
-    return "Generic File";
-  }
-
-  @Override
-  public String getDefaultFileExtension() {
-    return "";
-  }
+/**
+ * File type for viewing any file as raw text in the explorer. When content 
looks like text, save is
+ * enabled; when binary (null byte in sample), view is read-only and save is 
disabled. Never claims
+ * a file by extension ({@link #isHandledBy} returns false), so it is only 
used when explicitly
+ * opening as raw (e.g. via "Open as text" context menu).
+ */
+public class RawExplorerFileType extends 
BaseTextExplorerFileType<RawExplorerFileTypeHandler> {
 
-  @Override
-  public String[] getFilterExtensions() {
-    return new String[0];
-  }
+  private static final Properties CAPABILITIES =
+      FileTypeCapabilities.getCapabilities(
+          IHopFileType.CAPABILITY_SAVE,
+          IHopFileType.CAPABILITY_SAVE_AS,
+          IHopFileType.CAPABILITY_CLOSE,
+          IHopFileType.CAPABILITY_FILE_HISTORY,
+          IHopFileType.CAPABILITY_COPY,
+          IHopFileType.CAPABILITY_SELECT);
 
-  @Override
-  public String[] getFilterNames() {
-    return new String[0];
+  public RawExplorerFileType() {
+    super("Raw File", "", new String[0], new String[0], CAPABILITIES);
   }
 
   @Override
-  public Properties getCapabilities() {
-    return new Properties();
+  public boolean isHandledBy(String filename, boolean checkContent) throws 
HopException {
+    return false;
   }
 
   @Override
-  public boolean hasCapability(String capability) {
-    return false;
+  public String getFileTypeImage() {
+    return "ui/images/file.svg";
   }
 
   @Override
-  public IHopFileTypeHandler openFile(
-      HopGui hopGui, String filename, IVariables parentVariableSpace) throws 
HopException {
-    return new EmptyHopFileTypeHandler();
+  public RawExplorerFileTypeHandler createFileTypeHandler(
+      HopGui hopGui, ExplorerPerspective perspective, ExplorerFile file) {
+    return new RawExplorerFileTypeHandler(hopGui, perspective, file);
   }
 
   @Override
@@ -75,36 +71,4 @@ public class GenericFileType implements IHopFileType {
       throws HopException {
     return new EmptyHopFileTypeHandler();
   }
-
-  /**
-   * See if this is a generic file
-   *
-   * @param filename The filename
-   * @param checkContent True if we want to look inside the file content
-   * @return
-   * @throws HopException
-   */
-  @Override
-  public boolean isHandledBy(String filename, boolean checkContent) throws 
HopException {
-    try {
-      return HopVfs.getFileObject(filename).isFile();
-    } catch (Exception e) {
-      throw new HopException("Error seeing if file '" + filename + "' is a 
generic file", e);
-    }
-  }
-
-  @Override
-  public boolean supportsFile(IHasFilename metaObject) {
-    return false;
-  }
-
-  @Override
-  public List<IGuiContextHandler> getContextHandlers() {
-    return Collections.emptyList();
-  }
-
-  @Override
-  public String getFileTypeImage() {
-    return "ui/images/file.svg";
-  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileTypeHandler.java
similarity index 57%
copy from 
ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
copy to 
ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileTypeHandler.java
index b3497659b0..c446fee551 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/raw/RawExplorerFileTypeHandler.java
@@ -15,44 +15,54 @@
  * limitations under the License.
  */
 
-package org.apache.hop.ui.hopgui.perspective.explorer.file.types.text;
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.raw;
 
-import java.io.OutputStream;
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.Props;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.util.BinaryDetectionUtil;
 import org.apache.hop.core.vfs.HopVfs;
 import org.apache.hop.ui.core.PropsUi;
-import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
 import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
-import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.base.BaseExplorerFileTypeHandler;
+import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.text.BaseTextExplorerFileTypeHandler;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Text;
 
-/** This handles a text file in the file explorer perspective: open, save, ... 
*/
-public class BaseTextExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler {
+/**
+ * Handler for viewing any file as raw text. When content looks like text (no 
null byte in first
+ * 8KB), the file is editable and save/save-as are enabled; when binary, view 
is read-only and save
+ * buttons are disabled.
+ */
+public class RawExplorerFileTypeHandler extends 
BaseTextExplorerFileTypeHandler {
 
   private Text wText;
-  boolean reloadListener = false;
 
-  public BaseTextExplorerFileTypeHandler(
+  /** True if file was detected as binary (null byte in sample); save is 
disabled. */
+  private boolean binary;
+
+  public RawExplorerFileTypeHandler(
       HopGui hopGui, ExplorerPerspective perspective, ExplorerFile 
explorerFile) {
     super(hopGui, perspective, explorerFile);
   }
 
   @Override
   public void renderFile(Composite composite) {
-    // Render the file by simply showing the file content as a text widget...
-    //
-    wText = new Text(composite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+    binary = detectBinary();
+    int style = SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL;
+    if (binary) {
+      style |= SWT.READ_ONLY;
+    }
+    wText = new Text(composite, style);
     PropsUi.setLook(wText, Props.WIDGET_STYLE_FIXED);
     FormData fdText = new FormData();
     fdText.left = new FormAttachment(0, 0);
@@ -61,48 +71,63 @@ public class BaseTextExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
     fdText.bottom = new FormAttachment(100, 0);
     wText.setLayoutData(fdText);
 
-    // TODO: add bottom section to show status, size, changed dates, cursor 
position...
-    // TODO: options for validation, pretty print, ...
-    // TODO: options for reading the file with a various transform plugins
-    // TODO: option to discard changes (reload from disk)
-    // TODO: find in file feature, hook it up to the project find function
-    //
+    if (!binary) {
+      wText.addModifyListener(
+          e -> {
+            if (reloadListener) {
+              this.setChanged();
+              perspective.updateGui();
+            }
+          });
+    }
 
+    reloadListener = false;
     reload();
-    // If the widget changes after this it's been changed by the user
-    //
-    wText.addModifyListener(
-        e -> {
-          if (reloadListener) {
-            this.setChanged();
-            perspective.updateGui();
-          }
-        });
+    reloadListener = !binary;
+  }
+
+  private boolean detectBinary() {
+    try {
+      FileObject file = HopVfs.getFileObject(getFilename(), getVariables());
+      if (!file.exists() || !file.isFile()) {
+        return false;
+      }
+      try (InputStream in = HopVfs.getInputStream(file)) {
+        return !BinaryDetectionUtil.looksLikeText(in);
+      }
+    } catch (Exception e) {
+      LogChannel.UI.logBasic(
+          getClass().getSimpleName(),
+          "Error sampling file for binary detection, treating as text",
+          e);
+      return false;
+    }
   }
 
   @Override
-  public void save() throws HopException {
+  public boolean hasCapability(String capability) {
+    if (binary
+        && (IHopFileType.CAPABILITY_SAVE.equals(capability)
+            || IHopFileType.CAPABILITY_SAVE_AS.equals(capability))) {
+      return false;
+    }
+    return super.hasCapability(capability);
+  }
 
+  @Override
+  public void save() throws HopException {
+    if (binary) {
+      throw new HopException("Binary file cannot be saved as text.");
+    }
     try {
-      // Save the current explorer file ....
-      //
       String filename = explorerFile.getFilename();
-
       boolean fileExist = HopVfs.fileExists(filename);
-
-      // Save the file...
-      //
-      try (OutputStream outputStream = HopVfs.getOutputStream(filename, 
false)) {
+      try (java.io.OutputStream outputStream = 
HopVfs.getOutputStream(filename, false)) {
         outputStream.write(wText.getText().getBytes(StandardCharsets.UTF_8));
         outputStream.flush();
       }
-
       this.clearChanged();
-
-      // Update menu options, tab and tree item
       updateGui();
-
-      // If we create a new file, refresh the explorer perspective tree
       if (!fileExist) {
         perspective.refresh();
       }
@@ -113,50 +138,31 @@ public class BaseTextExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
 
   @Override
   public void saveAs(String filename) throws HopException {
-    try {
-
-      // Enforce file extension
-      if 
(!filename.toLowerCase().endsWith(this.getFileType().getDefaultFileExtension()))
 {
-        filename = filename + this.getFileType().getDefaultFileExtension();
-      }
-
-      // Normalize file name
-      filename = HopVfs.normalize(filename);
-
-      FileObject fileObject = HopVfs.getFileObject(filename);
-      if (fileObject.exists()) {
-        MessageBox box =
-            new MessageBox(hopGui.getActiveShell(), SWT.YES | SWT.NO | 
SWT.ICON_QUESTION);
-        box.setText("Overwrite?");
-        box.setMessage("Are you sure you want to overwrite file '" + filename 
+ "'?");
-        int answer = box.open();
-        if ((answer & SWT.YES) == 0) {
-          return;
-        }
-      }
-
-      setFilename(filename);
-
-      save();
-      hopGui.fileRefreshDelegate.register(filename, this);
-    } catch (Exception e) {
-      throw new HopException("Error validating file existence for '" + 
filename + "'", e);
+    if (binary) {
+      throw new HopException("Binary file cannot be saved as text.");
     }
+    filename = HopVfs.normalize(filename);
+    setFilename(filename);
+    save();
+    hopGui.fileRefreshDelegate.register(filename, this);
+  }
+
+  @Override
+  public boolean hasChanged() {
+    return !binary && super.hasChanged();
   }
 
   @Override
   public void reload() {
     try {
-      // Disable the Modifylistener temporary
       reloadListener = false;
       String contents = readTextFileContent("UTF-8");
       wText.setText(Const.NVL(contents, ""));
-
-      // enable the Modifylistener temporary
-      reloadListener = true;
     } catch (Exception e) {
-      LogChannel.UI.logError(
+      LogChannel.UI.logBasic(
           "Error reading contents of file '" + explorerFile.getFilename() + 
"'", e);
+    } finally {
+      reloadListener = !binary;
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
index b3497659b0..c0183deef5 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileTypeHandler.java
@@ -41,7 +41,7 @@ import org.eclipse.swt.widgets.Text;
 public class BaseTextExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler {
 
   private Text wText;
-  boolean reloadListener = false;
+  protected boolean reloadListener = false;
 
   public BaseTextExplorerFileTypeHandler(
       HopGui hopGui, ExplorerPerspective perspective, ExplorerFile 
explorerFile) {

Reply via email to