title: Copy from TableView/TreeTableView with overridden cell factory
body: <p>I was looking for a universal way (i.e., that can be applied to an arbitrary <code>TableView</code> or <code>TreeTableView</code>) to copy data "<strong>as you see it</strong>" from a <code>TableView</code>/<code>TreeTableView</code>. I found a couple of posts about how to copy contents from a <code>TableView</code> (<a href="https://stackoverflow.com/questions/29727276/how-to-copy-paste-table-cells-in-a-tableview">here</a> and <a href="https://stackoverflow.com/questions/25170119/allow-user-to-copy-data-from-tableview">here</a>), but the important part "<strong>as you see it</strong>" is the issue with all of them.</p>
  
  <p>All solutions I saw are relying on getting the data associated with each cell (pretty easy to do), and calling <code>.toString()</code> on it. The problem is that when you store one type (let's say a <code>Long</code>) as actual data in a column, and then define a custom cell factory to display it as a <code>String</code> (it's beyond the scope why you would do that, I just want a method that works with such table views):</p>
  
  <pre><code>TableColumn&lt;MyData, Long&gt; timeColumn;
  &lt;...&gt;    
          timeColumn.setCellFactory(param -&gt; new TableCell&lt;MyData, Long&gt;() {
                  @Override
                  protected void updateItem(Long item, boolean empty) {
                      super.updateItem(item, empty);
                      if (item == null || empty) {
                          super.setText(null);
                      } else {
                          super.setText(LocalDate.from(Instant.ofEpochMilli(item)).format(DateTimeFormatter.ISO_DATE));
                      }
                  }
              }
      );
  </code></pre>
  
  <p>Those methods based on converting the underlying data (which is <code>Long</code> here) to <code>String</code> will obviously not work, because they will copy a <strong>number</strong> and not a <strong>date</strong> (which is what the user sees in the table).</p>
  
  <p><strong>Possible (envisioned) solutions:</strong></p>
  
  <ol>
  <li><p>If I could get my hand on the <code>TableCell</code> object associated with each table cell, I could do <code>TableCell.getText()</code>, and we are done. Unfortunately, <code>TableView</code> does not allow this (have I missed a way to do it?)</p></li>
  <li><p>I can easily get the <code>CellFactory</code> associated with the column, and therefore create a new <code>TableCell</code> (identical to that one existing in the table view):</p>
  
  <p><code>TableCell&lt;T, ?&gt; cell = column.getCellFactory().call(column);</code></p>
  
  <p>Then the problem is there's no way (again, did I miss it?) to force a <code>TableCell</code> to call the <code>updateItem</code> method! I tried to use <code>commitEdit(T newValue)</code>, but it's pretty messy: there are checks inside, so you need to make the whole stuff (column, row, table) <code>Editable</code>, and call <code>startEdit</code> first.</p>
  
  <p>2a. So the only solution that works for me, uses the Reflection to call the protected <code>updateItem</code>, which feels kind of dirty hacking:</p>
  
  <pre><code>// For TableView:
  T selectedItem = &lt;...&gt;;
  // OR for TreeTableView:
  TreeItem&lt;T&gt; selectedItem = &lt;...&gt;;
  
  TableCell&lt;T, Object&gt; cell = (TableCell&lt;T, Object&gt;) column.getCellFactory().call(column);
  
  try {
      Method update = cell.getClass().getDeclaredMethod("updateItem", Object.class, boolean.class);
      update.setAccessible(true);
      Object data = column.getCellData(selectedItem);
      update.invoke(cell, data, data == null);
  } catch (Exception ex) {
      logger.warn("Failed to update item: ", ex);
  }
  if (cell.getText() != null) {
      return cell.getText().replaceAll(fieldSeparator, "");
  } else {
      return "";
  }
  </code></pre></li>
  </ol>
  
  <p>I would appreciate any comment on it, namely if this can be achieved with less blood. Or may be indicate some problems with my solution which I missed. </p>
  
  <p>Here's the full code in case someone wants to use it (in spite of its ugliness :)</p>
  
  <pre><code>package com.mycompany.util;
  
  import com.google.common.collect.Lists;
  import javafx.beans.property.ObjectProperty;
  import javafx.scene.control.*;
  import javafx.scene.input.Clipboard;
  import javafx.scene.input.DataFormat;
  import javafx.scene.input.KeyCode;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.lang.reflect.Method;
  import java.util.Collections;
  import java.util.List;
  import java.util.function.Function;
  import java.util.stream.Collectors;
  
  public class TableViewCopyable {
  
      private static final Logger logger = LoggerFactory.getLogger(TableViewCopyable.class.getName());
  
      protected final String defaultFieldSep;
      protected final String defaultLineSep;
  
      protected TableViewCopyable(String defaultFieldSep, String defaultLineSep) {
          this.defaultFieldSep = defaultFieldSep;
          this.defaultLineSep = defaultLineSep;
      }
  
      protected static final &lt;T&gt; void copyToClipboard(List&lt;T&gt; rows, Function&lt;List&lt;T&gt;, String&gt; extractor) {
          logger.info("Copied " + rows.size() + " item(s) to clipboard");
          Clipboard.getSystemClipboard().setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, extractor.apply(rows)));
      }
  
      public static TableViewCopyable with(String fieldSep, String lineSep) {
          return new TableViewCopyable(fieldSep, lineSep);
      }
  
      public static TableViewCopyable toCsv() {
          // When using System.lineSeparator() as line separator, there appears to be an extra line break :-/
          return with(",", "\n");
      }
  
      public final &lt;T&gt; void makeCopyable(TableView&lt;T&gt; table, Function&lt;List&lt;T&gt;, String&gt; extractor) {
  
          table.setOnKeyPressed(event -&gt; {
              if (event.getCode().equals(KeyCode.C) &amp;&amp; event.isControlDown() || event.isControlDown() &amp;&amp; event.getCode().equals(KeyCode.INSERT)) {
                  // "Smart" copying: if single selection, copy all by default. Otherwise copy selected by default
                  boolean selectedOnly = table.getSelectionModel().getSelectionMode().equals(SelectionMode.MULTIPLE);
                  copyToClipboard(getItemsToCopy(table, selectedOnly), extractor);
              }
          });
  
          MenuItem copy = new MenuItem("Copy selected");
          copy.setOnAction(event -&gt; copyToClipboard(table.getSelectionModel().getSelectedItems(), extractor));
          MenuItem copyAll = new MenuItem("Copy all");
          copyAll.setOnAction(event -&gt; copyToClipboard(table.getItems(), extractor));
  
          addToContextMenu(table.contextMenuProperty(), copy, copyAll);
      }
  
      public final &lt;T&gt; void makeCopyable(TreeTableView&lt;T&gt; table, Function&lt;List&lt;TreeItem&lt;T&gt;&gt;, String&gt; extractor) {
  
          table.setOnKeyPressed(event -&gt; {
              if (event.getCode().equals(KeyCode.C) &amp;&amp; event.isControlDown() || event.isControlDown() &amp;&amp; event.getCode().equals(KeyCode.INSERT)) {
                  // "Smart" copying: if single selection, copy all by default. Otherwise copy selected by default
                  boolean selectedOnly = table.getSelectionModel().getSelectionMode().equals(SelectionMode.MULTIPLE);
                  copyToClipboard(getItemsToCopy(table, selectedOnly), extractor);
              }
          });
  
          MenuItem copy = new MenuItem("Copy selected");
          copy.setOnAction(event -&gt; copyToClipboard(getItemsToCopy(table, true), extractor));
  
          MenuItem copyAll = new MenuItem("Copy all (expanded only)");
          copyAll.setOnAction(event -&gt; copyToClipboard(getItemsToCopy(table, false), extractor));
  
          addToContextMenu(table.contextMenuProperty(), copy, copyAll);
      }
  
      protected &lt;T&gt; List&lt;TreeItem&lt;T&gt;&gt; getItemsToCopy(TreeTableView&lt;T&gt; table, boolean selectedOnly) {
          if (selectedOnly) {
              // If multiple selection is allowed, copy only selected by default:
              return table.getSelectionModel().getSelectedItems();
          } else {
              // Otherwise, copy everything
              List&lt;TreeItem&lt;T&gt;&gt; list = Lists.newArrayList();
              for (int i = 0; i &lt; table.getExpandedItemCount(); i++) {
                  list.add(table.getTreeItem(i));
              }
              return list;
          }
      }
  
      protected &lt;T&gt; List&lt;T&gt; getItemsToCopy(TableView&lt;T&gt; table, boolean selectedOnly) {
          if (selectedOnly) {
              // If multiple selection is allowed, copy only selected by default:
              return table.getSelectionModel().getSelectedItems();
          } else {
              return table.getItems();
          }
      }
  
      protected void addToContextMenu(ObjectProperty&lt;ContextMenu&gt; menu, MenuItem... items) {
          if (menu.get() == null) {
              menu.set(new ContextMenu(items));
          } else {
              for (MenuItem item : items) {
                  menu.get().getItems().add(item);
              }
          }
      }
  
      public final &lt;T&gt; void makeCopyable(TableView&lt;T&gt; table, String fieldSeparator) {
          makeCopyable(table, csvVisibleColumns(table, fieldSeparator));
      }
  
      public final &lt;T&gt; void makeCopyable(TreeTableView&lt;T&gt; table, String fieldSeparator) {
          makeCopyable(table, csvVisibleColumns(table, fieldSeparator));
      }
  
      public final &lt;T&gt; void makeCopyable(TableView&lt;T&gt; table) {
          makeCopyable(table, csvVisibleColumns(table, defaultFieldSep));
      }
  
      public final &lt;T&gt; void makeCopyable(TreeTableView&lt;T&gt; table) {
          makeCopyable(table, defaultFieldSep);
      }
  
      protected &lt;T&gt; String extractDataFromCell(IndexedCell&lt;T&gt; cell, Object data, String fieldSeparator) {
          try {
              Method update = cell.getClass().getDeclaredMethod("updateItem", Object.class, boolean.class);
              update.setAccessible(true);
              update.invoke(cell, data, data == null);
          } catch (Exception ex) {
              logger.warn("Failed to updated item: ", ex);
          }
          if (cell.getText() != null) {
              return cell.getText().replaceAll(fieldSeparator, "");
          } else {
              return "";
          }
      }
  
      public final &lt;T&gt; Function&lt;List&lt;T&gt;, String&gt; csvVisibleColumns(TableView&lt;T&gt; table, String fieldSeparator) {
          return (List&lt;T&gt; items) -&gt; {
              StringBuilder builder = new StringBuilder();
              // Write table header
              builder.append(table.getVisibleLeafColumns().stream().map(TableColumn::getText).collect(Collectors.joining(fieldSeparator))).append(defaultLineSep);
              items.forEach(item -&gt; builder.append(
                      table.getVisibleLeafColumns()
                              .stream()
                              .map(col -&gt; extractDataFromCell(((TableColumn&lt;T, Object&gt;) col).getCellFactory().call((TableColumn&lt;T, Object&gt;) col), col.getCellData(item), fieldSeparator))
                              .collect(Collectors.joining(defaultFieldSep))
              ).append(defaultLineSep));
              return builder.toString();
          };
      }
  
      public final &lt;T&gt; Function&lt;List&lt;TreeItem&lt;T&gt;&gt;, String&gt; csvVisibleColumns(TreeTableView&lt;T&gt; table, String fieldSeparator) {
          return (List&lt;TreeItem&lt;T&gt;&gt; items) -&gt; {
              StringBuilder builder = new StringBuilder();
              // Write table header
              builder.append(table.getVisibleLeafColumns().stream().map(TreeTableColumn::getText).collect(Collectors.joining(fieldSeparator))).append(defaultLineSep);
              items.forEach(item -&gt; builder.append(
                      table.getVisibleLeafColumns()
                              .stream()
                              .map(col -&gt; extractDataFromCell(((TreeTableColumn&lt;T, Object&gt;) col).getCellFactory().call((TreeTableColumn&lt;T, Object&gt;) col), col.getCellData(item), fieldSeparator))
                              .collect(Collectors.joining(defaultFieldSep))
              ).append(defaultLineSep));
              return builder.toString();
          };
      }
  }
  </code></pre>
  
  <p>Then the usage is pretty simple:</p>
  
  <pre><code>    TableViewCopyable.toCsv().makeCopyable(someTreeTableView);
      TableViewCopyable.toCsv().makeCopyable(someTableView);
  </code></pre>
  
  <p>Thanks!    </p>