跳到内容
CrossFly 是 100% 开源在 GitHub 关注我们
CrossFlyCrossFly
开始使用
全部示例
配方设备249 行 · 1 个文件

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.

它展示了什么

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.

在你的机器上运行

每个示例都是普通的 Window,启动器只需要它的名字。

gradlew lwjgl3:run --args="classname dev.cf.recipe.R31Files"
修改它

Java 是提前编译的,浏览器无法自行重建你的修改 —— 这里的代码在发布前已由 TeaVM 转成 JavaScript。把文件复制到项目里,它在四个平台上原样运行;上面那条命令就是全部准备工作。

正在此处运行CrossFly 0.1.32
正在启动应用…

由 TeaVM 编译为 JavaScript —— 是同一份代码,不是录像。 · 全部示例

它用到了什么

本示例触及的框架类型。带链接的有参考页。

ButtonCapabilitiesCapabilityCardComponentDebugFileChooserGapGridLayoutLabelMaterialIconPalColorPdfPlatformUtilsScreenStylesVerticalLayoutWidgets

源码

完整无删减。这就是那个文件,不是片段。

原始
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();
   }
}
给 AI 助手

本页的机器可读版本,让模型直接读取示例集,而不是猜测 API。

本示例(JSON)本示例(Markdown)
上一个Empty, loading and broken下一个Video playback