Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "abstractmenumodel.h"
23 :
24 : #include "rcommand/commandtypes.h"
25 : #include "types/translatablestring.h"
26 :
27 : #include "log.h"
28 :
29 : using namespace muse::uicomponents;
30 : using namespace muse::ui;
31 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
32 : using namespace muse::actions;
33 : #endif
34 :
35 : const int AbstractMenuModel::INVALID_ITEM_INDEX = -1;
36 :
37 3 : AbstractMenuModel::AbstractMenuModel(QObject* parent)
38 3 : : QAbstractListModel(parent), muse::Contextable(muse::iocCtxForQmlObject(this))
39 : {
40 3 : }
41 :
42 0 : QVariant AbstractMenuModel::data(const QModelIndex& index, int role) const
43 : {
44 0 : int row = index.row();
45 :
46 0 : if (!isIndexValid(row)) {
47 0 : return QVariant();
48 : }
49 :
50 0 : MenuItem* item = m_items.at(row);
51 :
52 0 : switch (role) {
53 0 : case ItemRole: return QVariant::fromValue(item);
54 0 : case UserRole: return QVariant();
55 : }
56 :
57 0 : return QVariant();
58 : }
59 :
60 0 : bool AbstractMenuModel::isIndexValid(int index) const
61 : {
62 0 : return index >= 0 && index < m_items.size();
63 : }
64 :
65 0 : int AbstractMenuModel::rowCount(const QModelIndex&) const
66 : {
67 0 : return m_items.count();
68 : }
69 :
70 0 : QHash<int, QByteArray> AbstractMenuModel::roleNames() const
71 : {
72 : static const QHash<int, QByteArray> roles {
73 0 : { ItemRole, "item" }
74 0 : };
75 :
76 0 : return roles;
77 0 : }
78 :
79 0 : void AbstractMenuModel::handleMenuItem(const QString& itemId)
80 : {
81 0 : MenuItem& menuItem = findItem(itemId);
82 :
83 0 : std::string intent = menuItem.intent();
84 0 : if (muse::strings::startsWith(intent, "command://")) {
85 0 : auto params = menuItem.params();
86 0 : if (params.empty()) {
87 0 : dispatchCommand(rcommand::CommandQuery(intent));
88 : } else {
89 0 : dispatchCommand(rcommand::Command(intent), params);
90 : }
91 0 : } else {
92 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
93 0 : UriQuery query(intent);
94 0 : if (query.isValid()) {
95 0 : dispatchAction(query);
96 : } else {
97 0 : dispatchAction(actions::ActionCode(intent), menuItem.args());
98 : }
99 : #else
100 : UNREACHABLE;
101 : #endif
102 0 : }
103 0 : }
104 :
105 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
106 0 : void AbstractMenuModel::dispatchAction(const muse::actions::ActionCode& code, const muse::actions::ActionData& args)
107 : {
108 0 : IF_ASSERT_FAILED(!muse::strings::startsWith(code, "command://")) {
109 0 : dispatchCommand(rcommand::Command(code));
110 0 : return;
111 : }
112 0 : dispatcher()->dispatch(code, args);
113 : }
114 :
115 0 : void AbstractMenuModel::dispatchAction(const muse::UriQuery& query)
116 : {
117 0 : IF_ASSERT_FAILED(!muse::strings::startsWith(query.uri().scheme(), "command")) {
118 0 : dispatchCommand(rcommand::CommandQuery(query));
119 0 : return;
120 : }
121 0 : dispatcher()->dispatch(query);
122 : }
123 :
124 : #endif
125 :
126 0 : void AbstractMenuModel::dispatchCommand(const muse::rcommand::Command& command, const muse::rcommand::Params& params)
127 : {
128 0 : commandDispatcher()->dispatch(command, params);
129 0 : }
130 :
131 0 : void AbstractMenuModel::dispatchCommand(const muse::rcommand::CommandQuery& query)
132 : {
133 0 : commandDispatcher()->dispatch(query);
134 0 : }
135 :
136 0 : QVariantMap AbstractMenuModel::get(int index)
137 : {
138 0 : QVariantMap result;
139 :
140 0 : QHash<int, QByteArray> names = roleNames();
141 0 : QHashIterator<int, QByteArray> i(names);
142 0 : while (i.hasNext()) {
143 0 : i.next();
144 0 : QModelIndex idx = this->index(index, 0);
145 0 : QVariant data = idx.data(i.key());
146 0 : result[i.value()] = data;
147 0 : }
148 :
149 0 : return result;
150 0 : }
151 :
152 0 : void AbstractMenuModel::load()
153 : {
154 0 : subscribeOnChanges();
155 0 : }
156 :
157 0 : QVariantList AbstractMenuModel::itemsProperty() const
158 : {
159 0 : QVariantList items;
160 :
161 0 : for (MenuItem* item: m_items) {
162 0 : items << QVariant::fromValue(item);
163 : }
164 :
165 0 : return items;
166 0 : }
167 :
168 0 : const MenuItemList& AbstractMenuModel::items() const
169 : {
170 0 : return m_items;
171 : }
172 :
173 3 : void AbstractMenuModel::setItems(const MenuItemList& items)
174 : {
175 4 : TRACEFUNC;
176 :
177 3 : beginResetModel();
178 3 : m_items = items;
179 3 : updateShortcutsAll();
180 3 : endResetModel();
181 :
182 3 : emit itemsChanged();
183 3 : }
184 :
185 0 : void AbstractMenuModel::clear()
186 : {
187 0 : setItems(MenuItemList());
188 0 : }
189 :
190 0 : int AbstractMenuModel::itemIndex(const QString& itemId) const
191 : {
192 0 : for (int i = 0; i < m_items.size(); ++i) {
193 0 : if (m_items[i]->id() == itemId) {
194 0 : return i;
195 : }
196 : }
197 :
198 0 : return INVALID_ITEM_INDEX;
199 : }
200 :
201 0 : MenuItem& AbstractMenuModel::item(int index)
202 : {
203 0 : MenuItem& item = *m_items[index];
204 0 : if (item.isValid()) {
205 0 : return item;
206 : }
207 :
208 0 : static MenuItem dummy;
209 0 : return dummy;
210 : }
211 :
212 0 : MenuItem& AbstractMenuModel::findItem(const QString& itemId)
213 : {
214 0 : return item(m_items, itemId);
215 : }
216 :
217 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
218 0 : MenuItem& AbstractMenuModel::findItem(const ActionCode& actionCode)
219 : {
220 0 : MenuItemList list = items(m_items, actionCode);
221 0 : if (list.empty()) {
222 0 : static MenuItem dummy;
223 0 : return dummy;
224 : }
225 :
226 0 : if (list.size() > 1) {
227 0 : LOGD() << "There is more than one item for " << actionCode << ", will return the first one found";
228 : }
229 :
230 0 : return *list.front();
231 0 : }
232 :
233 0 : MenuItemList AbstractMenuModel::findItems(const ActionCode& actionCode)
234 : {
235 0 : return items(m_items, actionCode);
236 : }
237 :
238 : #endif
239 :
240 0 : MenuItem& AbstractMenuModel::findItem(const muse::rcommand::Command& command)
241 : {
242 0 : MenuItemList list = items(m_items, command);
243 0 : if (list.empty()) {
244 0 : static MenuItem dummy;
245 0 : return dummy;
246 : }
247 :
248 0 : if (list.size() > 1) {
249 0 : LOGD() << "There is more than one item for " << command << ", will return the first one found";
250 : }
251 :
252 0 : return *list.front();
253 0 : }
254 :
255 0 : MenuItemList AbstractMenuModel::findItems(const muse::rcommand::Command& command)
256 : {
257 0 : return items(m_items, command);
258 : }
259 :
260 0 : MenuItem& AbstractMenuModel::findMenu(const QString& menuId)
261 : {
262 0 : return menu(m_items, menuId);
263 : }
264 :
265 0 : MenuItem* AbstractMenuModel::makeMenu(const TranslatableString& title, const MenuItemList& items,
266 : const QString& menuId, bool enabled)
267 : {
268 0 : MenuItem* item = new MenuItem(this);
269 0 : item->setId(menuId);
270 0 : item->setTitle(title);
271 0 : item->setSubitems(items);
272 0 : item->setEnabled(enabled);
273 :
274 0 : updateShortcuts(item);
275 :
276 0 : return item;
277 : }
278 :
279 6 : MenuItem* AbstractMenuModel::makeMenuItem(const muse::rcommand::Command& command,
280 : const muse::rcommand::Params& params,
281 : const TranslatableString& title)
282 : {
283 6 : const rcommand::CommandInfo& info = commandsRegister()->commandInfo(command);
284 6 : if (!info.isValid()) {
285 0 : LOGW() << "not found command: " << command.toString();
286 0 : return nullptr;
287 : }
288 :
289 6 : MenuItem* item = new MenuItem(info, this);
290 6 : item->setCommand(command);
291 6 : item->setParams(params);
292 6 : item->setCommandState(commandsState()->commandState(command));
293 :
294 6 : if (!title.isEmpty()) {
295 6 : item->setTitle(title);
296 : }
297 :
298 6 : return item;
299 : }
300 :
301 6 : MenuItem* AbstractMenuModel::makeMenuItem(const rcommand::Command& command, const TranslatableString& title)
302 : {
303 6 : return makeMenuItem(command, {}, title);
304 : }
305 :
306 0 : MenuItem* AbstractMenuModel::makeMenuItem(const rcommand::CommandQuery& query, const TranslatableString& title)
307 : {
308 0 : return makeMenuItem(rcommand::Command(query.uri()), query.params(), title);
309 : }
310 :
311 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
312 0 : MenuItem* AbstractMenuModel::makeMenuItem(const ActionCode& actionCode, const TranslatableString& title)
313 : {
314 0 : if (muse::strings::startsWith(actionCode, "command://")) {
315 0 : return makeMenuItem(rcommand::CommandQuery(actionCode), title);
316 : }
317 :
318 0 : const UiAction& action = uiActionsRegister()->action(actionCode);
319 0 : if (!action.isValid()) {
320 0 : LOGW() << "not found action: " << actionCode;
321 0 : return nullptr;
322 : }
323 :
324 0 : MenuItem* item = new MenuItem(action, this);
325 0 : item->setState(uiActionsRegister()->actionState(actionCode));
326 :
327 0 : if (!title.isEmpty()) {
328 0 : item->setTitle(title);
329 : }
330 :
331 0 : ActionQuery q(actionCode);
332 0 : if (q.isValid()) {
333 0 : item->setQuery(q);
334 : }
335 :
336 0 : return item;
337 0 : }
338 :
339 : #endif
340 :
341 0 : MenuItem* AbstractMenuModel::makeSeparator()
342 : {
343 0 : MenuItem* item = new MenuItem(this);
344 0 : item->setTitle({});
345 0 : return item;
346 : }
347 :
348 0 : void AbstractMenuModel::subscribeOnChanges()
349 : {
350 0 : if (m_subscribedOnChanges) {
351 0 : return;
352 : }
353 :
354 0 : commandsState()->commandStateChanged().onReceive(this, [this](const rcommand::Command& command, const rcommand::CommandState& state) {
355 0 : onCommandStateChanged(command, state);
356 0 : });
357 :
358 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
359 0 : uiActionsRegister()->actionStateChanged().onReceive(this, [this](const ActionCodeList& codes) {
360 0 : onActionsStateChanges(codes);
361 0 : });
362 : #endif
363 :
364 : #ifdef MUSE_MODULE_SHORTCUTS_V2
365 0 : commandShortcutsRegister()->shortcutsChanged().onNotify(this, [this]() {
366 0 : updateShortcutsAll();
367 0 : });
368 : #else
369 : shortcutsRegister()->shortcutsChanged().onNotify(this, [this]() {
370 : updateShortcutsAll();
371 : });
372 : #endif
373 :
374 0 : m_subscribedOnChanges = true;
375 : }
376 :
377 0 : void AbstractMenuModel::onCommandStateChanged(const rcommand::Command& command, const rcommand::CommandState& state)
378 : {
379 0 : updateState(m_items, command, state);
380 0 : }
381 :
382 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
383 0 : void AbstractMenuModel::onActionsStateChanges(const muse::actions::ActionCodeList& codes)
384 : {
385 0 : TRACEFUNC;
386 :
387 0 : if (codes.empty()) {
388 0 : return;
389 : }
390 :
391 0 : std::map<actions::ActionCode, ui::UiActionState> states;
392 0 : updateState(m_items, codes, states);
393 0 : }
394 :
395 : #endif
396 :
397 0 : void AbstractMenuModel::setItem(int index, MenuItem* item)
398 : {
399 0 : if (!isIndexValid(index)) {
400 0 : return;
401 : }
402 :
403 0 : m_items[index] = item;
404 :
405 0 : QModelIndex modelIndex = this->index(index);
406 0 : emit dataChanged(modelIndex, modelIndex);
407 : }
408 :
409 0 : MenuItem& AbstractMenuModel::item(MenuItemList& items, const QString& itemId)
410 : {
411 0 : for (MenuItem* menuItem : items) {
412 0 : if (!menuItem) {
413 0 : continue;
414 : }
415 0 : if (menuItem->id() == itemId) {
416 0 : return *menuItem;
417 : }
418 :
419 0 : auto subitems = menuItem->subitems();
420 0 : if (!subitems.empty()) {
421 0 : MenuItem& subitem = item(subitems, itemId);
422 0 : if (subitem.id() == itemId) {
423 0 : return subitem;
424 : }
425 : }
426 0 : }
427 :
428 0 : static MenuItem dummy;
429 0 : return dummy;
430 : }
431 :
432 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
433 0 : MenuItemList AbstractMenuModel::items(const MenuItemList& items, const ActionCode& actionCode) const
434 : {
435 0 : MenuItemList result;
436 :
437 0 : for (MenuItem* menuItem : items) {
438 0 : if (!menuItem) {
439 0 : continue;
440 : }
441 :
442 0 : if (menuItem->actionCode() == actionCode) {
443 0 : result.append(menuItem);
444 : }
445 :
446 0 : auto subitems = menuItem->subitems();
447 0 : if (!subitems.empty()) {
448 0 : MenuItemList list = this->items(subitems, actionCode);
449 0 : if (!list.empty()) {
450 0 : result.append(list);
451 : }
452 0 : }
453 0 : }
454 :
455 0 : return result;
456 0 : }
457 :
458 : #endif
459 :
460 0 : MenuItemList AbstractMenuModel::items(const MenuItemList& items, const muse::rcommand::Command& command) const
461 : {
462 0 : MenuItemList result;
463 :
464 0 : for (MenuItem* menuItem : items) {
465 0 : if (!menuItem) {
466 0 : continue;
467 : }
468 :
469 0 : if (menuItem->command() == command) {
470 0 : result.append(menuItem);
471 : }
472 :
473 0 : auto subitems = menuItem->subitems();
474 0 : if (!subitems.empty()) {
475 0 : MenuItemList list = this->items(subitems, command);
476 0 : if (!list.empty()) {
477 0 : result.append(list);
478 : }
479 0 : }
480 0 : }
481 :
482 0 : return result;
483 0 : }
484 :
485 0 : MenuItem& AbstractMenuModel::menu(MenuItemList& items, const QString& menuId)
486 : {
487 0 : for (MenuItem* item : items) {
488 0 : if (!item) {
489 0 : continue;
490 : }
491 :
492 0 : if (item->id() == menuId) {
493 0 : return *item;
494 : }
495 :
496 0 : auto subitems = item->subitems();
497 0 : MenuItem& menuItem = menu(subitems, menuId);
498 0 : if (menuItem.isValid()) {
499 0 : return menuItem;
500 : }
501 0 : }
502 :
503 0 : static MenuItem dummy;
504 0 : return dummy;
505 : }
506 :
507 0 : void AbstractMenuModel::updateState(MenuItemList& items, const rcommand::Command& command, const rcommand::CommandState& state)
508 : {
509 0 : for (MenuItem* menuItem : items) {
510 0 : if (!menuItem) {
511 0 : continue;
512 : }
513 :
514 0 : if (command == menuItem->command()) {
515 0 : menuItem->setCommandState(state);
516 : }
517 :
518 0 : MenuItemList subitems = menuItem->subitems();
519 0 : if (!subitems.empty()) {
520 0 : updateState(subitems, command, state);
521 : }
522 0 : }
523 0 : }
524 :
525 : #ifdef MUSE_MODULE_ACTIONS_SUPPORT
526 0 : void AbstractMenuModel::updateState(MenuItemList& items, const actions::ActionCodeList& codes,
527 : std::map<actions::ActionCode, ui::UiActionState>& states)
528 : {
529 0 : for (MenuItem* menuItem : items) {
530 0 : if (!menuItem) {
531 0 : continue;
532 : }
533 :
534 0 : ActionCode code = menuItem->actionCode();
535 0 : if (muse::contains(codes, code)) {
536 0 : if (!muse::contains(states, code)) {
537 0 : states.insert({ code, uiActionsRegister()->actionState(code) });
538 : }
539 0 : menuItem->setState(states.at(code));
540 : }
541 :
542 0 : MenuItemList subitems = menuItem->subitems();
543 0 : if (!subitems.empty()) {
544 0 : updateState(subitems, codes, states);
545 : }
546 0 : }
547 0 : }
548 :
549 : #endif
550 :
551 3 : void AbstractMenuModel::updateShortcutsAll()
552 : {
553 9 : for (MenuItem* menuItem : m_items) {
554 6 : if (!menuItem) {
555 0 : continue;
556 : }
557 :
558 6 : updateShortcuts(menuItem);
559 : }
560 3 : }
561 :
562 6 : void AbstractMenuModel::updateShortcuts(MenuItem* item)
563 : {
564 6 : std::vector<std::string> shortcuts;
565 :
566 : #ifdef MUSE_MODULE_SHORTCUTS_V2
567 6 : shortcuts = commandShortcutsRegister()->shortcut(item->command()).sequences;
568 : #else
569 : shortcuts = shortcutsRegister()->shortcut(item->actionCode()).sequences;
570 : #endif
571 6 : item->setShortcuts(shortcuts);
572 :
573 6 : for (MenuItem* subItem : item->subitems()) {
574 0 : if (!subItem) {
575 0 : continue;
576 : }
577 :
578 0 : updateShortcuts(subItem);
579 6 : }
580 6 : }
|