From ab2cecda4e905feb07d048d0e10007fa61e7a55c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:32:59 +0000 Subject: [PATCH 1/2] Initial plan From 103b005691b13d4d17da026596128085434e6d64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:39:16 +0000 Subject: [PATCH 2/2] feat(addin): add copy ribbon buttons Co-authored-by: danmaps <10421813+danmaps@users.noreply.github.com> --- ProExporter/Buttons.cs | 331 ++++++++++++++++++++++++++++++++++++++++ ProExporter/Config.daml | 36 +++++ README.md | 2 + 3 files changed, 369 insertions(+) diff --git a/ProExporter/Buttons.cs b/ProExporter/Buttons.cs index c1a0e2b..4a5d494 100644 --- a/ProExporter/Buttons.cs +++ b/ProExporter/Buttons.cs @@ -1,11 +1,17 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Windows; +using ArcGIS.Core.CIM; +using ArcGIS.Core.Data; using ArcGIS.Desktop.Core; using ArcGIS.Desktop.Framework.Contracts; +using ArcGIS.Desktop.Framework.Threading.Tasks; using ArcGIS.Desktop.Mapping; namespace ProExporter @@ -376,6 +382,331 @@ def _main() -> None: } } + /// + /// Button: Copy selected layer datasource path to clipboard + /// + public class CopyLayerDataSourcePathButton : Button + { + protected override async void OnClick() + { + if (!ExportController.CanExport()) + { + MessageBox.Show("Please open a project first.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + try + { + var payload = await QueuedTask.Run(ClipboardHelpers.GetSelectedLayerDataSourcePathPayload); + ClipboardHelpers.CopyPayloadToClipboard(payload); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + /// + /// Button: Copy project home folder to clipboard + /// + public class CopyProjectHomeFolderButton : Button + { + protected override void OnClick() + { + if (!ExportController.CanExport()) + { + MessageBox.Show("Please open a project first.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + var project = Project.Current; + var projectHome = project == null ? null : Path.GetDirectoryName(project.URI); + if (string.IsNullOrWhiteSpace(projectHome)) + { + MessageBox.Show("Could not determine the current project home folder.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + ClipboardHelpers.CopyPayloadToClipboard(new ClipboardHelpers.ClipboardPayload( + projectHome, + $"Copied project home folder to clipboard:\n{projectHome}")); + } + } + + /// + /// Button: Copy APRX path to clipboard + /// + public class CopyAprxPathButton : Button + { + protected override void OnClick() + { + if (!ExportController.CanExport()) + { + MessageBox.Show("Please open a project first.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + var project = Project.Current; + if (project == null || string.IsNullOrWhiteSpace(project.URI)) + { + MessageBox.Show("Could not determine the current project path.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + ClipboardHelpers.CopyPayloadToClipboard(new ClipboardHelpers.ClipboardPayload( + project.URI, + $"Copied APRX path to clipboard:\n{project.URI}")); + } + } + + /// + /// Button: Copy selected layer fields to clipboard + /// + public class CopyLayerFieldsButton : Button + { + protected override async void OnClick() + { + if (!ExportController.CanExport()) + { + MessageBox.Show("Please open a project first.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + try + { + var payload = await QueuedTask.Run(ClipboardHelpers.GetSelectedLayerFieldsPayload); + ClipboardHelpers.CopyPayloadToClipboard(payload); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + } + } + } + + internal static class ClipboardHelpers + { + internal record ClipboardPayload(string Text, string SuccessMessage); + + internal static void CopyPayloadToClipboard(ClipboardPayload payload) + { + if (payload == null || string.IsNullOrWhiteSpace(payload.Text)) + { + MessageBox.Show("There is nothing to copy to the clipboard.", "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + Clipboard.SetText(payload.Text); + MessageBox.Show(payload.SuccessMessage, "ArcGIS Pro CLI", + MessageBoxButton.OK, MessageBoxImage.Information); + } + + internal static ClipboardPayload GetSelectedLayerDataSourcePathPayload() + { + var layer = GetSelectedLayer(); + + if (layer is FeatureLayer featureLayer) + { + using var featureClass = featureLayer.GetFeatureClass(); + if (featureClass == null) + throw new InvalidOperationException($"Could not access the datasource for '{layer.Name}'."); + + using var datastore = featureClass.GetDatastore(); + var datasetName = featureClass.GetDefinition()?.GetName(); + var fullPath = BuildDatasetPath(datastore, datasetName); + if (string.IsNullOrWhiteSpace(fullPath)) + throw new InvalidOperationException($"Could not determine the datasource path for '{layer.Name}'."); + + return new ClipboardPayload( + fullPath, + $"Copied datasource path for '{layer.Name}' to clipboard:\n{fullPath}"); + } + + if (layer is RasterLayer rasterLayer) + { + var fullPath = GetRasterLayerPath(rasterLayer); + if (string.IsNullOrWhiteSpace(fullPath)) + throw new InvalidOperationException($"Could not determine the datasource path for '{layer.Name}'."); + + return new ClipboardPayload( + fullPath, + $"Copied datasource path for '{layer.Name}' to clipboard:\n{fullPath}"); + } + + throw new InvalidOperationException( + $"Layer '{layer.Name}' does not expose a copyable datasource path. Select a feature or raster layer."); + } + + internal static ClipboardPayload GetSelectedLayerFieldsPayload() + { + var layer = GetSelectedLayer(); + if (layer is not FeatureLayer featureLayer) + { + throw new InvalidOperationException( + $"Layer '{layer.Name}' does not expose field definitions. Select a feature layer."); + } + + using var featureClass = featureLayer.GetFeatureClass(); + if (featureClass == null) + throw new InvalidOperationException($"Could not access the fields for '{layer.Name}'."); + + var definition = featureClass.GetDefinition(); + var fields = definition?.GetFields()?.ToList() ?? new List(); + if (fields.Count == 0) + throw new InvalidOperationException($"Layer '{layer.Name}' has no fields to copy."); + + var rows = new StringBuilder(); + rows.AppendLine("Name\tAlias\tType\tLength\tNullable\tEditable\tDomain"); + + foreach (var field in fields) + { + rows.AppendLine(string.Join("\t", new[] + { + SanitizeClipboardCell(field.Name), + SanitizeClipboardCell(field.AliasName), + SanitizeClipboardCell(field.FieldType.ToString()), + field.Length.ToString(), + field.IsNullable ? "true" : "false", + field.IsEditable ? "true" : "false", + SanitizeClipboardCell(GetDomainName(field)) + })); + } + + return new ClipboardPayload( + rows.ToString(), + $"Copied {fields.Count} field definitions from '{layer.Name}' to clipboard."); + } + + private static Layer GetSelectedLayer() + { + var mapView = MapView.Active; + if (mapView?.Map == null) + throw new InvalidOperationException("Open a map and select a layer in the Contents pane first."); + + var selectedLayers = mapView.GetSelectedLayers(); + if (selectedLayers == null || selectedLayers.Count == 0) + throw new InvalidOperationException("Select a layer in the Contents pane first."); + + return selectedLayers.First(); + } + + private static string BuildDatasetPath(Datastore datastore, string datasetName) + { + var datastorePath = GetDataStorePath(datastore); + if (string.IsNullOrWhiteSpace(datasetName)) + return datastorePath; + + if (datastore is FileSystemDatastore && string.IsNullOrEmpty(Path.GetExtension(datasetName))) + datasetName += ".shp"; + + if (string.IsNullOrWhiteSpace(datastorePath)) + return datasetName; + + return Path.Combine(datastorePath, datasetName); + } + + private static string GetRasterLayerPath(RasterLayer rasterLayer) + { + try + { + var dataConnection = rasterLayer.GetDataConnection(); + if (dataConnection is CIMStandardDataConnection standardConnection) + { + var workspacePath = GetWorkspacePath(standardConnection.WorkspaceConnectionString); + if (!string.IsNullOrWhiteSpace(workspacePath) && !string.IsNullOrWhiteSpace(standardConnection.Dataset)) + return Path.Combine(workspacePath, standardConnection.Dataset); + + if (!string.IsNullOrWhiteSpace(standardConnection.Dataset)) + return standardConnection.Dataset; + } + } + catch + { + // Fall back to layer name below if the data connection is unavailable. + } + + return rasterLayer.Name; + } + + private static string GetWorkspacePath(string workspaceConnectionString) + { + if (string.IsNullOrWhiteSpace(workspaceConnectionString)) + return null; + + var parts = workspaceConnectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var part in parts) + { + var separatorIndex = part.IndexOf('='); + if (separatorIndex <= 0 || separatorIndex == part.Length - 1) + continue; + + var key = part.Substring(0, separatorIndex).Trim(); + var value = part.Substring(separatorIndex + 1).Trim().Trim('"'); + if (key.Equals("DATABASE", StringComparison.OrdinalIgnoreCase)) + return value; + } + + return null; + } + + private static string GetDataStorePath(Datastore datastore) + { + if (datastore == null) + return null; + + try + { + if (datastore is Geodatabase geodatabase) + { + var connector = geodatabase.GetConnector(); + if (connector is FileGeodatabaseConnectionPath fileGdb) + return fileGdb.Path.LocalPath; + if (connector is DatabaseConnectionFile databaseConnectionFile) + return databaseConnectionFile.Path.LocalPath; + } + else if (datastore is FileSystemDatastore fileSystemDatastore) + { + var connector = fileSystemDatastore.GetConnector(); + if (connector is FileSystemConnectionPath fileSystemConnectionPath) + return fileSystemConnectionPath.Path.LocalPath; + } + } + catch + { + // Fall back below if connector inspection fails. + } + + return datastore.GetConnectionString(); + } + + private static string GetDomainName(Field field) + { + try + { + return field.GetDomain()?.GetName() ?? string.Empty; + } + catch + { + return string.Empty; + } + } + + private static string SanitizeClipboardCell(string value) + { + return (value ?? string.Empty).Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); + } + } + public class ActiveMapOnlyCheckBox : CheckBox { public ActiveMapOnlyCheckBox() diff --git a/ProExporter/Config.daml b/ProExporter/Config.daml index 2bc1c03..bd2f230 100644 --- a/ProExporter/Config.daml +++ b/ProExporter/Config.daml @@ -30,6 +30,10 @@ + + + + diff --git a/README.md b/README.md index 71c4fac..df58512 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ arcgis install arcgis layer "Parcels" ``` +The **CLI** ribbon tab also includes one-click copy buttons for the selected layer path, project home folder, APRX path, and selected layer fields. + More: [docs/quickstart.md](docs/quickstart.md) ## What's New in v0.4.0