Skip to content

Commit b1edb20

Browse files
committed
Add native linux titlebar
1 parent 6f9fde2 commit b1edb20

6 files changed

Lines changed: 118 additions & 28 deletions

File tree

app/lib/api/window.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/services.dart';
3+
import 'package:window_manager/window_manager.dart';
4+
5+
const _windowChannel = MethodChannel('linwood.dev/butterfly/window');
6+
7+
Future<void> applyNativeTitleBar(bool nativeTitleBar) async {
8+
if (kIsWeb) return;
9+
10+
// This still handles GtkHeaderBar on GNOME and the regular desktop
11+
// implementations on Windows, macOS and X11.
12+
await windowManager.setTitleBarStyle(
13+
nativeTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
14+
windowButtonVisibility: nativeTitleBar,
15+
);
16+
17+
if (defaultTargetPlatform == TargetPlatform.linux) {
18+
// gtk_window_set_decorated() is ineffective in GTK 3's Wayland backend.
19+
// Ask GDK to negotiate KDE's server/client decoration mode directly.
20+
await _windowChannel.invokeMethod<void>(
21+
'setNativeTitleBar',
22+
nativeTitleBar,
23+
);
24+
}
25+
}

app/lib/cubits/settings.dart

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:convert';
22

33
import 'package:butterfly/api/file_system.dart';
4+
import 'package:butterfly/api/window.dart';
45
import 'package:butterfly_api/butterfly_api.dart';
56
import 'package:collection/collection.dart';
67
import 'package:flutter/foundation.dart';
@@ -1352,20 +1353,15 @@ class SettingsCubit extends Cubit<ButterflySettings>
13521353
return save();
13531354
}
13541355

1355-
void setNativeTitleBar([bool? value]) {
1356+
Future<void> setNativeTitleBar([bool? value]) async {
13561357
if (kIsWeb || !isWindow) return;
1357-
windowManager.setTitleBarStyle(
1358-
(value ?? state.nativeTitleBar)
1359-
? TitleBarStyle.normal
1360-
: TitleBarStyle.hidden,
1361-
windowButtonVisibility: state.nativeTitleBar,
1362-
);
1358+
await applyNativeTitleBar(value ?? state.nativeTitleBar);
13631359
}
13641360

1365-
Future<void> changeNativeTitleBar(bool value, [bool modify = true]) {
1366-
if (modify) setNativeTitleBar(value);
1361+
Future<void> changeNativeTitleBar(bool value, [bool modify = true]) async {
1362+
if (modify) await setNativeTitleBar(value);
13671363
emit(state.copyWith(nativeTitleBar: value));
1368-
return save();
1364+
await save();
13691365
}
13701366

13711367
Future<void> changeSyncMode(SyncMode syncMode) {

app/lib/main.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ Future<void> main([List<String> args = const []]) async {
4444
talker.info('App started');
4545
usePathUrlStrategy();
4646

47-
await setup();
47+
final prefs = await SharedPreferences.getInstance();
48+
final settingsCubit = SettingsCubit(prefs);
49+
await setup(nativeTitleBar: settingsCubit.state.nativeTitleBar);
4850
var initialLocation = '/';
4951
final argParser = ArgParser();
5052
argParser.addOption('path', abbr: 'p');
5153
final result = argParser.parse(args);
52-
final prefs = await SharedPreferences.getInstance();
53-
final settingsCubit = SettingsCubit(prefs);
5454
Object? initialExtra;
5555
if (result.arguments.isNotEmpty && !kIsWeb) {
5656
var path = result.arguments[0].replaceAll('\\', '/');
@@ -323,7 +323,6 @@ class ButterflyApp extends StatelessWidget {
323323
if (!kIsWeb && isWindow) {
324324
windowManager.waitUntilReadyToShow(null, () async {
325325
settingsCubit.setTheme(context);
326-
settingsCubit.setNativeTitleBar();
327326
await windowManager.show();
328327
});
329328
}

app/lib/setup.dart

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:butterfly/api/window.dart';
12
import 'package:flutter/foundation.dart';
23
import 'package:flutter/material.dart';
34
import 'package:flutter/services.dart';
@@ -7,21 +8,21 @@ import 'package:window_manager/window_manager.dart';
78

89
import 'main.dart';
910

10-
Future<void> setup() async {
11+
Future<void> setup({required bool nativeTitleBar}) async {
1112
pdfrxFlutterInitialize();
1213
if (!kIsWeb && isWindow) {
1314
await windowManager.ensureInitialized();
14-
const kWindowOptions = WindowOptions(
15+
const windowOptions = WindowOptions(
1516
minimumSize: Size(410, 300),
1617
title: applicationName,
1718
backgroundColor: Colors.transparent,
1819
);
1920

20-
// Use it only after calling `hiddenWindowAtLaunch`
21-
await windowManager.waitUntilReadyToShow(kWindowOptions).then((_) async {
22-
await windowManager.setResizable(true);
23-
await windowManager.setPreventClose(false);
24-
});
21+
await windowManager.waitUntilReadyToShow(windowOptions);
22+
23+
await windowManager.setResizable(true);
24+
await windowManager.setPreventClose(false);
25+
await applyNativeTitleBar(nativeTitleBar);
2526
}
2627
setupFullScreen();
2728
setupLicenses();

app/lib/setup_web.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import 'package:flutter/services.dart';
33
import 'setup.dart' as general_setup;
44
import 'embed/action_web.dart' as action;
55

6-
Future<void> setup() async {
6+
Future<void> setup({required bool nativeTitleBar}) async {
77
await BrowserContextMenu.disableContextMenu();
88
action.setup();
9-
await general_setup.setup();
9+
await general_setup.setup(nativeTitleBar: nativeTitleBar);
1010
}

app/linux/runner/my_application.cc

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,75 @@
44
#ifdef GDK_WINDOWING_X11
55
#include <gdk/gdkx.h>
66
#endif
7+
#ifdef GDK_WINDOWING_WAYLAND
8+
#include <gdk/gdkwayland.h>
9+
#endif
710

811
#include "flutter/generated_plugin_registrant.h"
912

1013
struct _MyApplication {
1114
GtkApplication parent_instance;
1215
char** dart_entrypoint_arguments;
16+
FlMethodChannel* window_channel;
1317
};
1418

1519
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
1620

21+
static FlMethodResponse* set_native_title_bar(GtkWindow* window,
22+
FlValue* args) {
23+
if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_BOOL) {
24+
return FL_METHOD_RESPONSE(fl_method_error_response_new(
25+
"invalid-argument", "Expected a boolean nativeTitleBar value.",
26+
nullptr));
27+
}
28+
GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
29+
if (gdk_window == nullptr) {
30+
return FL_METHOD_RESPONSE(fl_method_error_response_new(
31+
"window-not-realized", "The GTK window has not been realized.",
32+
nullptr));
33+
}
34+
#ifdef GDK_WINDOWING_WAYLAND
35+
if (GDK_IS_WAYLAND_WINDOW(gdk_window)) {
36+
const gboolean native_title_bar = fl_value_get_bool(args);
37+
if (native_title_bar) {
38+
gdk_wayland_window_announce_ssd(gdk_window);
39+
} else {
40+
gdk_wayland_window_announce_csd(gdk_window);
41+
}
42+
}
43+
#endif
44+
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
45+
}
46+
47+
static void window_method_call_cb(FlMethodChannel* channel,
48+
FlMethodCall* method_call,
49+
gpointer user_data) {
50+
GtkWindow* window = GTK_WINDOW(user_data);
51+
g_autoptr(FlMethodResponse) response = nullptr;
52+
if (g_strcmp0(fl_method_call_get_name(method_call), "setNativeTitleBar") ==
53+
0) {
54+
response = set_native_title_bar(window, fl_method_call_get_args(method_call));
55+
} else {
56+
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
57+
}
58+
g_autoptr(GError) error = nullptr;
59+
if (!fl_method_call_respond(method_call, response, &error)) {
60+
g_warning("Failed to send window method response: %s", error->message);
61+
}
62+
}
63+
64+
static void create_window_channel(MyApplication* self,
65+
FlView* view,
66+
GtkWindow* window) {
67+
FlEngine* engine = fl_view_get_engine(view);
68+
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(engine);
69+
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
70+
self->window_channel = fl_method_channel_new(
71+
messenger, "linwood.dev/butterfly/window", FL_METHOD_CODEC(codec));
72+
fl_method_channel_set_method_call_handler(
73+
self->window_channel, window_method_call_cb, window, nullptr);
74+
}
75+
1776
// Called when first Flutter frame received.
1877
static void first_frame_cb(MyApplication* self, FlView *view)
1978
{
@@ -33,16 +92,24 @@ static void my_application_activate(GApplication* application) {
3392
// in case the window manager does more exotic layout, e.g. tiling.
3493
// If running on Wayland assume the header bar will work (may need changing
3594
// if future cases occur).
36-
gboolean use_header_bar = TRUE;
95+
gboolean use_header_bar = FALSE;
3796
#ifdef GDK_WINDOWING_X11
38-
GdkScreen* screen = gtk_window_get_screen(window);
97+
GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(window));
3998
if (GDK_IS_X11_SCREEN(screen)) {
4099
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
41-
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
42-
use_header_bar = FALSE;
100+
if (g_strcmp0(wm_name, "GNOME Shell") == 0) {
101+
use_header_bar = TRUE;
43102
}
44-
}
103+
} else
45104
#endif
105+
{
106+
const gchar* current_desktop = g_getenv("XDG_CURRENT_DESKTOP");
107+
if (current_desktop != nullptr) {
108+
g_auto(GStrv) desktops = g_strsplit(current_desktop, ":", -1);
109+
use_header_bar = g_strv_contains(
110+
reinterpret_cast<const gchar* const*>(desktops), "GNOME");
111+
}
112+
}
46113
if (use_header_bar) {
47114
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
48115
gtk_widget_show(GTK_WIDGET(header_bar));
@@ -73,6 +140,7 @@ static void my_application_activate(GApplication* application) {
73140

74141
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
75142

143+
create_window_channel(self, view, window);
76144
gtk_widget_grab_focus(GTK_WIDGET(view));
77145
}
78146

@@ -116,6 +184,7 @@ static void my_application_shutdown(GApplication* application) {
116184
// Implements GObject::dispose.
117185
static void my_application_dispose(GObject* object) {
118186
MyApplication* self = MY_APPLICATION(object);
187+
g_clear_object(&self->window_channel);
119188
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
120189
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
121190
}

0 commit comments

Comments
 (0)