Skip to content

Instantly share code, notes, and snippets.

@whiplashoo
Created June 21, 2023 14:24
Show Gist options
  • Save whiplashoo/f2111c4e4ea7feff581ee0b1a7c42c95 to your computer and use it in GitHub Desktop.
Save whiplashoo/f2111c4e4ea7feff581ee0b1a7c42c95 to your computer and use it in GitHub Desktop.
Adding a Menu Bar extra (icon) to a Flutter macOS app
import Cocoa
import FlutterMacOS
import SwiftUI
// Relevant blog post: https://blog.whidev.com/menu-bar-extra-flutter-macos-app
struct Shortcut {
var combination: String
var description: String
var createdAt: Int
}
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
var statusBar: NSStatusItem?
var flutterViewController: FlutterViewController?
var popover = NSPopover()
override func applicationWillFinishLaunching(_ notification: Notification) {
let flutterViewController = NSApplication.shared.windows.first?.contentViewController as? FlutterViewController
self.flutterViewController = flutterViewController
}
override func applicationDidFinishLaunching(_ aNotification: Notification) {
statusBar = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = statusBar?.button {
button.image = NSImage(named: "MenuBarIcon")
button.action = #selector(togglePopover(_:))
}
}
@objc func togglePopover(_ sender: AnyObject) {
if let button = statusBar?.button {
if popover.isShown {
popover.performClose(sender)
} else {
let frontmostAppName = NSWorkspace.shared.frontmostApplication?.localizedName
if let flutterViewController = flutterViewController {
let statusBarChannel = FlutterMethodChannel(name: "com.sk.statusbar", binaryMessenger: flutterViewController.engine.binaryMessenger)
statusBarChannel.invokeMethod("getShortcutsForApp", arguments: frontmostAppName) { (result: Any?) in
if let error = result as? FlutterError {
print("Error Occurred: \(error.message ?? "")")
} else if let shortcutsJSON = result as? [[String: Any]] {
var shortcuts: [Shortcut] = []
shortcutsJSON.forEach { item in
if let combination = item["combination"] as? String,
let createdAt = item["createdAt"] as? Int,
let description = item["description"] as? String {
shortcuts.append(Shortcut(combination: combination, description: description, createdAt: createdAt ))
}
}
if #available(macOS 10.15, *) {
let contentView = PopoverContentView(appName: frontmostAppName!, shortcuts: shortcuts)
let popover = NSPopover()
popover.contentSize = NSSize(width: 300, height: 400)
popover.contentViewController = NSHostingController(rootView: contentView)
popover.behavior = .transient
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popover = popover
}
}
}
}
}
}
}
}
struct PopoverContentView: View {
var appName: String
var shortcuts: [Shortcut]
init(appName: String, shortcuts: [Shortcut]) {
self.appName = appName
self.shortcuts = shortcuts
}
var body: some View {
ZStack {
VStack{
Text("\(appName) shortcuts:")
.font(.headline)
.padding()
if shortcuts.isEmpty {
VStack {
Text("No shortcuts saved for \(appName).").font(.headline)
Text("Go to Shortcut Keeper to \n add shortcuts for this app.")
.multilineTextAlignment(.center).padding()
}.padding()
} else {
shortcutsList
}
}
}
}
var shortcutsList: some View {
List(shortcuts, id: \.createdAt) { item in
HStack {
Text(item.combination)
.font(.subheadline)
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
Text(item.description)
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
const platform = MethodChannel('com.sk.statusbar');
platform.setMethodCallHandler((MethodCall call) async {
if (call.method == "getShortcutsForApp") {
try {
String appName = call.arguments.toString();
//final shortcutsList = getShortcutsForApp(appName);
final shortcutsList = [
{
"combination": "Shift-Command-I",
"description": "Open Developer Tools",
"app": "Microsoft Visual Studio Code",
"createdAt": 1686733115884
},
{
"combination": "Control-Command-T",
"description": "Tile tabs vertically",
"app": "Microsoft Visual Studio Code",
"createdAt": 1686733115883
},
{
"combination": "Control-Shift-Command-T",
"description": "Tile tabs horizontally",
"app": "Microsoft Visual Studio Code",
"createdAt": 1686733115882
}
];
return shortcutsList;
} catch (e, stackTrace) {
debugPrint("Error Occurred: $e\nStackTrace: $stackTrace");
return null;
}
}
});
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter macOS with menu bar icon',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter macOS with menu bar icon'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment