/src/libreoffice/sfx2/source/commandpopup/CommandPopup.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | */ |
9 | | |
10 | | #include <commandpopup/CommandPopup.hxx> |
11 | | |
12 | | #include <sfx2/msgpool.hxx> |
13 | | #include <sfx2/bindings.hxx> |
14 | | #include <sfx2/msg.hxx> |
15 | | #include <sfx2/viewfrm.hxx> |
16 | | |
17 | | #include <comphelper/processfactory.hxx> |
18 | | #include <comphelper/dispatchcommand.hxx> |
19 | | |
20 | | #include <com/sun/star/ui/theModuleUIConfigurationManagerSupplier.hpp> |
21 | | #include <com/sun/star/util/URL.hpp> |
22 | | #include <com/sun/star/util/URLTransformer.hpp> |
23 | | #include <com/sun/star/i18n/CharacterClassification.hpp> |
24 | | |
25 | | #include <vcl/commandinfoprovider.hxx> |
26 | | #include <vcl/event.hxx> |
27 | | #include <vcl/svapp.hxx> |
28 | | #include <i18nlangtag/languagetag.hxx> |
29 | | |
30 | | using namespace css; |
31 | | |
32 | | MenuContentHandler::MenuContentHandler(uno::Reference<frame::XFrame> const& xFrame) |
33 | 0 | : m_xContext(comphelper::getProcessComponentContext()) |
34 | 0 | , m_xFrame(xFrame) |
35 | 0 | , m_xCharacterClassification(i18n::CharacterClassification::create(m_xContext)) |
36 | 0 | , m_xURLTransformer(util::URLTransformer::create(m_xContext)) |
37 | 0 | , m_sModuleLongName(vcl::CommandInfoProvider::GetModuleIdentifier(xFrame)) |
38 | 0 | { |
39 | 0 | uno::Reference<ui::XModuleUIConfigurationManagerSupplier> xModuleConfigSupplier; |
40 | 0 | xModuleConfigSupplier.set(ui::theModuleUIConfigurationManagerSupplier::get(m_xContext)); |
41 | |
|
42 | 0 | uno::Reference<ui::XUIConfigurationManager> xConfigurationManager; |
43 | 0 | xConfigurationManager = xModuleConfigSupplier->getUIConfigurationManager(m_sModuleLongName); |
44 | |
|
45 | 0 | uno::Reference<container::XIndexAccess> xConfigData; |
46 | 0 | xConfigData |
47 | 0 | = xConfigurationManager->getSettings(u"private:resource/menubar/menubar"_ustr, false); |
48 | |
|
49 | 0 | gatherMenuContent(xConfigData, m_aMenuContent); |
50 | 0 | } |
51 | | |
52 | | void MenuContentHandler::gatherMenuContent( |
53 | | uno::Reference<container::XIndexAccess> const& xIndexAccess, MenuContent& rMenuContent) |
54 | 0 | { |
55 | 0 | std::u16string_view aMenuLabelSeparator = AllSettings::GetLayoutRTL() ? u" ◂ " : u" ▸ "; |
56 | 0 | for (sal_Int32 n = 0; n < xIndexAccess->getCount(); n++) |
57 | 0 | { |
58 | 0 | MenuContent aNewContent; |
59 | 0 | uno::Sequence<beans::PropertyValue> aProperties; |
60 | 0 | uno::Reference<container::XIndexAccess> xIndexContainer; |
61 | |
|
62 | 0 | if (!(xIndexAccess->getByIndex(n) >>= aProperties)) |
63 | 0 | continue; |
64 | | |
65 | 0 | bool bIsVisible = true; |
66 | 0 | bool bIsEnabled = true; |
67 | |
|
68 | 0 | for (auto const& rProperty : aProperties) |
69 | 0 | { |
70 | 0 | OUString aPropertyName = rProperty.Name; |
71 | 0 | if (aPropertyName == "CommandURL") |
72 | 0 | rProperty.Value >>= aNewContent.m_aCommandURL; |
73 | 0 | else if (aPropertyName == "ItemDescriptorContainer") |
74 | 0 | rProperty.Value >>= xIndexContainer; |
75 | 0 | else if (aPropertyName == "IsVisible") |
76 | 0 | rProperty.Value >>= bIsVisible; |
77 | 0 | else if (aPropertyName == "Enabled") |
78 | 0 | rProperty.Value >>= bIsEnabled; |
79 | 0 | } |
80 | |
|
81 | 0 | if (!bIsEnabled || !bIsVisible) |
82 | 0 | continue; |
83 | | |
84 | 0 | auto aCommandProperties = vcl::CommandInfoProvider::GetCommandProperties( |
85 | 0 | aNewContent.m_aCommandURL, m_sModuleLongName); |
86 | 0 | aNewContent.m_aMenuLabel = vcl::CommandInfoProvider::GetLabelForCommand(aCommandProperties); |
87 | |
|
88 | 0 | if (!rMenuContent.m_aFullLabelWithPath.isEmpty()) |
89 | 0 | aNewContent.m_aFullLabelWithPath += aMenuLabelSeparator; |
90 | 0 | aNewContent.m_aFullLabelWithPath += aNewContent.m_aMenuLabel; |
91 | 0 | aNewContent.m_aSearchableMenuLabel = toLower(aNewContent.m_aFullLabelWithPath); |
92 | |
|
93 | 0 | aNewContent.m_aTooltip = vcl::CommandInfoProvider::GetTooltipForCommand( |
94 | 0 | aNewContent.m_aCommandURL, aCommandProperties, m_xFrame); |
95 | |
|
96 | 0 | if (xIndexContainer.is()) |
97 | 0 | gatherMenuContent(xIndexContainer, aNewContent); |
98 | |
|
99 | 0 | rMenuContent.m_aSubMenuContent.push_back(std::move(aNewContent)); |
100 | 0 | } |
101 | 0 | } |
102 | | |
103 | | void MenuContentHandler::findInMenu(OUString const& rText, |
104 | | std::unique_ptr<weld::TreeView>& rpCommandTreeView, |
105 | | std::vector<CurrentEntry>& rCommandList) |
106 | 0 | { |
107 | 0 | m_aAdded.clear(); |
108 | |
|
109 | 0 | OUString aLowerCaseText = toLower(rText); |
110 | | |
111 | | // find submenus and menu items that start with the searched text |
112 | 0 | auto aTextStartCriterium = [](MenuContent const& rMenuContent, OUString const& rSearchText) { |
113 | 0 | OUString aSearchText = " / " + rSearchText; |
114 | 0 | return rMenuContent.m_aSearchableMenuLabel.indexOf(aSearchText) > 0; |
115 | 0 | }; |
116 | |
|
117 | 0 | findInMenuRecursive(m_aMenuContent, aLowerCaseText, rpCommandTreeView, rCommandList, |
118 | 0 | aTextStartCriterium); |
119 | | |
120 | | // find submenus and menu items that contain the searched text |
121 | 0 | auto aTextAllCriterium = [](MenuContent const& rMenuContent, OUString const& rSearchText) { |
122 | 0 | return rMenuContent.m_aSearchableMenuLabel.indexOf(rSearchText) > 0; |
123 | 0 | }; |
124 | |
|
125 | 0 | findInMenuRecursive(m_aMenuContent, aLowerCaseText, rpCommandTreeView, rCommandList, |
126 | 0 | aTextAllCriterium); |
127 | 0 | } |
128 | | |
129 | | void MenuContentHandler::findInMenuRecursive( |
130 | | MenuContent const& rMenuContent, OUString const& rText, |
131 | | std::unique_ptr<weld::TreeView>& rpCommandTreeView, std::vector<CurrentEntry>& rCommandList, |
132 | | std::function<bool(MenuContent const&, OUString const&)> const& rSearchCriterium) |
133 | 0 | { |
134 | 0 | for (MenuContent const& aSubContent : rMenuContent.m_aSubMenuContent) |
135 | 0 | { |
136 | 0 | if (rSearchCriterium(aSubContent, rText)) |
137 | 0 | { |
138 | 0 | addCommandIfPossible(aSubContent, rpCommandTreeView, rCommandList); |
139 | 0 | } |
140 | 0 | findInMenuRecursive(aSubContent, rText, rpCommandTreeView, rCommandList, rSearchCriterium); |
141 | 0 | } |
142 | 0 | } |
143 | | |
144 | | void MenuContentHandler::addCommandIfPossible( |
145 | | MenuContent const& rMenuContent, const std::unique_ptr<weld::TreeView>& rpCommandTreeView, |
146 | | std::vector<CurrentEntry>& rCommandList) |
147 | 0 | { |
148 | 0 | if (m_aAdded.find(rMenuContent.m_aFullLabelWithPath) != m_aAdded.end()) |
149 | 0 | return; |
150 | | |
151 | 0 | OUString sCommandURL = rMenuContent.m_aCommandURL; |
152 | 0 | util::URL aCommandURL; |
153 | 0 | aCommandURL.Complete = sCommandURL; |
154 | |
|
155 | 0 | if (!m_xURLTransformer->parseStrict(aCommandURL)) |
156 | 0 | return; |
157 | | |
158 | 0 | auto* pViewFrame = SfxViewFrame::Current(); |
159 | 0 | if (!pViewFrame) |
160 | 0 | return; |
161 | | |
162 | 0 | SfxSlotPool& rSlotPool = SfxSlotPool::GetSlotPool(pViewFrame); |
163 | 0 | const SfxSlot* pSlot = rSlotPool.GetUnoSlot(aCommandURL.Path); |
164 | 0 | if (!pSlot) |
165 | 0 | return; |
166 | | |
167 | 0 | std::unique_ptr<SfxPoolItem> pState; |
168 | 0 | SfxItemState eState = pViewFrame->GetBindings().QueryState(pSlot->GetSlotId(), pState); |
169 | 0 | if (eState == SfxItemState::DISABLED) |
170 | 0 | return; |
171 | | |
172 | 0 | auto xGraphic = vcl::CommandInfoProvider::GetXGraphicForCommand(sCommandURL, m_xFrame); |
173 | 0 | rCommandList.emplace_back(sCommandURL, rMenuContent.m_aTooltip); |
174 | |
|
175 | 0 | auto pIter = rpCommandTreeView->make_iterator(); |
176 | 0 | rpCommandTreeView->insert(nullptr, -1, &rMenuContent.m_aFullLabelWithPath, nullptr, nullptr, |
177 | 0 | nullptr, false, pIter.get()); |
178 | 0 | rpCommandTreeView->set_image(*pIter, xGraphic); |
179 | 0 | m_aAdded.insert(rMenuContent.m_aFullLabelWithPath); |
180 | 0 | } |
181 | | |
182 | | OUString MenuContentHandler::toLower(OUString const& rString) |
183 | 0 | { |
184 | 0 | const css::lang::Locale& rLocale = Application::GetSettings().GetUILanguageTag().getLocale(); |
185 | |
|
186 | 0 | return m_xCharacterClassification->toLower(rString, 0, rString.getLength(), rLocale); |
187 | 0 | } |
188 | | |
189 | | CommandListBox::CommandListBox(weld::Window* pParent, uno::Reference<frame::XFrame> const& xFrame) |
190 | 0 | : mxBuilder(Application::CreateBuilder(pParent, u"sfx/ui/commandpopup.ui"_ustr)) |
191 | 0 | , mxPopover(mxBuilder->weld_popover(u"CommandPopup"_ustr)) |
192 | 0 | , mpEntry(mxBuilder->weld_entry(u"command_entry"_ustr)) |
193 | 0 | , mpCommandTreeView(mxBuilder->weld_tree_view(u"command_treeview"_ustr)) |
194 | 0 | , mpMenuContentHandler(std::make_unique<MenuContentHandler>(xFrame)) |
195 | 0 | { |
196 | 0 | mpEntry->connect_changed(LINK(this, CommandListBox, ModifyHdl)); |
197 | 0 | mpEntry->connect_key_press(LINK(this, CommandListBox, TreeViewKeyPress)); |
198 | 0 | mpCommandTreeView->connect_query_tooltip(LINK(this, CommandListBox, QueryTooltip)); |
199 | 0 | mpCommandTreeView->connect_row_activated(LINK(this, CommandListBox, RowActivated)); |
200 | |
|
201 | 0 | Size aFrameSize = pParent->get_size(); |
202 | | |
203 | | // Set size of the pop-over window |
204 | 0 | tools::Long nWidth = std::max(tools::Long(400), aFrameSize.Width() / 3); |
205 | 0 | mpCommandTreeView->set_size_request(nWidth, 400); |
206 | | |
207 | | // Set the location of the pop-over window |
208 | 0 | tools::Rectangle aRect(Point(aFrameSize.Width() / 2, 0), Size(0, 0)); |
209 | 0 | mxPopover->popup_at_rect(pParent, aRect); |
210 | 0 | mpEntry->grab_focus(); |
211 | 0 | } |
212 | | |
213 | | IMPL_LINK_NOARG(CommandListBox, QueryTooltip, const weld::TreeIter&, OUString) |
214 | 0 | { |
215 | 0 | size_t nSelected = mpCommandTreeView->get_selected_index(); |
216 | 0 | if (nSelected < maCommandList.size()) |
217 | 0 | { |
218 | 0 | auto const& rCurrent = maCommandList[nSelected]; |
219 | 0 | return rCurrent.m_aTooltip; |
220 | 0 | } |
221 | 0 | return OUString(); |
222 | 0 | } |
223 | | |
224 | | IMPL_LINK_NOARG(CommandListBox, RowActivated, weld::TreeView&, bool) |
225 | 0 | { |
226 | 0 | OUString aCommandURL; |
227 | 0 | int nSelected = mpCommandTreeView->get_selected_index(); |
228 | 0 | if (nSelected != -1 && nSelected < int(maCommandList.size())) |
229 | 0 | { |
230 | 0 | auto const& rCurrent = maCommandList[nSelected]; |
231 | 0 | aCommandURL = rCurrent.m_aCommandURL; |
232 | 0 | } |
233 | 0 | dispatchCommandAndClose(aCommandURL); |
234 | 0 | return true; |
235 | 0 | } |
236 | | |
237 | | IMPL_LINK(CommandListBox, TreeViewKeyPress, const KeyEvent&, rKeyEvent, bool) |
238 | 0 | { |
239 | 0 | if (rKeyEvent.GetKeyCode().GetCode() == KEY_DOWN || rKeyEvent.GetKeyCode().GetCode() == KEY_UP) |
240 | 0 | { |
241 | 0 | int nDirection = rKeyEvent.GetKeyCode().GetCode() == KEY_DOWN ? 1 : -1; |
242 | 0 | int nNewIndex = mpCommandTreeView->get_selected_index() + nDirection; |
243 | 0 | nNewIndex = std::clamp(nNewIndex, 0, mpCommandTreeView->n_children() - 1); |
244 | 0 | mpCommandTreeView->select(nNewIndex); |
245 | 0 | mpCommandTreeView->set_cursor(nNewIndex); |
246 | 0 | return true; |
247 | 0 | } |
248 | 0 | else if (rKeyEvent.GetKeyCode().GetCode() == KEY_RETURN) |
249 | 0 | { |
250 | 0 | RowActivated(*mpCommandTreeView); |
251 | 0 | return true; |
252 | 0 | } |
253 | | |
254 | 0 | return false; |
255 | 0 | } |
256 | | |
257 | | IMPL_LINK_NOARG(CommandListBox, ModifyHdl, weld::Entry&, void) |
258 | 0 | { |
259 | 0 | mpCommandTreeView->clear(); |
260 | 0 | maCommandList.clear(); |
261 | |
|
262 | 0 | OUString sText = mpEntry->get_text(); |
263 | 0 | if (sText.isEmpty()) |
264 | 0 | return; |
265 | | |
266 | 0 | mpCommandTreeView->freeze(); |
267 | 0 | mpMenuContentHandler->findInMenu(sText, mpCommandTreeView, maCommandList); |
268 | 0 | mpCommandTreeView->thaw(); |
269 | |
|
270 | 0 | if (mpCommandTreeView->n_children() > 0) |
271 | 0 | { |
272 | 0 | mpCommandTreeView->set_cursor(0); |
273 | 0 | mpCommandTreeView->select(0); |
274 | 0 | } |
275 | |
|
276 | 0 | mpEntry->grab_focus(); |
277 | 0 | } |
278 | | |
279 | | void CommandListBox::dispatchCommandAndClose(OUString const& rCommand) |
280 | 0 | { |
281 | 0 | mxPopover->popdown(); |
282 | |
|
283 | 0 | if (!rCommand.isEmpty()) |
284 | 0 | comphelper::dispatchCommand(rCommand, uno::Sequence<beans::PropertyValue>()); |
285 | 0 | } |
286 | | |
287 | | void CommandPopupHandler::showPopup(weld::Window* pParent, |
288 | | css::uno::Reference<css::frame::XFrame> const& xFrame) |
289 | 0 | { |
290 | 0 | auto pCommandListBox = std::make_unique<CommandListBox>(pParent, xFrame); |
291 | 0 | pCommandListBox->connect_closed(LINK(this, CommandPopupHandler, PopupModeEnd)); |
292 | 0 | mpListBox = std::move(pCommandListBox); |
293 | 0 | } |
294 | | |
295 | 0 | IMPL_LINK_NOARG(CommandPopupHandler, PopupModeEnd, weld::Popover&, void) { mpListBox.reset(); } |
296 | | |
297 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |