diff --git a/Cargo.lock b/Cargo.lock index b8b0e96..6b8ef0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ dependencies = [ "cc", "chewing_capi", "embed-resource", + "getrandom", "log", "nine_patch_drawable", "win_dbg_logger", diff --git a/chewing_tip/CClassFactory.cpp b/chewing_tip/CClassFactory.cpp index 50ec36b..84caaa5 100644 --- a/chewing_tip/CClassFactory.cpp +++ b/chewing_tip/CClassFactory.cpp @@ -60,4 +60,4 @@ STDMETHODIMP CClassFactory::LockServer(BOOL fLock) { return S_OK; } -} // namespace Chewing \ No newline at end of file +} // namespace Chewing diff --git a/chewing_tip/CClassFactory.h b/chewing_tip/CClassFactory.h index 140e021..341e66a 100644 --- a/chewing_tip/CClassFactory.h +++ b/chewing_tip/CClassFactory.h @@ -19,9 +19,9 @@ class CClassFactory : public IClassFactory { STDMETHODIMP LockServer(BOOL fLock); private: - ~CClassFactory() {} + virtual ~CClassFactory() {} unsigned long refCount_; }; -} // namespace Chewing \ No newline at end of file +} // namespace Chewing diff --git a/chewing_tip/Cargo.toml b/chewing_tip/Cargo.toml index 808c6b7..ca86d3b 100644 --- a/chewing_tip/Cargo.toml +++ b/chewing_tip/Cargo.toml @@ -32,6 +32,7 @@ windows = { version = "0.58.0", features = [ "Win32_UI_TextServices", "Win32_UI_WindowsAndMessaging", ] } +getrandom = "0.2.15" [build-dependencies] anyhow = "1.0.95" diff --git a/chewing_tip/ChewingTextService.cpp b/chewing_tip/ChewingTextService.cpp index b278714..9186719 100644 --- a/chewing_tip/ChewingTextService.cpp +++ b/chewing_tip/ChewingTextService.cpp @@ -24,11 +24,16 @@ #include #include +#include +#include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -36,7 +41,7 @@ #include #include -#include "LangBarButton.h" +#include "TextService.h" #include "Utils.h" #include "resource.h" #include "libime2.h" @@ -82,7 +87,6 @@ TextService::TextService(): outputSimpChinese_(false), lastKeyDownCode_(0), messageTimerId_(0), - imeModeIcon_(NULL), symbolsFileTime_(0), chewingContext_(NULL) { @@ -95,34 +99,77 @@ TextService::TextService(): // add language bar buttons // siwtch Chinese/English modes - switchLangButton_ = new Ime::LangBarButton(this, g_modeButtonGuid, ID_SWITCH_LANG); - switchLangButton_->setTooltip(IDS_SWITCH_LANG); - addButton(switchLangButton_); + TF_LANGBARITEMINFO info = { + clsid(), + g_modeButtonGuid, + TF_LBI_STYLE_BTN_BUTTON, + 0, + {} + }; + LPCWSTR tooltip; + int len; + len = LoadStringW(g_hInstance, IDS_SWITCH_LANG, (LPWSTR)&tooltip, 0); + wcsncpy_s(info.szDescription, sizeof(info.szDescription), tooltip, len); + CreateLangBarButton( + info, + SysAllocStringLen(tooltip, len), + LoadIconW(g_hInstance, MAKEINTRESOURCEW(IDI_CHI)), + NULL, + ID_SWITCH_LANG, + this, + switchLangButton_.put_void() + ); + addButton(switchLangButton_.get()); // toggle full shape/half shape - switchShapeButton_ = new Ime::LangBarButton(this, g_shapeTypeButtonGuid, ID_SWITCH_SHAPE); - switchShapeButton_->setTooltip(IDS_SWITCH_SHAPE); - addButton(switchShapeButton_); + len = LoadStringW(g_hInstance, IDS_SWITCH_SHAPE, (LPWSTR)&tooltip, 0); + info.guidItem = g_shapeTypeButtonGuid; + wcsncpy_s(info.szDescription, sizeof(info.szDescription), tooltip, len); + CreateLangBarButton( + info, + SysAllocStringLen(tooltip, len), + LoadIconW(g_hInstance, MAKEINTRESOURCEW(IDI_HALF_SHAPE)), + NULL, + ID_SWITCH_SHAPE, + this, + switchShapeButton_.put_void() + ); + addButton(switchShapeButton_.get()); // settings and others, may open a popup menu - Ime::LangBarButton* button = new Ime::LangBarButton(this, g_settingsButtonGuid); - button->setTooltip(IDS_SETTINGS); - button->setIcon(IDI_CONFIG); - HMENU menu = ::LoadMenuW(g_hInstance, LPCTSTR(IDR_MENU)); + len = LoadStringW(g_hInstance, IDS_SETTINGS, (LPWSTR)&tooltip, 0); + info.guidItem = g_settingsButtonGuid; + info.dwStyle = TF_LBI_STYLE_BTN_MENU; + wcsncpy_s(info.szDescription, sizeof(info.szDescription), tooltip, len); + HMENU menu = ::LoadMenuW(g_hInstance, MAKEINTRESOURCEW(IDR_MENU)); popupMenu_ = ::GetSubMenu(menu, 0); - button->setMenu(popupMenu_); - addButton(button); - button->Release(); + CreateLangBarButton( + info, + SysAllocStringLen(tooltip, len), + LoadIconW(g_hInstance, MAKEINTRESOURCEW(IDI_CONFIG)), + popupMenu_, + 0, + this, + settingsMenuButton_.put_void() + ); + addButton(settingsMenuButton_.get()); // Windows 8 systray IME mode icon if(IsWindows8OrGreater()) { - imeModeIcon_ = new Ime::LangBarButton(this, _GUID_LBI_INPUTMODE, ID_MODE_ICON); - if (isLightTheme()) { - imeModeIcon_->setIcon(IDI_ENG); - } else { - imeModeIcon_->setIcon(IDI_ENG_DARK); - } - addButton(imeModeIcon_); + len = LoadStringW(g_hInstance, IDS_SWITCH_SHAPE, (LPWSTR)&tooltip, 0); + info.guidItem = _GUID_LBI_INPUTMODE; + info.dwStyle = TF_LBI_STYLE_BTN_BUTTON; + wcsncpy_s(info.szDescription, sizeof(info.szDescription), tooltip, len); + CreateLangBarButton( + info, + SysAllocStringLen(tooltip, len), + LoadIconW(g_hInstance, MAKEINTRESOURCEW(isLightTheme() ? IDI_ENG : IDI_ENG_DARK)), + NULL, + ID_MODE_ICON, + this, + imeModeIcon_.put_void() + ); + addButton(imeModeIcon_.get()); } } @@ -133,13 +180,6 @@ TextService::~TextService(void) { if(messageWindow_) hideMessage(); - if(switchLangButton_) - switchLangButton_->Release(); - if(switchShapeButton_) - switchShapeButton_->Release(); - if(imeModeIcon_) - imeModeIcon_->Release(); - freeChewingContext(); } @@ -152,6 +192,7 @@ void TextService::onActivate() { config().reloadIfNeeded(); initChewingContext(); updateLangButtons(); + if(imeModeIcon_) // windows 8 IME mode icon imeModeIcon_->setEnabled(isKeyboardOpened()); } @@ -550,7 +591,7 @@ bool TextService::onPreservedKey(const GUID& guid) { // virtual -bool TextService::onCommand(UINT id, CommandType type) { +STDMETHODIMP TextService::onCommand(UINT id, CommandType type) { assert(chewingContext_); if(type == COMMAND_RIGHT_CLICK) { if(id == ID_MODE_ICON) { // Windows 8 IME mode icon @@ -574,7 +615,7 @@ bool TextService::onCommand(UINT id, CommandType type) { } else { // we only handle right click in Windows 8 for the IME mode icon - return false; + return S_FALSE; } } else { @@ -638,10 +679,27 @@ bool TextService::onCommand(UINT id, CommandType type) { // Need to update the old ChewingIME docs break; default: - return false; + return S_FALSE; } } - return true; + return S_OK; +} + +STDMETHODIMP TextService::QueryInterface(REFIID riid, void **ppvObj) { + if (IsEqualIID(riid, IID_IRunCommand)) { + *ppvObj = (IRunCommand*)this; + AddRef(); + return S_OK; + } + return Ime::TextService::QueryInterface(riid, ppvObj); +} + +STDMETHODIMP_(ULONG) TextService::AddRef() { + return Ime::TextService::AddRef(); +} + +STDMETHODIMP_(ULONG) TextService::Release() { + return Ime::TextService::Release(); } // called when the keyboard is opened or closed @@ -948,26 +1006,19 @@ void TextService::updateLangButtons() { int langMode = ::chewing_get_ChiEngMode(chewingContext_); if(langMode != langMode_) { langMode_ = langMode; - if (isLightTheme()) { - switchLangButton_->setIcon(langMode == CHINESE_MODE ? IDI_CHI : IDI_ENG); - } else { - switchLangButton_->setIcon(langMode == CHINESE_MODE ? IDI_CHI_DARK : IDI_ENG_DARK); - } + UINT iconId = isLightTheme() ? langMode == CHINESE_MODE ? IDI_CHI : IDI_ENG + : langMode == CHINESE_MODE ? IDI_CHI_DARK : IDI_ENG_DARK; + switchLangButton_->setIcon(LoadIconW(g_hInstance, MAKEINTRESOURCEW(iconId))); if(imeModeIcon_) { - // FIXME: we need a better set of icons to meet the - // WIndows 8 IME guideline and UX guidelines. - if (isLightTheme()) { - imeModeIcon_->setIcon(langMode == CHINESE_MODE ? IDI_CHI : IDI_ENG); - } else { - imeModeIcon_->setIcon(langMode == CHINESE_MODE ? IDI_CHI_DARK : IDI_ENG_DARK); - } + imeModeIcon_->setIcon(LoadIconW(g_hInstance, MAKEINTRESOURCEW(iconId))); } } int shapeMode = ::chewing_get_ShapeMode(chewingContext_); if(shapeMode != shapeMode_) { shapeMode_ = shapeMode; - switchShapeButton_->setIcon(shapeMode == FULLSHAPE_MODE ? IDI_FULL_SHAPE : IDI_HALF_SHAPE); + UINT iconId = shapeMode == FULLSHAPE_MODE ? IDI_FULL_SHAPE : IDI_HALF_SHAPE; + switchShapeButton_->setIcon(LoadIconW(g_hInstance, MAKEINTRESOURCEW(iconId))); } } diff --git a/chewing_tip/ChewingTextService.h b/chewing_tip/ChewingTextService.h index 31d992d..0500006 100644 --- a/chewing_tip/ChewingTextService.h +++ b/chewing_tip/ChewingTextService.h @@ -21,20 +21,24 @@ #define CHEWING_TEXT_SERVICE_H #include +#include +#include #include #include #include +#include #include "TextService.h" #include "EditSession.h" -#include "LangBarButton.h" #include "ChewingConfig.h" #include "libime2.h" namespace Chewing { -class TextService: public Ime::TextService { +class TextService: + public IRunCommand, + public Ime::TextService { public: TextService(); virtual ~TextService(void); @@ -55,8 +59,6 @@ class TextService: public Ime::TextService { virtual bool onPreservedKey(const GUID& guid); - virtual bool onCommand(UINT id, CommandType type); - // called when the keyboard is opened or closed virtual void onKeyboardStatusChanged(bool opened); @@ -74,6 +76,15 @@ class TextService: public Ime::TextService { return config_; } +public: + // IRunCommand + STDMETHODIMP onCommand(UINT id, CommandType type); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP_(ULONG) Release(void); + private: void initChewingContext(); // initialize chewing context void freeChewingContext(); // free chewing context @@ -120,9 +131,10 @@ class TextService: public Ime::TextService { bool showingCandidates_; UINT messageTimerId_; - Ime::LangBarButton* switchLangButton_; - Ime::LangBarButton* switchShapeButton_; - Ime::LangBarButton* imeModeIcon_; // IME mode icon, a special language button (Windows 8 only) + winrt::com_ptr switchLangButton_; + winrt::com_ptr switchShapeButton_; + winrt::com_ptr settingsMenuButton_; + winrt::com_ptr imeModeIcon_; // IME mode icon, a special language button (Windows 8 only) HMENU popupMenu_; bool outputSimpChinese_; // output simplified Chinese diff --git a/chewing_tip/LangBarButton.cpp b/chewing_tip/LangBarButton.cpp deleted file mode 100644 index 4353f0c..0000000 --- a/chewing_tip/LangBarButton.cpp +++ /dev/null @@ -1,369 +0,0 @@ -// -// Copyright (C) 2013 Hong Jen Yee (PCMan) -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Library General Public -// License as published by the Free Software Foundation; either -// version 2 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Library General Public License for more details. -// -// You should have received a copy of the GNU Library General Public -// License along with this library; if not, write to the -// Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, -// Boston, MA 02110-1301, USA. -// - -#include "LangBarButton.h" -#include "TextService.h" -#include -#include -#include -#include - -extern HINSTANCE g_hInstance; - -namespace Ime { - -LangBarButton::LangBarButton(TextService* service, const GUID& guid, UINT commandId, const wchar_t* text, DWORD style): - textService_(service), - tooltip_(), - commandId_(commandId), - menu_(NULL), - icon_(NULL), - status_(0), - refCount_(1) { - - assert(service); - - textService_->AddRef(); - info_.clsidService = service->clsid(); - info_.guidItem = guid; - info_.dwStyle = style; - info_.ulSort = 0; - setText(text); -} - -LangBarButton::~LangBarButton(void) { - if(textService_) - textService_->Release(); - if(menu_) - ::DestroyMenu(menu_); -} - -const wchar_t* LangBarButton::text() const { - return info_.szDescription; -} - -void LangBarButton::setText(const wchar_t* text) { - if (text && text[0] != '\0') { - wcsncpy(info_.szDescription, text, TF_LBI_DESC_MAXLEN - 1); - info_.szDescription[TF_LBI_DESC_MAXLEN - 1] = 0; - } - else { - // NOTE: The language button text should NOT be empty. - // Otherwise, when the button status or icon is changed after its creation, - // the button will disappear temporarily in Windows 10 for unknown reason. - // This can be considered a bug of Windows 10 and there does not seem to be a way to fix it. - // So we need to avoid empty button text otherwise the language button won't work properly. - // Here we use a space character to make the text non-empty to workaround the problem. - wcscpy(info_.szDescription, L" "); - } - update(TF_LBI_TEXT); -} - -void LangBarButton::setText(UINT stringId) { - const wchar_t* str; - int len = ::LoadStringW(g_hInstance, stringId, (LPTSTR)&str, 0); - if(str) { - if(len > (TF_LBI_DESC_MAXLEN - 1)) - len = TF_LBI_DESC_MAXLEN - 1; - wcsncpy(info_.szDescription, str, len); - info_.szDescription[len] = 0; - update(TF_LBI_TEXT); - } -} - -// public methods -const wchar_t* LangBarButton::tooltip() const { - return tooltip_.c_str(); -} - -void LangBarButton::setTooltip(const wchar_t* tooltip) { - tooltip_ = tooltip; - update(TF_LBI_TOOLTIP); -} - -void LangBarButton::setTooltip(UINT tooltipId) { - const wchar_t* str; - // If this parameter is 0, then lpBuffer receives a read-only pointer to the resource itself. - auto len = ::LoadStringW(g_hInstance, tooltipId, (LPTSTR)&str, 0); - if(str) { - tooltip_ = std::wstring(str, len); - update(TF_LBI_TOOLTIP); - } -} - -HICON LangBarButton::icon() const { - return icon_; -} - -// The language button does not take owner ship of the icon -// That means, when the button is destroyed, it will not destroy -// the icon automatically. -void LangBarButton::setIcon(HICON icon) { - icon_ = icon; - update(TF_LBI_ICON); -} - -void LangBarButton::setIcon(UINT iconId) { - HICON icon = ::LoadIconW(g_hInstance, (LPCTSTR)iconId); - if(icon) - setIcon(icon); -} - -UINT LangBarButton::commandId() const { - return commandId_; -} - -void LangBarButton::setCommandId(UINT id) { - commandId_ = id; -} - -HMENU LangBarButton::menu() const { - return menu_; -} - -void LangBarButton::setMenu(HMENU menu) { - if(menu_) { - ::DestroyMenu(menu_); - } - menu_ = menu; - // FIXME: how to handle toggle buttons? - if(menu) - info_.dwStyle = TF_LBI_STYLE_BTN_MENU; - else - info_.dwStyle = TF_LBI_STYLE_BTN_BUTTON; -} - -bool LangBarButton::enabled() const { - return !(status_ & TF_LBI_STATUS_DISABLED); -} - -void LangBarButton::setEnabled(bool enable) { - if(enabled() != enable) { - if(enable) - status_ &= ~TF_LBI_STATUS_DISABLED; - else - status_ |= TF_LBI_STATUS_DISABLED; - update(TF_LBI_STATUS); - } -} - -// need to create the button with TF_LBI_STYLE_BTN_TOGGLE style -bool LangBarButton::toggled() const { - return (status_ & TF_LBI_STATUS_BTN_TOGGLED) ? true : false; -} - -void LangBarButton::setToggled(bool toggle) { - if(toggled() != toggle) { - if(toggle) - status_ |= TF_LBI_STATUS_BTN_TOGGLED; - else - status_ &= ~TF_LBI_STATUS_BTN_TOGGLED; - update(TF_LBI_STATUS); - } -} - - -DWORD LangBarButton::style() const { - return info_.dwStyle; -} - -void LangBarButton::setStyle(DWORD style) { - info_.dwStyle = style; -} - - -// COM stuff - -// IUnknown -STDMETHODIMP LangBarButton::QueryInterface(REFIID riid, void **ppvObj) { - if (ppvObj == NULL) - return E_INVALIDARG; - - if(IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_ITfLangBarItem) || IsEqualIID(riid, IID_ITfLangBarItemButton)) - *ppvObj = (ITfLangBarItemButton*)this; - else if(IsEqualIID(riid, IID_ITfSource)) - *ppvObj = (ITfSource*)this; - else - *ppvObj = NULL; - - if(*ppvObj) { - AddRef(); - return S_OK; - } - return E_NOINTERFACE; -} - -// IUnknown implementation -STDMETHODIMP_(ULONG) LangBarButton::AddRef(void) { - return ++refCount_; -} - -STDMETHODIMP_(ULONG) LangBarButton::Release(void) { - assert(refCount_ > 0); - const ULONG newCount = --refCount_; - if (0 == refCount_) - delete this; - return newCount; -} - -// ITfLangBarItem -STDMETHODIMP LangBarButton::GetInfo(TF_LANGBARITEMINFO *pInfo) { - *pInfo = info_; - return S_OK; -} - -STDMETHODIMP LangBarButton::GetStatus(DWORD *pdwStatus) { - *pdwStatus = status_; - return S_OK; -} - -STDMETHODIMP LangBarButton::Show(BOOL fShow) { - return E_NOTIMPL; -} - -STDMETHODIMP LangBarButton::GetTooltipString(BSTR *pbstrToolTip) { - *pbstrToolTip = ::SysAllocString(tooltip_.c_str()); - return *pbstrToolTip ? S_OK : E_FAIL; -} - -// ITfLangBarItemButton -STDMETHODIMP LangBarButton::OnClick(TfLBIClick click, POINT pt, const RECT *prcArea) { - TextService::CommandType type; - if(click == TF_LBI_CLK_RIGHT) - type = TextService::COMMAND_RIGHT_CLICK; - else - type = TextService::COMMAND_LEFT_CLICK; - textService_->onCommand(commandId_, type); - return S_OK; -} - -STDMETHODIMP LangBarButton::InitMenu(ITfMenu *pMenu) { - if(!menu_) - return E_FAIL; - buildITfMenu(pMenu, menu_); - return S_OK; -} - -STDMETHODIMP LangBarButton::OnMenuSelect(UINT wID) { - textService_->onCommand(wID, TextService::COMMAND_MENU); - return S_OK; -} - -STDMETHODIMP LangBarButton::GetIcon(HICON *phIcon) { - // https://msdn.microsoft.com/zh-tw/library/windows/desktop/ms628718%28v=vs.85%29.aspx - // The caller will delete the icon when it's no longer needed. - // However, we might still need it. So let's return a copy here. - *phIcon = (HICON)CopyImage(icon_, IMAGE_ICON, 0, 0, 0); - return S_OK; -} - -STDMETHODIMP LangBarButton::GetText(BSTR *pbstrText) { - *pbstrText = ::SysAllocString(info_.szDescription); - return *pbstrText ? S_OK : S_FALSE; -} - -// ITfSource -STDMETHODIMP LangBarButton::AdviseSink(REFIID riid, IUnknown *punk, DWORD *pdwCookie) { - if(IsEqualIID(riid, IID_ITfLangBarItemSink)) { - ITfLangBarItemSink* langBarItemSink; - if(punk->QueryInterface(IID_ITfLangBarItemSink, (void **)&langBarItemSink) == S_OK) { - *pdwCookie = (DWORD)rand(); - sinks_[*pdwCookie] = langBarItemSink; - return S_OK; - } - else - return E_NOINTERFACE; - } - return CONNECT_E_CANNOTCONNECT; -} - -STDMETHODIMP LangBarButton::UnadviseSink(DWORD dwCookie) { - std::map::iterator it = sinks_.find(dwCookie); - if(it != sinks_.end()) { - ITfLangBarItemSink* langBarItemSink = (ITfLangBarItemSink*)it->second; - langBarItemSink->Release(); - sinks_.erase(it); - return S_OK; - } - return CONNECT_E_NOCONNECTION; -} - - -// build ITfMenu according to the content of HMENU -void LangBarButton::buildITfMenu(ITfMenu* menu, HMENU templ) { - int n = ::GetMenuItemCount(templ); - for(int i = 0; i < n; ++i) { - MENUITEMINFO mi; - wchar_t textBuffer[256]; - memset(&mi, 0, sizeof(mi)); - mi.cbSize = sizeof(mi); - mi.dwTypeData = (LPTSTR)textBuffer; - mi.cch = 255; - mi.fMask = MIIM_FTYPE|MIIM_ID|MIIM_STATE|MIIM_STRING|MIIM_SUBMENU; - if(::GetMenuItemInfoW(templ, i, TRUE, &mi)) { - UINT flags = 0; - wchar_t* text = nullptr; - ULONG textLen = 0; - ITfMenu* subMenu = NULL; - ITfMenu** pSubMenu = NULL; - if(mi.hSubMenu) { // has submenu - pSubMenu = &subMenu; - flags |= TF_LBMENUF_SUBMENU; - } - if(mi.fType == MFT_STRING) { // text item - text = (wchar_t*)mi.dwTypeData; - textLen = mi.cch; - } - else if(mi.fType == MFT_SEPARATOR) { // separator item - flags |= TF_LBMENUF_SEPARATOR; - } - else // other types are not supported - continue; - - if(mi.fState & MFS_CHECKED) // checked - flags |= TF_LBMENUF_CHECKED; - if(mi.fState & (MFS_GRAYED|MFS_DISABLED)) // disabled - flags |= TF_LBMENUF_GRAYED; - - if(menu->AddMenuItem(mi.wID, flags, NULL, 0, text, textLen, pSubMenu) == S_OK) { - if(subMenu) { - buildITfMenu(subMenu, mi.hSubMenu); - subMenu->Release(); - } - } - } - else { - DWORD error = ::GetLastError(); - } - } -} - -// call all sinks to generate update notifications -void LangBarButton::update(DWORD flags) { - if(!sinks_.empty()) { - std::map::iterator it; - for(it = sinks_.begin(); it != sinks_.end(); ++it) { - ITfLangBarItemSink* sink = it->second; - sink->OnUpdate(flags); - } - } -} - -} // namespace Ime - diff --git a/chewing_tip/LangBarButton.h b/chewing_tip/LangBarButton.h deleted file mode 100644 index aac7934..0000000 --- a/chewing_tip/LangBarButton.h +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (C) 2013 Hong Jen Yee (PCMan) -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Library General Public -// License as published by the Free Software Foundation; either -// version 2 of the License, or (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Library General Public License for more details. -// -// You should have received a copy of the GNU Library General Public -// License along with this library; if not, write to the -// Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, -// Boston, MA 02110-1301, USA. -// - -#ifndef IME_LANGUAGE_BAR_BUTTON_H -#define IME_LANGUAGE_BAR_BUTTON_H - -#include -#include -#include -#include -#include - -namespace Ime { - -class TextService; - -class LangBarButton: - public ITfLangBarItemButton, - public ITfSource { -public: - LangBarButton(TextService* service, const GUID& guid, UINT commandId = 0, const wchar_t* text = NULL, DWORD style = TF_LBI_STYLE_BTN_BUTTON); - - // public methods - const wchar_t* text() const; - void setText(const wchar_t* text); - void setText(UINT stringId); - - const wchar_t* tooltip() const; - void setTooltip(const wchar_t* tooltip); - void setTooltip(UINT stringId); - - HICON icon() const; - void setIcon(HICON icon); - void setIcon(UINT iconId); - - UINT commandId() const; - void setCommandId(UINT id); - - HMENU menu() const; - void setMenu(HMENU menu); - - bool enabled() const; - void setEnabled(bool enable); - - // need to create the button with TF_LBI_STYLE_BTN_TOGGLE style - bool toggled() const; - void setToggled(bool toggle); - - DWORD style() const; - void setStyle(DWORD style); - - // COM-related stuff - - // IUnknown - STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); - STDMETHODIMP_(ULONG) AddRef(void); - STDMETHODIMP_(ULONG) Release(void); - - // ITfLangBarItem - STDMETHODIMP GetInfo(TF_LANGBARITEMINFO *pInfo); - STDMETHODIMP GetStatus(DWORD *pdwStatus); - STDMETHODIMP Show(BOOL fShow); - STDMETHODIMP GetTooltipString(BSTR *pbstrToolTip); - - // ITfLangBarItemButton - STDMETHODIMP OnClick(TfLBIClick click, POINT pt, const RECT *prcArea); - STDMETHODIMP InitMenu(ITfMenu *pMenu); - STDMETHODIMP OnMenuSelect(UINT wID); - STDMETHODIMP GetIcon(HICON *phIcon); - STDMETHODIMP GetText(BSTR *pbstrText); - - // ITfSource - STDMETHODIMP AdviseSink(REFIID riid, IUnknown *punk, DWORD *pdwCookie); - STDMETHODIMP UnadviseSink(DWORD dwCookie); - - void update(DWORD flags = TF_LBI_BTNALL); - - TextService* textService() const { - return textService_; - }; - -protected: // COM object should not be deleted directly. calling Release() instead. - virtual ~LangBarButton(void); - -private: - void buildITfMenu(ITfMenu* menu, HMENU templ); - -private: - int refCount_; - TextService* textService_; - TF_LANGBARITEMINFO info_; - UINT commandId_; - std::wstring tooltip_; - HICON icon_; - HMENU menu_; - std::map sinks_; - DWORD status_; -}; - -} - -#endif diff --git a/chewing_tip/TextService.cpp b/chewing_tip/TextService.cpp index aa40d4b..8d5e214 100644 --- a/chewing_tip/TextService.cpp +++ b/chewing_tip/TextService.cpp @@ -19,10 +19,10 @@ #include "TextService.h" #include "EditSession.h" -#include "LangBarButton.h" #include "libime2.h" #include +#include #include #include #include @@ -114,10 +114,11 @@ DWORD TextService::langBarStatus() const { return 0; } -void TextService::addButton(LangBarButton* button) { +void TextService::addButton(ITfLangBarItemButton* button) { if(button) { - winrt::com_ptr btn; + winrt::com_ptr btn; btn.copy_from(button); + langBarButtons_.emplace_back(btn); if(isActivated()) { winrt::com_ptr langBarItemMgr; @@ -128,23 +129,6 @@ void TextService::addButton(LangBarButton* button) { } } -void TextService::removeButton(LangBarButton* button) { - if(button) { - winrt::com_ptr btn; - btn.copy_from(button); - auto it = find(langBarButtons_.begin(), langBarButtons_.end(), btn); - if(it != langBarButtons_.end()) { - if(isActivated()) { - winrt::com_ptr langBarItemMgr; - if(threadMgr_->QueryInterface(IID_ITfLangBarItemMgr, langBarItemMgr.put_void()) == S_OK) { - langBarItemMgr->RemoveItem(button); - } - } - langBarButtons_.erase(it); - } - } -} - // preserved key void TextService::addPreservedKey(UINT keyCode, UINT modifiers, const GUID& guid) { PreservedKey preservedKey; diff --git a/chewing_tip/TextService.h b/chewing_tip/TextService.h index 25e5102..2aeab92 100644 --- a/chewing_tip/TextService.h +++ b/chewing_tip/TextService.h @@ -20,6 +20,7 @@ #ifndef IME_TEXT_SERVICE_H #define IME_TEXT_SERVICE_H +#include #include #include "EditSession.h" #include "KeyEvent.h" @@ -52,12 +53,6 @@ class TextService: public ITfCompartmentEventSink { public: - enum CommandType { // used in onCommand() - COMMAND_LEFT_CLICK, - COMMAND_RIGHT_CLICK, - COMMAND_MENU - }; - TextService(); // public methods @@ -98,8 +93,7 @@ class TextService: DWORD langBarStatus() const; // language bar buttons - void addButton(LangBarButton* button); - void removeButton(LangBarButton* button); + void addButton(ITfLangBarItemButton* button); // preserved keys void addPreservedKey(UINT keyCode, UINT modifiers, const GUID& guid); @@ -156,9 +150,6 @@ class TextService: virtual bool onPreservedKey(const GUID& guid) { return false; } - // called when a language button or menu item is clicked - virtual bool onCommand(UINT id, CommandType type) { return false; } - // called when a value in the global or thread compartment changed. virtual void onCompartmentChanged(const GUID& key); @@ -280,7 +271,7 @@ class TextService: ITfComposition* composition_; // acquired when starting composition, released when ending composition winrt::com_ptr langBarMgr_; - std::vector> langBarButtons_; + std::vector> langBarButtons_; std::vector preservedKeys_; std::vector compartmentMonitors_; diff --git a/chewing_tip/build.rs b/chewing_tip/build.rs index 6efbe18..b084f18 100644 --- a/chewing_tip/build.rs +++ b/chewing_tip/build.rs @@ -1,4 +1,4 @@ -use std::{env, process::Command}; +use std::{env, path::PathBuf, process::Command}; fn main() -> anyhow::Result<()> { let out_dir = env::var("OUT_DIR").unwrap(); @@ -16,11 +16,12 @@ fn main() -> anyhow::Result<()> { .arg(&out_dir) .arg("idl/libime2.idl") .status()?; + let idl_client = PathBuf::from(&out_dir).join("libime2_i.c"); embed_resource::compile("ChewingTextService.rc", embed_resource::NONE).manifest_required()?; cc::Build::new() .cpp(true) - .std("c++17") + .std("c++20") .define("_UNICODE", "1") .define("UNICODE", "1") .define("_CRT_SECURE_NO_WARNINGS", "1") @@ -32,13 +33,14 @@ fn main() -> anyhow::Result<()> { .file("DllEntry.cpp") .file("EditSession.cpp") .file("KeyEvent.cpp") - .file("LangBarButton.cpp") .file("TextService.cpp") .file("Utils.cpp") + .file(idl_client) .include("../libchewing/include") .include(&out_dir) .compile("chewing_tip"); + println!("cargo::rerun-if-changed=idl/libime2.idl"); println!("cargo::rerun-if-changed=CClassFactory.cpp"); println!("cargo::rerun-if-changed=CClassFactory.h"); println!("cargo::rerun-if-changed=ChewingConfig.cpp"); @@ -50,8 +52,6 @@ fn main() -> anyhow::Result<()> { println!("cargo::rerun-if-changed=EditSession.h"); println!("cargo::rerun-if-changed=KeyEvent.cpp"); println!("cargo::rerun-if-changed=KeyEvent.h"); - println!("cargo::rerun-if-changed=LangBarButton.cpp"); - println!("cargo::rerun-if-changed=LangBarButton.h"); println!("cargo::rerun-if-changed=TextService.cpp"); println!("cargo::rerun-if-changed=TextService.h"); println!("cargo::rerun-if-changed=Utils.cpp"); diff --git a/chewing_tip/idl/libime2.idl b/chewing_tip/idl/libime2.idl index b5fc0e1..584f185 100644 --- a/chewing_tip/idl/libime2.idl +++ b/chewing_tip/idl/libime2.idl @@ -63,6 +63,33 @@ interface IMessageWindow : IWindow void setText(LPCWSTR text); } +enum CommandType { + COMMAND_LEFT_CLICK, + COMMAND_RIGHT_CLICK, + COMMAND_MENU, +}; + +[ +object, +uuid(f320f835-b95d-4d3f-89d5-fd4ab7b9d7bb), +local +] +interface IRunCommand : IUnknown +{ + HRESULT onCommand(UINT id, enum CommandType cmdType); +} + +[ +object, +uuid(4db963b1-ced3-42b7-8f87-937534740e7a), +local +] +interface ILangBarButton : ITfLangBarItemButton +{ + HRESULT setIcon(HICON icon); + HRESULT setEnabled(boolean enabled); +} + [local] void LibIME2Init(); [local] void CreateImeWindow([out] void **window); [local] void CreateMessageWindow(HWND parent, [in] LPCWSTR image_path, [out] void **messagewindow); @@ -70,4 +97,13 @@ interface IMessageWindow : IWindow [local] IWindow *ImeWindowFromHwnd(HWND hwnd); [local] boolean ImeWindowRegisterClass(HINSTANCE hinstance); [local] void CreateDisplayAttributeProvider([out] void **provider); -[local] HRESULT RegisterDisplayAttribute([in] const GUID *guid, [in] TF_DISPLAYATTRIBUTE da, [out] UINT32 *atom); \ No newline at end of file +[local] HRESULT RegisterDisplayAttribute([in] const GUID *guid, [in] TF_DISPLAYATTRIBUTE da, [out] UINT32 *atom); +[local] void CreateLangBarButton( + TF_LANGBARITEMINFO info, + [in] BSTR tooltip, + [in] HICON icon, + [in] HMENU menu, + DWORD commandId, + [in] IRunCommand *pRunCommand, + [out] void **ppLangBarButton +); diff --git a/chewing_tip/src/lang_bar.rs b/chewing_tip/src/lang_bar.rs index 8b13789..ae1ad10 100644 --- a/chewing_tip/src/lang_bar.rs +++ b/chewing_tip/src/lang_bar.rs @@ -1 +1,259 @@ +use std::{cell::Cell, collections::BTreeMap, ffi::c_void, sync::RwLock}; +use windows::Win32::{ + Foundation::{BOOL, E_FAIL, E_INVALIDARG, POINT, RECT}, + UI::{ + TextServices::{ + ITfLangBarItem, ITfLangBarItemButton, ITfLangBarItemButton_Impl, + ITfLangBarItemButton_Vtbl, ITfLangBarItemSink, ITfLangBarItem_Impl, ITfMenu, ITfSource, + ITfSource_Impl, TfLBIClick, TF_LANGBARITEMINFO, TF_LBI_CLK_RIGHT, TF_LBI_ICON, + TF_LBI_STATUS, TF_LBI_STATUS_DISABLED, TF_LBI_STATUS_HIDDEN, TF_LBMENUF_CHECKED, + TF_LBMENUF_GRAYED, TF_LBMENUF_SEPARATOR, TF_LBMENUF_SUBMENU, + }, + WindowsAndMessaging::{ + CopyIcon, DestroyIcon, GetMenuItemCount, GetMenuItemInfoW, HICON, HMENU, MENUITEMINFOW, + MENU_ITEM_STATE, MFS_CHECKED, MFS_DISABLED, MFS_GRAYED, MFT_SEPARATOR, MFT_STRING, + MIIM_FTYPE, MIIM_ID, MIIM_STATE, MIIM_STRING, MIIM_SUBMENU, + }, + }, +}; +use windows_core::{ + implement, interface, ComObjectInner, IUnknown, IUnknown_Vtbl, Interface, Result, BSTR, GUID, + PWSTR, +}; + +#[repr(C)] +enum CommandType { + LeftClick, + RightClick, + Menu, +} + +#[interface("f320f835-b95d-4d3f-89d5-fd4ab7b9d7bb")] +pub(crate) unsafe trait IRunCommand: IUnknown { + fn on_command(&self, id: u32, cmd_type: CommandType); +} + +#[interface("4db963b1-ced3-42b7-8f87-937534740e7a")] +pub(crate) unsafe trait ILangBarButton: ITfLangBarItemButton { + fn set_icon(&self, icon: HICON) -> Result<()>; + fn set_enabled(&self, enabled: bool) -> Result<()>; +} + +#[derive(Debug)] +#[implement(ITfLangBarItem, ITfLangBarItemButton, ITfSource, ILangBarButton)] +struct LangBarButton { + info: TF_LANGBARITEMINFO, + status: Cell, + tooltip: BSTR, + icon: Cell, + /// borrowed - we don't own this menu + menu: HMENU, + command_id: u32, + run_command: IRunCommand, + sinks: RwLock>, +} + +impl Drop for LangBarButton { + fn drop(&mut self) { + if !self.icon.get().is_invalid() { + let _ = unsafe { DestroyIcon(self.icon.get()) }; + } + } +} + +#[no_mangle] +unsafe extern "C" fn CreateLangBarButton( + info: TF_LANGBARITEMINFO, + tooltip: BSTR, + icon: HICON, + menu: HMENU, + command_id: u32, + run_command: IRunCommand, + ret: *mut *mut c_void, +) { + let lang_bar_btn = LangBarButton { + info, + status: Cell::new(0), + tooltip, + icon: Cell::new(icon), + menu, + command_id, + run_command, + sinks: RwLock::new(BTreeMap::new()), + } + .into_object(); + ret.write(lang_bar_btn.into_interface::().into_raw()); +} + +impl ILangBarButton_Impl for LangBarButton_Impl { + unsafe fn set_icon(&self, icon: HICON) -> Result<()> { + if !self.icon.get().is_invalid() { + DestroyIcon(self.icon.get())?; + } + self.icon.set(icon); + self.update_sinks(TF_LBI_ICON)?; + Ok(()) + } + + unsafe fn set_enabled(&self, enabled: bool) -> Result<()> { + if enabled { + self.status.set(self.status.get() & !TF_LBI_STATUS_DISABLED); + } else { + self.status.set(self.status.get() | TF_LBI_STATUS_DISABLED); + } + self.update_sinks(TF_LBI_STATUS)?; + Ok(()) + } +} + +impl ITfLangBarItem_Impl for LangBarButton_Impl { + fn GetInfo(&self, pinfo: *mut TF_LANGBARITEMINFO) -> Result<()> { + if pinfo.is_null() { + return Err(E_INVALIDARG.into()); + } + unsafe { + pinfo.write(self.info); + } + Ok(()) + } + + fn GetStatus(&self) -> Result { + Ok(self.status.get()) + } + + fn Show(&self, fshow: BOOL) -> Result<()> { + if fshow.as_bool() { + self.status.set(self.status.get() & !TF_LBI_STATUS_HIDDEN); + } else { + self.status.set(self.status.get() | TF_LBI_STATUS_HIDDEN); + } + self.update_sinks(TF_LBI_STATUS)?; + Ok(()) + } + + fn GetTooltipString(&self) -> Result { + Ok(self.tooltip.clone()) + } +} + +impl ITfLangBarItemButton_Impl for LangBarButton_Impl { + fn OnClick(&self, click: TfLBIClick, _pt: &POINT, _prcareaa: *const RECT) -> Result<()> { + let cmd_type = if click == TF_LBI_CLK_RIGHT { + CommandType::RightClick + } else { + CommandType::LeftClick + }; + unsafe { self.run_command.on_command(self.command_id, cmd_type) }; + Ok(()) + } + + fn InitMenu(&self, pmenu: Option<&ITfMenu>) -> Result<()> { + if self.menu.is_invalid() { + return Err(E_FAIL.into()); + } + if let Some(menu) = pmenu { + build_menu(menu, self.menu) + } else { + Err(E_FAIL.into()) + } + } + + fn OnMenuSelect(&self, wid: u32) -> Result<()> { + unsafe { self.run_command.on_command(wid, CommandType::Menu) }; + Ok(()) + } + + fn GetIcon(&self) -> Result { + unsafe { CopyIcon(self.icon.get()) } + } + + fn GetText(&self) -> Result { + BSTR::from_wide(&self.info.szDescription) + } +} + +impl ITfSource_Impl for LangBarButton_Impl { + fn AdviseSink(&self, riid: *const GUID, punk: Option<&IUnknown>) -> Result { + if riid.is_null() || punk.is_none() { + return Err(E_INVALIDARG.into()); + } + if unsafe { *riid == ITfLangBarItemSink::IID } { + let mut cookie = [0; 4]; + if let Err(_) = getrandom::getrandom(&mut cookie) { + return Err(E_FAIL.into()); + } + let cookie = u32::from_ne_bytes(cookie); + let sink: ITfLangBarItemSink = punk.unwrap().cast()?; + if let Ok(mut sinks) = self.sinks.write() { + sinks.insert(cookie, sink); + return Ok(cookie); + } + } + Err(E_FAIL.into()) + } + + fn UnadviseSink(&self, dwcookie: u32) -> Result<()> { + if let Ok(mut sinks) = self.sinks.write() { + sinks.remove(&dwcookie); + return Ok(()); + } + Err(E_FAIL.into()) + } +} + +impl LangBarButton { + fn update_sinks(&self, dwflags: u32) -> Result<()> { + if let Ok(sinks) = self.sinks.read() { + for sink in sinks.values() { + unsafe { sink.OnUpdate(dwflags)? }; + } + } + Ok(()) + } +} + +fn build_menu(menu: &ITfMenu, hmenu: HMENU) -> Result<()> { + for i in 0..unsafe { GetMenuItemCount(hmenu) } { + let mut text_buffer = [0_u16; 256]; + let mut mi = MENUITEMINFOW { + cbSize: size_of::() as u32, + fMask: MIIM_FTYPE | MIIM_ID | MIIM_STATE | MIIM_STRING | MIIM_SUBMENU, + dwTypeData: PWSTR::from_raw(text_buffer.as_mut_ptr()), + cch: 255, + ..Default::default() + }; + if let Ok(()) = unsafe { GetMenuItemInfoW(hmenu, i as u32, true, &mut mi) } { + let mut flags = 0; + let mut sub_menu = None; + if !mi.hSubMenu.is_invalid() { + flags |= TF_LBMENUF_SUBMENU; + } + if mi.fType == MFT_SEPARATOR { + flags |= TF_LBMENUF_SEPARATOR; + } else if mi.fType != MFT_STRING { + // other types of menu are not supported + continue; + } + if mi.fState.contains(MFS_CHECKED) { + flags |= TF_LBMENUF_CHECKED; + } + // Despite the document says MFS_GRAYED and MFS_DISABLED has the same value 0x3 + // Inactive menu item actually has value 0x2, same as MF_DISABLED + if mi.fState.contains(MFS_GRAYED) + || mi.fState.contains(MFS_DISABLED) + || mi.fState.contains(MENU_ITEM_STATE(0x2)) + { + flags |= TF_LBMENUF_GRAYED; + } + if let Ok(()) = + unsafe { menu.AddMenuItem(mi.wID, flags, None, None, &text_buffer, &mut sub_menu) } + { + if let Some(sub_menu) = sub_menu { + build_menu(&sub_menu, mi.hSubMenu)?; + } + } + } + } + Ok(()) +}