Saltar al contenido
CrossFly es 100% open sourceSíguenos en GitHub
CrossFlyCrossFly
Empezar
Todos los ejemplos
RecetasDispositivo249 líneas · 1 archivos

Files, sharing and printing

Ask the capability, not the platform. Phones, Windows 10+ and the Mac have the picker and the share sheet; Linux has no share sheet; a browser has the picker always and the sheet only sometimes. Printing takes a PDF, and every platform's own dialog does the rest.

Qué muestra esto

Choosing a file, sharing it, saving it and printing — and asking first whether this platform can.

These two are the clearest case in the framework of a capability that is not a property of the platform. macOS has both since 29/09/2026, through AppKit's own share menu, and Windows 10+ since 01/10/2026, through WinRT's DataTransferManager; Linux has a file dialog and no share sheet. The browser has a picker always and a share sheet only in some browsers, over HTTPS. So "am I on the web?" is the wrong question, and isSupported is the right one — it is answered at runtime, per capability, and two browsers on the same machine can answer it differently.

The picker is asynchronous, and has to be: the web build is single-threaded JavaScript, so a blocking file dialog cannot exist on one of the four platforms. The callback arrives on the paint thread, so it may touch the UI directly, as it does below.

The cancel is not an error and not a null: open answers with an empty array. That is one branch, always present, and forgetting it is what produces a screen that appears to do nothing when the user backs out of the dialog.

Ejecutarlo en tu máquina

Cada ejemplo es una Window normal, así que el lanzador solo necesita su nombre.

gradlew lwjgl3:run --args="classname dev.cf.recipe.R31Files"
Editarlo

Java se compila antes de ejecutarse, así que el navegador no puede reconstruir una edición por su cuenta: el código que ves aquí se convirtió en JavaScript con TeaVM antes de publicarse. Copia el archivo a un proyecto y funciona sin cambios en las cuatro plataformas; el comando de arriba es toda la preparación.

Ejecutándose aquíCrossFly 0.1.32
Iniciando la aplicación…

Compilado a JavaScript por TeaVM: es el mismo código, no un vídeo de él. · Todos los ejemplos

Qué usa

Tipos del framework que toca este ejemplo. Los enlazados tienen página de referencia.

ButtonCapabilitiesCapabilityCardComponentDebugFileChooserGapGridLayoutLabelMaterialIconPalColorPdfPlatformUtilsScreenStylesVerticalLayoutWidgets

El código

Completo, sin recortes. Es el archivo, no un fragmento.

Crudo
dev/cf/recipe/R31Files.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.capability.Capabilities;
import dev.crossfly.capability.FileChooser;
import dev.crossfly.util.Debug;
import dev.crossfly.util.pdf.Pdf;
import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Choosing a file, sharing it, saving it and printing — and asking first whether this platform can.
 *
 * <p>These two are the clearest case in the framework of a capability that is <b>not</b> a property
 * of the platform. macOS has both since 29/09/2026, through AppKit's own share menu, and Windows 10+
 * since 01/10/2026, through WinRT's DataTransferManager; Linux has a file dialog and no share sheet.
 * The browser has a picker always and a share sheet only in
 * some browsers, over HTTPS. So "am I on the web?" is the wrong question, and
 * {@link PlatformUtils#isSupported} is the right one — it is answered at runtime, per capability,
 * and two browsers on the same machine can answer it differently.</p>
 *
 * <p>The picker is <b>asynchronous, and has to be</b>: the web build is single-threaded JavaScript,
 * so a blocking file dialog cannot exist on one of the four platforms. The callback arrives on the
 * paint thread, so it may touch the UI directly, as it does below.</p>
 *
 * <p>The cancel is not an error and not a null: {@link FileChooser#open} answers with an
 * <b>empty array</b>. That is one branch, always present, and forgetting it is what produces a
 * screen that appears to do nothing when the user backs out of the dialog.</p>
 */
public class R31Files extends Recipe
{
   private Label answer;

   /** The last pick, kept so the share row has something to hand over. */
   private FileChooser.PickedFile chosen;

   public String title()
   {
      return "Files, sharing and printing";
   }

   public String note()
   {
      return "Ask the capability, not the platform. Phones, Windows 10+ and the Mac have the picker and"
         + " the share sheet; Linux has no share sheet; a browser has the picker always and the sheet only"
         + " sometimes."
         + " Printing takes a PDF, and every platform's own dialog does the rest.";
   }

   public void build(Component body) throws Exception
   {
      answer = label("Nothing chosen yet.", 1f, false, Styles.captionColor, Position.START);
      answer.testTag("files.answer");
      // Pinned below the scrolling body, so the answer stays in view (see Recipe.pin).
      pin(answer);

      row(body, "files.choose", 0xE2C8, "Choose a file", PlatformUtils.isSupported(Capability.FILE_PICKER),
         () -> Capabilities.files()
            .open(new FileChooser.Options().title("Choose a file").extensions("pdf", "png", "jpg", "txt"),
               files ->
               {
                  // The empty array IS the cancel. No null to check, and no exception to catch.
                  if (files.length == 0)
                  {
                     said("Cancelled");
                     return;
                  }
                  chosen = files[0];
                  read(chosen);
               }));

      row(body, "files.share-link", 0xE80D, "Share a link", PlatformUtils.isSupported(Capability.SHARING),
         () -> Capabilities.sharing().text("CrossFly", "https://crossfly.dev"));

      // Sharing.bytes and not Sharing.file, and the difference is the whole lesson: a file the
      // user picked has no path on Android (it is a content:// document) and none at all in a
      // browser, so the path-shaped call cannot serve the most ordinary flow there is — pick
      // something, then send it. Handing over the content works on every platform that shares.
      row(body, "files.share-file", 0xE2C4, "Share the chosen file",
         PlatformUtils.isSupported(Capability.SHARING), () ->
         {
            if (chosen == null)
            {
               said("Choose a file first");
               return;
            }
            try
            {
               Capabilities.sharing().bytes("CrossFly", chosen.name, chosen.bytes());
            }
            catch (Exception e)
            {
               said("Could not share: " + e.getMessage());
            }
         });

      // Saving is NOT sharing, and the two rows sit together so the difference is visible: sharing
      // sends content somewhere else and never says where it went; saving puts a file under the
      // person's own control, with a name they picked, in a place they can find again.
      row(body, "files.save", 0xE161, "Save a file",
         PlatformUtils.isSupported(Capability.SAVE_FILE), () ->
         {
            byte[] content = chosen != null ? bytesOf(chosen)
               : "Saved by CrossFly.\n".getBytes(java.nio.charset.StandardCharsets.UTF_8);
            String name = chosen != null ? chosen.name : "crossfly.txt";
            String mime = chosen != null ? chosen.mimeType() : "text/plain";
            if (content == null)
               return;
            Capabilities.saver()
               .save(name, mime, content, (saved, error) -> said(saved ? "Saved " + name
                  : error == null ? "Cancelled" : "Could not save: " + error));
         });

      // Printing is the third way out, and it takes a PDF because every platform's print system
      // already speaks PDF -- a receipt generated here and a boleto downloaded from a server
      // print through the same call.
      row(body, "files.print", 0xE8AD, "Print a receipt",
         PlatformUtils.isSupported(Capability.PRINTING), this::printReceipt);
   }

   /**
    * A receipt on an 80 mm roll, built with {@link Pdf} and handed to the system's print dialog.
    * <p>
    * The answer has <b>four</b> values and the browser is why: a phone and a desktop say whether
    * the job was sent or cancelled, and a browser only says that its dialog closed. Treating
    * {@code CLOSED} as either of the other two would be the app guessing.
    */
   private void printReceipt()
   {
      Pdf pdf = new Pdf();
      pdf.page(Pdf.ROLL_80MM_WIDTH, 300);
      pdf.font(Pdf.COURIER_BOLD, 14).text(12, 270, "RECEIPT 4471");
      // Amounts in reais on purpose: a Brazilian receipt, with the comma as decimal separator.
      pdf.font(Pdf.COURIER, 10).text(12, 250, "2 x Specialty coffee R$ 38,00");
      pdf.font(Pdf.COURIER, 10).text(12, 235, "1 x Sugar 1 kg       R$  6,90");
      pdf.lineWidth(1).line(12, 225, 215, 225);
      pdf.font(Pdf.COURIER_BOLD, 11).text(12, 208, "Total               R$ 44,90");
      Capabilities.printing().print("Receipt 4471",pdf.build(), (result, error) ->
      {
         switch (result)
         {
            case SENT:
               said("Sent to the printer");
               break;
            case CANCELLED:
               said("Cancelled");
               break;
            case CLOSED:
               said("The print dialog closed (this platform does not say whether it printed)");
               break;
            default:
               said("Could not print: " + error);
         }
      });
   }

   /**
    * The chosen file's bytes, or null after saying why not.
    *
    * @param f the chosen file
    * @return its content, or null
    */
   private byte[] bytesOf(FileChooser.PickedFile f)
   {
      try
      {
         return f.bytes();
      }
      catch (Exception e)
      {
         said("Could not read " + f.name + ": " + e.getMessage());
         return null;
      }
   }

   /**
    * Proves the pick is real by reading it, which is the part that differs most between platforms:
    * a path on the desktop and on iOS, a content resolver on Android, an ArrayBuffer in a browser.
    */
   private void read(FileChooser.PickedFile f)
   {
      try
      {
         int bytes = f.bytes().length;
         said(f.name + " — " + bytes + " bytes read, " + f.mimeType());
      }
      catch (Exception e)
      {
         said(f.name + " — could not be read: " + e.getMessage());
      }
   }

   /**
    * A row per capability. The button carries a {@code testTag} so a TestCase can drive it — which
    * on a phone is the only way in, since the whole UI is drawn on one GL surface and a device
    * automation tool sees a single view with no children inside it.
    *
    * @param tag       stable name for the button, for tests
    * @param icone     the Material glyph on the tile
    * @param name      the row's label
    * @param available whether the platform has this capability
    * @param go        what the button does
    */
   private void row(Component body, String tag, int icone, String name, boolean available,
      Runnable go) throws Exception
   {
      Card c = new Card();
      c.setLayout(new GridLayout(Position.START, Position.CENTER));
      c.getStyle()
         .outgap(Gap.ofFont(0, 0, 0, .5f));

      PalColor tom = available ? Tones.accent() : Tones.danger();

      Component text = new Component();
      text.setLayout(new VerticalLayout(Position.START, Position.START));
      text.add(LAY_AUTO, label(name, 1.2f, true, Styles.textColor, Position.START));
      text.add(LAY_AUTO, label(available ? "Ready" : "Not on this platform", 1f, true, tom,
         Position.START));

      Button b = new Button(MaterialIcon.of(available ? 0xE5C8 : 0xE002, 1.4f));
      b.setEnabled(available);
      b.testTag(tag);
      b.onAction(e -> go.run());

      c.add(0f, 1f, 0f, Tones.tile(icone, tom), outgap(text, .6f, 0, 0, 0), b);
      body.add(LAY_AUTO, c);
   }

   private void said(String what)
   {
      // Also to the log, and not only to the label. This screen is the one people drive by hand on
      // a device or over ssh to find out whether a native capability actually worked -- and a
      // verdict that exists only as pixels is unreadable exactly then. It cost a round trip here:
      // a save that left no file on disk, with no way to tell a cancel from a failure.
      Debug.log("[files] " + what);
      answer.setText(what);
      answer.invalidateMeasure(); // the pinned band must grow with it (see Recipe.pin)
      Screen.repaint();
   }
}
Para una IA

Versiones de esta página legibles por máquina, para que un modelo lea el estante en vez de adivinar la API.

Este ejemplo, en JSONEste ejemplo, en Markdown
AnteriorEmpty, loading and brokenSiguienteVideo playback