Created
June 21, 2023 14:24
-
-
Save whiplashoo/f2111c4e4ea7feff581ee0b1a7c42c95 to your computer and use it in GitHub Desktop.
Adding a Menu Bar extra (icon) to a Flutter macOS app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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