Skip to content

Commit 7743ba8

Browse files
committed
[windows] Implement popUp metnod
1 parent bf585b3 commit 7743ba8

File tree

6 files changed

+228
-31
lines changed

6 files changed

+228
-31
lines changed

.clang-format

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Defines the Chromium style for automatic reformatting.
2+
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
3+
BasedOnStyle: Chromium
4+
# This defaults to 'Auto'. Explicitly set it for a while, so that
5+
# 'vector<vector<int> >' in existing files gets formatted to
6+
# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
7+
# 'int>>' if the file already contains at least one such instance.)
8+
Standard: Cpp11
9+
SortIncludes: true
10+
---
11+
Language: ObjC
12+
ColumnLimit: 100

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# The .vscode folder contains launch configuration and tasks you configure in
1919
# VS Code which you may wish to be included in version control, so this line
2020
# is commented out by default.
21-
#.vscode/
21+
.vscode/
2222

2323
# Flutter/Dart/Pub related
2424
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.

example/lib/pages/home.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:bot_toast/bot_toast.dart';
2+
import 'package:flutter/foundation.dart';
23
import 'package:flutter/gestures.dart';
34
import 'package:flutter/material.dart';
45
import 'package:preference_list/preference_list.dart';
@@ -14,6 +15,7 @@ class HomePage extends StatefulWidget {
1415
class _HomePageState extends State<HomePage> {
1516
bool _shouldReact = false;
1617
Offset? _position;
18+
Placement _placement = Placement.bottomLeft;
1719

1820
Menu? _menu;
1921

@@ -85,7 +87,7 @@ class _HomePageState extends State<HomePage> {
8587
popUpContextualMenu(
8688
_menu!,
8789
position: _position,
88-
placement: Placement.topRight,
90+
placement: _placement,
8991
);
9092
}
9193

@@ -97,6 +99,18 @@ class _HomePageState extends State<HomePage> {
9799
children: [
98100
PreferenceListItem(
99101
title: const Text('popUp'),
102+
accessoryView: ToggleButtons(
103+
children: <Widget>[
104+
for (var placement in Placement.values)
105+
Text('${describeEnum(placement)}'),
106+
],
107+
onPressed: (int index) async {
108+
_placement = Placement.values[index];
109+
setState(() {});
110+
},
111+
isSelected:
112+
Placement.values.map((e) => e == _placement).toList(),
113+
),
100114
onTap: () {
101115
_position = null;
102116
_handleClickPopUp();

lib/src/contextual_menu.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:ui';
23

34
import 'package:flutter/foundation.dart';
45
import 'package:flutter/services.dart';
@@ -37,6 +38,7 @@ class _ContextualMenu with MenuBehavior {
3738
}) async {
3839
_menu = menu;
3940
final Map<String, dynamic> arguments = {
41+
'devicePixelRatio': window.devicePixelRatio,
4042
'menu': menu.toJson(),
4143
'position': position != null
4244
? {
@@ -53,7 +55,7 @@ class _ContextualMenu with MenuBehavior {
5355
Future<void> popUpContextualMenu(
5456
Menu menu, {
5557
Offset? position,
56-
Placement placement = Placement.topLeft,
58+
Placement placement = Placement.bottomRight,
5759
}) {
5860
return _ContextualMenu.instance.popUp(
5961
menu,

windows/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ apply_standard_settings(${PLUGIN_NAME})
1313
set_target_properties(${PLUGIN_NAME} PROPERTIES
1414
CXX_VISIBILITY_PRESET hidden)
1515
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
16+
target_compile_definitions(${PLUGIN_NAME} PRIVATE _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
1617
target_include_directories(${PLUGIN_NAME} INTERFACE
1718
"${CMAKE_CURRENT_SOURCE_DIR}/include")
1819
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)

windows/contextual_menu_plugin.cpp

Lines changed: 196 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,238 @@
33
// This must be included before many other Windows headers.
44
#include <windows.h>
55

6-
// For getPlatformVersion; remove unless needed for your plugin implementation.
7-
#include <VersionHelpers.h>
8-
96
#include <flutter/method_channel.h>
107
#include <flutter/plugin_registrar_windows.h>
118
#include <flutter/standard_method_codec.h>
129

10+
#include <codecvt>
1311
#include <map>
1412
#include <memory>
1513
#include <sstream>
1614

15+
using flutter::EncodableList;
16+
using flutter::EncodableMap;
17+
using flutter::EncodableValue;
18+
1719
namespace {
1820

21+
const EncodableValue* ValueOrNull(const EncodableMap& map, const char* key) {
22+
auto it = map.find(EncodableValue(key));
23+
if (it == map.end()) {
24+
return nullptr;
25+
}
26+
return &(it->second);
27+
}
28+
std::unique_ptr<flutter::MethodChannel<EncodableValue>,
29+
std::default_delete<flutter::MethodChannel<EncodableValue>>>
30+
channel = nullptr;
1931
class ContextualMenuPlugin : public flutter::Plugin {
2032
public:
21-
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
33+
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
2234

23-
ContextualMenuPlugin();
35+
ContextualMenuPlugin(flutter::PluginRegistrarWindows* registrar);
2436

2537
virtual ~ContextualMenuPlugin();
2638

2739
private:
40+
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> g_converter;
41+
42+
flutter::PluginRegistrarWindows* registrar;
43+
44+
// The ID of the WindowProc delegate registration.
45+
int window_proc_id = -1;
46+
47+
HMENU hMenu;
48+
49+
void ContextualMenuPlugin::_CreateMenu(HMENU menu, EncodableMap args);
50+
51+
// Called for top-level WindowProc delegation.
52+
std::optional<LRESULT> ContextualMenuPlugin::HandleWindowProc(HWND hwnd,
53+
UINT message,
54+
WPARAM wparam,
55+
LPARAM lparam);
56+
HWND ContextualMenuPlugin::GetMainWindow();
57+
void ContextualMenuPlugin::PopUp(
58+
const flutter::MethodCall<EncodableValue>& method_call,
59+
std::unique_ptr<flutter::MethodResult<EncodableValue>> result);
60+
2861
// Called when a method is called on this plugin's channel from Dart.
2962
void HandleMethodCall(
30-
const flutter::MethodCall<flutter::EncodableValue> &method_call,
31-
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
63+
const flutter::MethodCall<EncodableValue>& method_call,
64+
std::unique_ptr<flutter::MethodResult<EncodableValue>> result);
3265
};
3366

3467
// static
3568
void ContextualMenuPlugin::RegisterWithRegistrar(
36-
flutter::PluginRegistrarWindows *registrar) {
37-
auto channel =
38-
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
39-
registrar->messenger(), "contextual_menu",
40-
&flutter::StandardMethodCodec::GetInstance());
69+
flutter::PluginRegistrarWindows* registrar) {
70+
channel = std::make_unique<flutter::MethodChannel<EncodableValue>>(
71+
registrar->messenger(), "contextual_menu",
72+
&flutter::StandardMethodCodec::GetInstance());
4173

42-
auto plugin = std::make_unique<ContextualMenuPlugin>();
74+
auto plugin = std::make_unique<ContextualMenuPlugin>(registrar);
4375

4476
channel->SetMethodCallHandler(
45-
[plugin_pointer = plugin.get()](const auto &call, auto result) {
77+
[plugin_pointer = plugin.get()](const auto& call, auto result) {
4678
plugin_pointer->HandleMethodCall(call, std::move(result));
4779
});
4880

4981
registrar->AddPlugin(std::move(plugin));
5082
}
5183

52-
ContextualMenuPlugin::ContextualMenuPlugin() {}
84+
ContextualMenuPlugin::ContextualMenuPlugin(
85+
flutter::PluginRegistrarWindows* registrar)
86+
: registrar(registrar) {
87+
window_proc_id = registrar->RegisterTopLevelWindowProcDelegate(
88+
[this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
89+
return HandleWindowProc(hwnd, message, wparam, lparam);
90+
});
91+
}
5392

5493
ContextualMenuPlugin::~ContextualMenuPlugin() {}
5594

56-
void ContextualMenuPlugin::HandleMethodCall(
57-
const flutter::MethodCall<flutter::EncodableValue> &method_call,
58-
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
59-
if (method_call.method_name().compare("getPlatformVersion") == 0) {
60-
std::ostringstream version_stream;
61-
version_stream << "Windows ";
62-
if (IsWindows10OrGreater()) {
63-
version_stream << "10+";
64-
} else if (IsWindows8OrGreater()) {
65-
version_stream << "8";
66-
} else if (IsWindows7OrGreater()) {
67-
version_stream << "7";
95+
void ContextualMenuPlugin::_CreateMenu(HMENU menu, EncodableMap args) {
96+
EncodableList items =
97+
std::get<EncodableList>(args.at(EncodableValue("items")));
98+
99+
int count = GetMenuItemCount(menu);
100+
for (int i = 0; i < count; i++) {
101+
// always remove at 0 because they shift every time
102+
RemoveMenu(menu, 0, MF_BYPOSITION);
103+
}
104+
105+
for (EncodableValue item_value : items) {
106+
EncodableMap item_map = std::get<EncodableMap>(item_value);
107+
int id = std::get<int>(item_map.at(EncodableValue("id")));
108+
std::string type =
109+
std::get<std::string>(item_map.at(EncodableValue("type")));
110+
std::string label =
111+
std::get<std::string>(item_map.at(EncodableValue("label")));
112+
auto* checked = std::get_if<bool>(ValueOrNull(item_map, "checked"));
113+
bool disabled = std::get<bool>(item_map.at(EncodableValue("disabled")));
114+
115+
UINT_PTR item_id = id;
116+
UINT uFlags = MF_STRING;
117+
118+
if (disabled) {
119+
uFlags |= MF_GRAYED;
120+
}
121+
122+
if (type.compare("separator") == 0) {
123+
AppendMenuW(menu, MF_SEPARATOR, item_id, NULL);
124+
} else {
125+
if (type.compare("checkbox") == 0) {
126+
if (checked == nullptr) {
127+
// skip
128+
} else {
129+
uFlags |= (*checked == true ? MF_CHECKED : MF_UNCHECKED);
130+
}
131+
} else if (type.compare("submenu") == 0) {
132+
uFlags |= MF_POPUP;
133+
HMENU sub_menu = ::CreatePopupMenu();
134+
_CreateMenu(sub_menu, std::get<EncodableMap>(
135+
item_map.at(EncodableValue("submenu"))));
136+
item_id = reinterpret_cast<UINT_PTR>(sub_menu);
137+
}
138+
AppendMenuW(menu, uFlags, item_id, g_converter.from_bytes(label).c_str());
68139
}
69-
result->Success(flutter::EncodableValue(version_stream.str()));
140+
}
141+
}
142+
143+
std::optional<LRESULT> ContextualMenuPlugin::HandleWindowProc(HWND hWnd,
144+
UINT message,
145+
WPARAM wParam,
146+
LPARAM lParam) {
147+
std::optional<LRESULT> result;
148+
if (message == WM_COMMAND) {
149+
flutter::EncodableMap eventData = flutter::EncodableMap();
150+
eventData[flutter::EncodableValue("id")] =
151+
flutter::EncodableValue((int)wParam);
152+
153+
channel->InvokeMethod("onMenuItemClick",
154+
std::make_unique<flutter::EncodableValue>(eventData));
155+
}
156+
return std::nullopt;
157+
}
158+
159+
HWND ContextualMenuPlugin::GetMainWindow() {
160+
return ::GetAncestor(registrar->GetView()->GetNativeWindow(), GA_ROOT);
161+
}
162+
163+
void ContextualMenuPlugin::PopUp(
164+
const flutter::MethodCall<EncodableValue>& method_call,
165+
std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {
166+
HWND hWnd = GetMainWindow();
167+
168+
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
169+
170+
std::string placement =
171+
std::get<std::string>(args.at(EncodableValue("placement")));
172+
double device_pixel_ratio =
173+
std::get<double>(args.at(flutter::EncodableValue("devicePixelRatio")));
174+
175+
hMenu = CreatePopupMenu();
176+
_CreateMenu(hMenu, std::get<EncodableMap>(args.at(EncodableValue("menu"))));
177+
178+
double x, y;
179+
180+
UINT uFlags = TPM_TOPALIGN;
181+
182+
POINT cursorPos;
183+
GetCursorPos(&cursorPos);
184+
x = cursorPos.x;
185+
y = cursorPos.y;
186+
187+
if (args.find(EncodableValue("position")) != args.end()) {
188+
const EncodableMap& position =
189+
std::get<EncodableMap>(args.at(EncodableValue("position")));
190+
191+
double position_x =
192+
std::get<double>(position.at(flutter::EncodableValue("x")));
193+
double position_y =
194+
std::get<double>(position.at(flutter::EncodableValue("y")));
195+
196+
RECT window_rect, client_rect;
197+
TITLEBARINFOEX title_bar_info;
198+
199+
GetWindowRect(hWnd, &window_rect);
200+
GetClientRect(hWnd, &client_rect);
201+
title_bar_info.cbSize = sizeof(TITLEBARINFOEX);
202+
::SendMessage(hWnd, WM_GETTITLEBARINFOEX, 0, (LPARAM)&title_bar_info);
203+
int32_t title_bar_height =
204+
title_bar_info.rcTitleBar.bottom == 0
205+
? 0
206+
: title_bar_info.rcTitleBar.bottom - title_bar_info.rcTitleBar.top;
207+
208+
int border_thickness =
209+
((window_rect.right - window_rect.left) - client_rect.right) / 2;
210+
211+
x = static_cast<double>((position_x * device_pixel_ratio) +
212+
(window_rect.left + border_thickness));
213+
y = static_cast<double>((position_y * device_pixel_ratio) +
214+
(window_rect.top + title_bar_height));
215+
}
216+
217+
if (placement.compare("topLeft") == 0) {
218+
uFlags = TPM_BOTTOMALIGN | TPM_RIGHTALIGN;
219+
} else if (placement.compare("topRight") == 0) {
220+
uFlags = TPM_BOTTOMALIGN | TPM_LEFTALIGN;
221+
} else if (placement.compare("bottomLeft") == 0) {
222+
uFlags = TPM_TOPALIGN | TPM_RIGHTALIGN;
223+
} else if (placement.compare("bottomRight") == 0) {
224+
uFlags = TPM_TOPALIGN | TPM_LEFTALIGN;
225+
}
226+
227+
TrackPopupMenu(hMenu, uFlags, static_cast<int>(x), static_cast<int>(y), 0,
228+
hWnd, NULL);
229+
230+
result->Success(EncodableValue(true));
231+
}
232+
233+
void ContextualMenuPlugin::HandleMethodCall(
234+
const flutter::MethodCall<EncodableValue>& method_call,
235+
std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {
236+
if (method_call.method_name().compare("popUp") == 0) {
237+
PopUp(method_call, std::move(result));
70238
} else {
71239
result->NotImplemented();
72240
}

0 commit comments

Comments
 (0)