How can I stream the selection of colors from the native macOS color picker to Flutter using EventChannels?

244 views Asked by At

I am attempting to add a feature to macos_ui that allows the user to launch the native macOS color picker and stream back their color selections via EventChannel.

I am able to launch the picker (Cocoa's NSColorPanel), but the color selection does not get streamed back. When I run the example application via XCode, I can see that the color selections are registered, but they do not seem to be streamed back to Flutter through the EventChannel. What is the proper way to handle this? Is it even possible to stream these events back to Flutter, given that they take place in a native macOS view?

Here is the Swift code I have so far:

MacosUIPlugin.swift

import Cocoa
import FlutterMacOS

public class MacOSUiPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
  private let colorPanelProvider: ColorPanelProvider
  private var eventSink: FlutterEventSink?

  init(colorPanelProvider: ColorPanelProvider) {
    self.colorPanelProvider = colorPanelProvider
    super.init()
  }
  
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(
      name: "dev.groovinchip.macos_ui",
      binaryMessenger: registrar.messenger)

    let colorSelectionChannel = FlutterEventChannel(
      name: "dev.groovinchip.macos_ui/color_panel",
      binaryMessenger: registrar.messenger)
  
    let colorPanelProvider = ColorPanelProvider()
      
    let instance = MacOSUiPlugin(colorPanelProvider: colorPanelProvider)
    colorSelectionChannel.setStreamHandler(instance)
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "color_panel":
      colorPanelProvider.openPanel()
      result(true)
    default:
      result(FlutterMethodNotImplemented)
    }
  }
  
  public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    print("listening to MacosUIPluginEvents")
    eventSink = events
    //colorPanelProvider.startStream()
    return nil
  }
  
  public func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    return nil
  }
}

extension NSColor {
  var hexString: String {
    let red = Int(round(self.redComponent * 0xFF))
    let green = Int(round(self.greenComponent * 0xFF))
    let blue = Int(round(self.blueComponent * 0xFF))
    let hexString = NSString(format: "#%02X%02X%02X", red, green, blue)
    return hexString as String
  }
}

ColorPanelProvider.swift

import FlutterMacOS

class ColorPanelProvider: NSObject, FlutterStreamHandler {
  var eventSink: FlutterEventSink?
  let colorPanel = NSColorPanel.shared

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    print("listening to ColorPanelProvider events")
    eventSink = events
    return nil
  }

  func openPanel() {
    colorPanel.setTarget(self)
    colorPanel.setAction(#selector(startStream))
    colorPanel.makeKeyAndOrderFront(self)
    colorPanel.isContinuous = true
    startStream()
  }

  @objc private func currentColor() -> String {
    print("currentColor: \(colorPanel.color.asFlutterHexString)")
    return colorPanel.color.asFlutterHexString
  }

  @objc public func startStream() {
    print("starting ColorPanelProvider stream")
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(currentColor),
      name: NSColorPanel.colorDidChangeNotification,
      object: colorPanel)
    
    eventSink?(currentColor())
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    return nil
  }
}

extension NSColor {
  var asFlutterHexString: String {
    let red = Int(round(self.redComponent * 0xFF))
    let green = Int(round(self.greenComponent * 0xFF))
    let blue = Int(round(self.blueComponent * 0xFF))
    let hexString = NSString(format: "#%02X%02X%02X", red, green, blue)
    return hexString.replacingOccurrences(of: "#", with: "0xFF") as String
  }
}

1

There are 1 answers

1
Stefan Blos On BEST ANSWER

You have been on the right track, but there's a few things that require some fixing. There's three things:

  1. In MacosUIPlugin.swift you need to listen to the colorChannelProvider instead of the instance in your register function. So you need to change the following line
colorSelectionChannel.setStreamHandler(instance)

to this:

colorSelectionChannel.setStreamHandler(colorChannelProvider)
  1. In ColorPanelProvider.swift you listen to color changes in startStream. While that is the correct way to do it you have to hand in the colorPanel that sends these changes so that the function can access the color with colorPanel.color. The function signature should look like this:
@objc public func startStream(colorPanel: NSColorPanel)
  1. When you register the startStream method as the target to receive updates from the colorPanel in openPanel() you need to set it up so that it will get called correctly. So change the second line of the method to look like this and everything works like a charm:
colorPanel.setAction(#selector(startStream(colorPanel:)))

Hopefully, this will fix everything. In case there's anymore problems, please let me know.