#include "flutter_window.h" #include "utils.h" #include "file_info.h" #include "file_icon.h" #include #include "flutter/generated_plugin_registrant.h" void trayMessageHandler(TrayIcon *, UINT); HICON getIconHandle(std::string); const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key); static std::unique_ptr> appChannel; static std::unique_ptr> trayChannel; FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); // register channel registerMethodChannel(flutter_controller_->engine()); // setup trayicon m_tray.setUserData(this); m_tray.setListener(trayMessageHandler); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { m_tray.setVisible(false); if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } void FlutterWindow::SetMinimumSize(SIZE size) { min_window_size_ = size; } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // my custom message handler std::optional r = handleMessage(hwnd, message, wparam, lparam); if (r) { return *r; } // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; case WM_GETMINMAXINFO: if (min_window_size_.cx > 0 && min_window_size_.cy > 0) { LPMINMAXINFO info = reinterpret_cast(lparam); info->ptMinTrackSize.x = min_window_size_.cx; info->ptMinTrackSize.y = min_window_size_.cy; } break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } void FlutterWindow::registerMethodChannel(flutter::FlutterEngine* engine) { appChannel = std::make_unique>( engine->messenger(), "app.nomo/app", &flutter::StandardMethodCodec::GetInstance()); appChannel->SetMethodCallHandler([this](const auto& call, auto result) { handleAppMethodCall(call, std::move(result)); }); trayChannel = std::make_unique>( engine->messenger(), "app.nomo/tray", &flutter::StandardMethodCodec::GetInstance()); trayChannel->SetMethodCallHandler([this](const auto& call, auto result) { handleTrayMethodCall(call, std::move(result)); }); } void FlutterWindow::handleAppMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result) { if (method_call.method_name().compare("activeWindow") == 0) { onActiveWindow(); result->Success(flutter::EncodableValue(true)); // } else if (method_call.method_name().compare("hideWindow") == 0) { onHideWindow(); result->Success(flutter::EncodableValue(true)); // } else if (method_call.method_name().compare("toggleWindow") == 0) { onToggleWindow(); result->Success(flutter::EncodableValue(true)); // } else if (method_call.method_name().compare("destroyWindow") == 0) { Destroy(); result->Success(flutter::EncodableValue(true)); // } else if (method_call.method_name().compare("extractFileIcon") == 0) { extractFileIcon(method_call, std::move(result)); // } else if (method_call.method_name().compare("getFileDescription") == 0) { getFileDescription(method_call, std::move(result)); // } else { result->NotImplemented(); } } void FlutterWindow::handleTrayMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result) { if (method_call.method_name().compare("setSystemTray") == 0) { setSystemTray(method_call, std::move(result)); // } else if (method_call.method_name().compare("resetSystemTray") == 0) { resetSystemTray(method_call, std::move(result)); // } else if (method_call.method_name().compare("setContextMenu") == 0) { setContextMenu(method_call, std::move(result)); // } else if (method_call.method_name().compare("popupContextMenu") == 0) { popUpContextMenu(method_call, std::move(result)); // } else { result->NotImplemented(); } } std::optional FlutterWindow::handleMessage(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) { if (message == WM_CLOSE) { if (m_tray.isVisible()) { onHideWindow(); } else { Destroy(); } return std::make_optional(0); } if (message == WM_COMMAND) { flutter::EncodableMap eventData = flutter::EncodableMap(); eventData[flutter::EncodableValue("id")] = flutter::EncodableValue((int)wparam); trayChannel->InvokeMethod("onTrayMenuItemClick", std::make_unique(eventData)); } return std::nullopt; } void FlutterWindow::onActiveWindow() { bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0; if (isVisible) { WINDOWPLACEMENT windowPlacement; GetWindowPlacement(GetHandle(), &windowPlacement); if (windowPlacement.showCmd == SW_SHOWMINIMIZED) { PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0); } } else { onShowWindow(); } } void FlutterWindow::onToggleWindow() { bool isVisible = (::GetWindowLong(GetHandle(), GWL_STYLE) & WS_VISIBLE) != 0; if (isVisible) { WINDOWPLACEMENT windowPlacement; GetWindowPlacement(GetHandle(), &windowPlacement); if (windowPlacement.showCmd == SW_SHOWMINIMIZED) { PostMessage(GetHandle(), WM_SYSCOMMAND, SC_RESTORE, 0); } else { onHideWindow(); } } else { onShowWindow(); } } void FlutterWindow::onShowWindow() { ShowWindow(GetHandle(), SW_SHOW); SetForegroundWindow(GetHandle()); auto args = std::make_unique(nullptr); appChannel->InvokeMethod("onWindowShow", std::move(args)); } void FlutterWindow::onHideWindow() { ShowWindow(GetHandle(), SW_HIDE); auto args = std::make_unique(nullptr); appChannel->InvokeMethod("onWindowHide", std::move(args)); } void FlutterWindow::onTrayIconLButtonDown() { auto args = std::make_unique(nullptr); trayChannel->InvokeMethod("onTrayIconLButtonDown", std::move(args)); } void FlutterWindow::onTrayIconLButtonUp() { auto args = std::make_unique(nullptr); trayChannel->InvokeMethod("onTrayIconLButtonUp", std::move(args)); } void FlutterWindow::onTrayIconRButtonDown() { auto args = std::make_unique(nullptr); trayChannel->InvokeMethod("onTrayIconRButtonDown", std::move(args)); } void FlutterWindow::onTrayIconRButtonUp() { auto args = std::make_unique(nullptr); trayChannel->InvokeMethod("onTrayIconRButtonUp", std::move(args)); } void FlutterWindow::setSystemTray( const flutter::MethodCall& method_call, std::unique_ptr> result) { const flutter::EncodableMap& args = std::get(*method_call.arguments()); std::string tooltip = std::get(args.at(flutter::EncodableValue("tooltip"))); std::string icon = std::get(args.at(flutter::EncodableValue("icon"))); m_tray.setName((LPCWSTR)(Utf16FromUtf8(tooltip.c_str()).c_str())); m_tray.setIcon(getIconHandle(icon)); m_tray.setVisible(true); result->Success(flutter::EncodableValue(true)); } void FlutterWindow::resetSystemTray( const flutter::MethodCall& method_call, std::unique_ptr> result) { m_tray.setVisible(false); result->Success(flutter::EncodableValue(true)); } void FlutterWindow::setContextMenu( const flutter::MethodCall& method_call, std::unique_ptr> result) { const flutter::EncodableMap& args = std::get(*method_call.arguments()); m_menuHandle = CreatePopupMenu(); createMenu(m_menuHandle, std::get( args.at(flutter::EncodableValue("menu")))); result->Success(flutter::EncodableValue(true)); } void FlutterWindow::popUpContextMenu( const flutter::MethodCall& method_call, std::unique_ptr> result) { POINT pt; if (GetCursorPos(&pt)) { ::SetForegroundWindow(GetHandle()); TrackPopupMenu(m_menuHandle, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, GetHandle(), NULL); } result->Success(flutter::EncodableValue(true)); } void FlutterWindow::extractFileIcon( const flutter::MethodCall& method_call, std::unique_ptr> result) { const flutter::EncodableMap& args = std::get(*method_call.arguments()); std::string fileName = std::get(args.at(flutter::EncodableValue("fileName"))); std::string iconName = std::get(args.at(flutter::EncodableValue("iconName"))); auto ret = SaveIcon(fileName, iconName, 32, 0); result->Success(flutter::EncodableValue(ret)); } void FlutterWindow::getFileDescription( const flutter::MethodCall& method_call, std::unique_ptr> result) { const flutter::EncodableMap& args = std::get(*method_call.arguments()); std::wstring fileName = Utf16FromUtf8(std::get(args.at(flutter::EncodableValue("fileName"))).c_str()); std::wstring description; FileInfoUtils::GetFileDescription(fileName, description); std::wcout << fileName << " " << description << std::endl; result->Success(flutter::EncodableValue(utf8_encode(description))); } void FlutterWindow::createMenu(HMENU menu, flutter::EncodableMap args) { flutter::EncodableList items = std::get( args.at(flutter::EncodableValue("items"))); int count = GetMenuItemCount(menu); for (int i = 0; i < count; i++) { // always remove at 0 because they shift every time RemoveMenu(menu, 0, MF_BYPOSITION); } for (flutter::EncodableValue item_value : items) { flutter::EncodableMap item_map = std::get(item_value); int id = std::get(item_map.at(flutter::EncodableValue("id"))); std::string type = std::get(item_map.at(flutter::EncodableValue("type"))); std::string label = std::get(item_map.at(flutter::EncodableValue("label"))); auto* checked = std::get_if(ValueOrNull(item_map, "checked")); bool disabled = std::get(item_map.at(flutter::EncodableValue("disabled"))); UINT_PTR item_id = id; UINT uFlags = MF_STRING; if (disabled) { uFlags |= MF_GRAYED; } if (type.compare("separator") == 0) { AppendMenuW(menu, MF_SEPARATOR, item_id, NULL); } else { if (type.compare("checkbox") == 0) { if (checked == nullptr) { // skip } else { uFlags |= (*checked == true ? MF_CHECKED : MF_UNCHECKED); } } else if (type.compare("submenu") == 0) { uFlags |= MF_POPUP; HMENU sub_menu = ::CreatePopupMenu(); createMenu(sub_menu, std::get(item_map.at( flutter::EncodableValue("submenu")))); item_id = reinterpret_cast(sub_menu); } AppendMenuW(menu, uFlags, item_id, (LPCWSTR)(Utf16FromUtf8(label.c_str()).c_str())); } } } void trayMessageHandler(TrayIcon *pTrayIcon, UINT uMsg) { switch (uMsg) { case WM_LBUTTONDOWN: ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonDown(); break; case WM_LBUTTONUP: ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconLButtonUp(); break; case WM_RBUTTONDOWN: ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonDown(); break; case WM_RBUTTONUP: ((FlutterWindow *)pTrayIcon->getUserData())->onTrayIconRButtonUp(); break; } } HICON getIconHandle(std::string iconPath) { return static_cast(LoadImage(NULL, (LPCWSTR)(Utf16FromUtf8(iconPath.c_str()).c_str()), IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED)); } const flutter::EncodableValue* ValueOrNull(const flutter::EncodableMap& map, const char* key) { auto it = map.find(flutter::EncodableValue(key)); if (it == map.end()) { return nullptr; } return &(it->second); }