File indexing completed on 2024-05-05 17:43:53

0001 /*
0002     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "window.h"
0008 
0009 #include "debug.h"
0010 
0011 #include <QDBusConnection>
0012 #include <QDBusMessage>
0013 #include <QDBusPendingCallWatcher>
0014 #include <QDBusPendingReply>
0015 #include <QDebug>
0016 #include <QList>
0017 #include <QMutableListIterator>
0018 #include <QVariantList>
0019 
0020 #include <algorithm>
0021 
0022 #include "actions.h"
0023 #include "dbusmenuadaptor.h"
0024 #include "icons.h"
0025 #include "menu.h"
0026 #include "utils.h"
0027 
0028 #include "../libdbusmenuqt/dbusmenushortcut_p.h"
0029 
0030 static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions");
0031 static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus");
0032 
0033 static const QString s_applicationActionsPrefix = QStringLiteral("app.");
0034 static const QString s_unityActionsPrefix = QStringLiteral("unity.");
0035 static const QString s_windowActionsPrefix = QStringLiteral("win.");
0036 
0037 Window::Window(const QString &serviceName)
0038     : QObject()
0039     , m_serviceName(serviceName)
0040 {
0041     qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName;
0042 
0043     Q_ASSERT(!serviceName.isEmpty());
0044 
0045     GDBusMenuTypes_register();
0046     DBusMenuTypes_register();
0047 }
0048 
0049 Window::~Window() = default;
0050 
0051 void Window::init()
0052 {
0053     qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win"
0054                            << m_windowObjectPath << "unity" << m_unityObjectPath;
0055 
0056     if (!m_applicationMenuObjectPath.isEmpty()) {
0057         m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this);
0058         connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties);
0059         connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
0060         connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed);
0061         // basically so it replies on DBus no matter what
0062         connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
0063         connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged);
0064         connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged);
0065     }
0066 
0067     if (!m_menuBarObjectPath.isEmpty()) {
0068         m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this);
0069         connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties);
0070         connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties);
0071         connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed);
0072         connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed);
0073         connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged);
0074         connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged);
0075     }
0076 
0077     if (!m_applicationObjectPath.isEmpty()) {
0078         m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this);
0079         connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
0080             onActionsChanged(dirtyActions, s_applicationActionsPrefix);
0081         });
0082         connect(m_applicationActions, &Actions::loaded, this, [this] {
0083             if (m_menuInited) {
0084                 onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix);
0085             } else {
0086                 initMenu();
0087             }
0088         });
0089         m_applicationActions->load();
0090     }
0091 
0092     if (!m_unityObjectPath.isEmpty()) {
0093         m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this);
0094         connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
0095             onActionsChanged(dirtyActions, s_unityActionsPrefix);
0096         });
0097         connect(m_unityActions, &Actions::loaded, this, [this] {
0098             if (m_menuInited) {
0099                 onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix);
0100             } else {
0101                 initMenu();
0102             }
0103         });
0104         m_unityActions->load();
0105     }
0106 
0107     if (!m_windowObjectPath.isEmpty()) {
0108         m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this);
0109         connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) {
0110             onActionsChanged(dirtyActions, s_windowActionsPrefix);
0111         });
0112         connect(m_windowActions, &Actions::loaded, this, [this] {
0113             if (m_menuInited) {
0114                 onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix);
0115             } else {
0116                 initMenu();
0117             }
0118         });
0119         m_windowActions->load();
0120     }
0121 }
0122 
0123 WId Window::winId() const
0124 {
0125     return m_winId;
0126 }
0127 
0128 void Window::setWinId(WId winId)
0129 {
0130     m_winId = winId;
0131 }
0132 
0133 QString Window::serviceName() const
0134 {
0135     return m_serviceName;
0136 }
0137 
0138 QString Window::applicationObjectPath() const
0139 {
0140     return m_applicationObjectPath;
0141 }
0142 
0143 void Window::setApplicationObjectPath(const QString &applicationObjectPath)
0144 {
0145     m_applicationObjectPath = applicationObjectPath;
0146 }
0147 
0148 QString Window::unityObjectPath() const
0149 {
0150     return m_unityObjectPath;
0151 }
0152 
0153 void Window::setUnityObjectPath(const QString &unityObjectPath)
0154 {
0155     m_unityObjectPath = unityObjectPath;
0156 }
0157 
0158 QString Window::applicationMenuObjectPath() const
0159 {
0160     return m_applicationMenuObjectPath;
0161 }
0162 
0163 void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath)
0164 {
0165     m_applicationMenuObjectPath = applicationMenuObjectPath;
0166 }
0167 
0168 QString Window::menuBarObjectPath() const
0169 {
0170     return m_menuBarObjectPath;
0171 }
0172 
0173 void Window::setMenuBarObjectPath(const QString &menuBarObjectPath)
0174 {
0175     m_menuBarObjectPath = menuBarObjectPath;
0176 }
0177 
0178 QString Window::windowObjectPath() const
0179 {
0180     return m_windowObjectPath;
0181 }
0182 
0183 void Window::setWindowObjectPath(const QString &windowObjectPath)
0184 {
0185     m_windowObjectPath = windowObjectPath;
0186 }
0187 
0188 QString Window::currentMenuObjectPath() const
0189 {
0190     return m_currentMenuObjectPath;
0191 }
0192 
0193 QString Window::proxyObjectPath() const
0194 {
0195     return m_proxyObjectPath;
0196 }
0197 
0198 void Window::initMenu()
0199 {
0200     if (m_menuInited) {
0201         return;
0202     }
0203 
0204     if (!registerDBusObject()) {
0205         return;
0206     }
0207 
0208     // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none
0209     // so we subscribe to the menu bar as soon as it shows up so we can figure out
0210     // if we have a menu bar, an app menu, or just nothing
0211     if (m_applicationMenu) {
0212         m_applicationMenu->start(0);
0213     }
0214 
0215     if (m_menuBar) {
0216         m_menuBar->start(0);
0217     }
0218 
0219     m_menuInited = true;
0220 }
0221 
0222 void Window::menuItemsChanged(const QVector<uint> &itemIds)
0223 {
0224     if (qobject_cast<Menu *>(sender()) != m_currentMenu) {
0225         return;
0226     }
0227 
0228     DBusMenuItemList items;
0229 
0230     for (uint id : itemIds) {
0231         const auto newItem = m_currentMenu->getItem(id);
0232 
0233         DBusMenuItem dBusItem{// 0 is menu, items start at 1
0234                               static_cast<int>(id),
0235                               gMenuToDBusMenuProperties(newItem)};
0236         items.append(dBusItem);
0237     }
0238 
0239     Q_EMIT ItemsPropertiesUpdated(items, {});
0240 }
0241 
0242 void Window::menuChanged(const QVector<uint> &menuIds)
0243 {
0244     if (qobject_cast<Menu *>(sender()) != m_currentMenu) {
0245         return;
0246     }
0247 
0248     for (uint menu : menuIds) {
0249         Q_EMIT LayoutUpdated(3 /*revision*/, menu);
0250     }
0251 }
0252 
0253 void Window::onMenuSubscribed(uint id)
0254 {
0255     // When it was a delayed GetLayout request, send the reply now
0256     const auto pendingReplies = m_pendingGetLayouts.values(id);
0257     if (!pendingReplies.isEmpty()) {
0258         for (const auto &pendingReply : pendingReplies) {
0259             if (pendingReply.type() != QDBusMessage::InvalidMessage) {
0260                 auto reply = pendingReply.createReply();
0261 
0262                 DBusMenuLayoutItem item;
0263                 uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item);
0264 
0265                 reply << revision << QVariant::fromValue(item);
0266 
0267                 QDBusConnection::sessionBus().send(reply);
0268             }
0269         }
0270         m_pendingGetLayouts.remove(id);
0271     } else {
0272         Q_EMIT LayoutUpdated(2 /*revision*/, id);
0273     }
0274 }
0275 
0276 bool Window::getAction(const QString &name, GMenuAction &action) const
0277 {
0278     QString lookupName;
0279     Actions *actions = getActionsForAction(name, lookupName);
0280 
0281     if (!actions) {
0282         return false;
0283     }
0284 
0285     return actions->get(lookupName, action);
0286 }
0287 
0288 void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp)
0289 {
0290     QString lookupName;
0291     Actions *actions = getActionsForAction(name, lookupName);
0292     if (!actions) {
0293         return;
0294     }
0295 
0296     actions->trigger(lookupName, target, timestamp);
0297 }
0298 
0299 Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const
0300 {
0301     if (name.startsWith(QLatin1String("app."))) {
0302         lookupName = name.mid(4);
0303         return m_applicationActions;
0304     } else if (name.startsWith(QLatin1String("unity."))) {
0305         lookupName = name.mid(6);
0306         return m_unityActions;
0307     } else if (name.startsWith(QLatin1String("win."))) {
0308         lookupName = name.mid(4);
0309         return m_windowActions;
0310     }
0311 
0312     return nullptr;
0313 }
0314 
0315 void Window::onActionsChanged(const QStringList &dirty, const QString &prefix)
0316 {
0317     if (m_applicationMenu) {
0318         m_applicationMenu->actionsChanged(dirty, prefix);
0319     }
0320     if (m_menuBar) {
0321         m_menuBar->actionsChanged(dirty, prefix);
0322     }
0323 }
0324 
0325 bool Window::registerDBusObject()
0326 {
0327     Q_ASSERT(m_proxyObjectPath.isEmpty());
0328 
0329     static int menus = 0;
0330     ++menus;
0331 
0332     new DbusmenuAdaptor(this);
0333 
0334     const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus));
0335     qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath;
0336 
0337     if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) {
0338         qCWarning(DBUSMENUPROXY) << "Failed to register object";
0339         return false;
0340     }
0341 
0342     m_proxyObjectPath = objectPath;
0343 
0344     return true;
0345 }
0346 
0347 void Window::updateWindowProperties()
0348 {
0349     const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu()));
0350 
0351     if (!hasMenu) {
0352         Q_EMIT requestRemoveWindowProperties();
0353         return;
0354     }
0355 
0356     Menu *oldMenu = m_currentMenu;
0357     Menu *newMenu = qobject_cast<Menu *>(sender());
0358     // set current menu as needed
0359     if (!m_currentMenu) {
0360         m_currentMenu = newMenu;
0361         // Menu Bar takes precedence over application menu
0362     } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) {
0363         qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar";
0364         m_currentMenu = newMenu;
0365         // TODO update layout
0366     }
0367 
0368     if (m_currentMenu != oldMenu) {
0369         // update entire menu now
0370         Q_EMIT LayoutUpdated(4 /*revision*/, 0);
0371     }
0372 
0373     Q_EMIT requestWriteWindowProperties();
0374 }
0375 
0376 // DBus
0377 bool Window::AboutToShow(int id)
0378 {
0379     // We always request the first time GetLayout is called and keep up-to-date internally
0380     // No need to have us prepare anything here
0381     Q_UNUSED(id);
0382     return false;
0383 }
0384 
0385 void Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp)
0386 {
0387     Q_UNUSED(data);
0388 
0389     if (!m_currentMenu) {
0390         return;
0391     }
0392 
0393     // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked"
0394 
0395     if (eventId == QLatin1String("clicked")) {
0396         const QVariantMap item = m_currentMenu->getItem(id);
0397         const QString action = item.value(QStringLiteral("action")).toString();
0398         const QVariant target = item.value(QStringLiteral("target"));
0399         if (!action.isEmpty()) {
0400             triggerAction(action, target, timestamp);
0401         }
0402     }
0403 }
0404 
0405 DBusMenuItemList Window::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames)
0406 {
0407     Q_UNUSED(ids);
0408     Q_UNUSED(propertyNames);
0409     return DBusMenuItemList();
0410 }
0411 
0412 uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem)
0413 {
0414     Q_UNUSED(recursionDepth); // TODO
0415     Q_UNUSED(propertyNames);
0416 
0417     int subscription;
0418     int sectionId;
0419     int index;
0420 
0421     Utils::intToTreeStructure(parentId, subscription, sectionId, index);
0422 
0423     if (!m_currentMenu) {
0424         return 1;
0425     }
0426 
0427     if (!m_currentMenu->hasSubscription(subscription)) {
0428         // let's serve multiple similar requests in one go once we've processed them
0429         m_pendingGetLayouts.insert(subscription, message());
0430         setDelayedReply(true);
0431 
0432         m_currentMenu->start(subscription);
0433         return 1;
0434     }
0435 
0436     bool ok;
0437     const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok);
0438 
0439     if (!ok) {
0440         qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId;
0441         return 1;
0442     }
0443 
0444     // If a particular entry is requested, see what it is and resolve as necessary
0445     // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0
0446     // so resolve that and return the correct menu
0447     if (index > 0) {
0448         // non-zero index indicates item within a menu but the index in the list still starts at zero
0449         if (section.items.count() < index) {
0450             qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds";
0451             return 0;
0452         }
0453 
0454         const auto &requestedItem = section.items.at(index - 1);
0455 
0456         auto it = requestedItem.constFind(QStringLiteral(":submenu"));
0457         if (it != requestedItem.constEnd()) {
0458             const GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
0459             return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem);
0460         } else {
0461             // TODO
0462             return 0;
0463         }
0464     }
0465 
0466     dbusItem.id = parentId; // TODO
0467     dbusItem.properties = {{QStringLiteral("children-display"), QStringLiteral("submenu")}};
0468 
0469     int count = 0;
0470 
0471     const auto itemsToBeAdded = section.items;
0472     for (const auto &item : itemsToBeAdded) {
0473         DBusMenuLayoutItem child{
0474             Utils::treeStructureToInt(section.id, sectionId, ++count),
0475             gMenuToDBusMenuProperties(item),
0476             {} // children
0477         };
0478         dbusItem.children.append(child);
0479 
0480         // Now resolve section aliases
0481         auto it = item.constFind(QStringLiteral(":section"));
0482         if (it != item.constEnd()) {
0483             // references another place, add it instead
0484             GMenuSection gmenuSection = qdbus_cast<GMenuSection>(it->value<QDBusArgument>());
0485 
0486             // remember where the item came from and give it an appropriate ID
0487             // so updates signalled by the app will map to the right place
0488             int originalSubscription = gmenuSection.subscription;
0489             int originalMenu = gmenuSection.menu;
0490 
0491             // TODO start subscription if we don't have it
0492             auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items;
0493 
0494             // Check whether it's an alias to an alias
0495             // FIXME make generic/recursive
0496             if (items.count() == 1) {
0497                 const auto &aliasedItem = items.constFirst();
0498                 auto findIt = aliasedItem.constFind(QStringLiteral(":section"));
0499                 if (findIt != aliasedItem.constEnd()) {
0500                     GMenuSection gmenuSection2 = qdbus_cast<GMenuSection>(findIt->value<QDBusArgument>());
0501                     items = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items;
0502 
0503                     originalSubscription = gmenuSection2.subscription;
0504                     originalMenu = gmenuSection2.menu;
0505                 }
0506             }
0507 
0508             int aliasedCount = 0;
0509             for (const auto &aliasedItem : qAsConst(items)) {
0510                 DBusMenuLayoutItem aliasedChild{
0511                     Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount),
0512                     gMenuToDBusMenuProperties(aliasedItem),
0513                     {} // children
0514                 };
0515                 dbusItem.children.append(aliasedChild);
0516             }
0517         }
0518     }
0519 
0520     // revision, unused in libdbusmenuqt
0521     return 1;
0522 }
0523 
0524 QDBusVariant Window::GetProperty(int id, const QString &property)
0525 {
0526     Q_UNUSED(id);
0527     Q_UNUSED(property);
0528     QDBusVariant value;
0529     return value;
0530 }
0531 
0532 QString Window::status() const
0533 {
0534     return QStringLiteral("normal");
0535 }
0536 
0537 uint Window::version() const
0538 {
0539     return 4;
0540 }
0541 
0542 QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const
0543 {
0544     QVariantMap result;
0545 
0546     result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString());
0547 
0548     if (source.contains(QLatin1String(":section"))) {
0549         result.insert(QStringLiteral("type"), QStringLiteral("separator"));
0550     }
0551 
0552     const bool isMenu = source.contains(QLatin1String(":submenu"));
0553     if (isMenu) {
0554         result.insert(QStringLiteral("children-display"), QStringLiteral("submenu"));
0555     }
0556 
0557     QString accel = source.value(QStringLiteral("accel")).toString();
0558     if (!accel.isEmpty()) {
0559         QStringList shortcut;
0560 
0561         // TODO use regexp or something
0562         if (accel.contains(QLatin1String("<Primary>")) || accel.contains(QLatin1String("<Control>"))) {
0563             shortcut.append(QStringLiteral("Control"));
0564             accel.remove(QLatin1String("<Primary>"));
0565             accel.remove(QLatin1String("<Control>"));
0566         }
0567 
0568         if (accel.contains(QLatin1String("<Shift>"))) {
0569             shortcut.append(QStringLiteral("Shift"));
0570             accel.remove(QLatin1String("<Shift>"));
0571         }
0572 
0573         if (accel.contains(QLatin1String("<Alt>"))) {
0574             shortcut.append(QStringLiteral("Alt"));
0575             accel.remove(QLatin1String("<Alt>"));
0576         }
0577 
0578         if (accel.contains(QLatin1String("<Super>"))) {
0579             shortcut.append(QStringLiteral("Super"));
0580             accel.remove(QLatin1String("<Super>"));
0581         }
0582 
0583         if (!accel.isEmpty()) {
0584             // TODO replace "+" by "plus" and "-" by "minus"
0585             shortcut.append(accel);
0586 
0587             // TODO does gmenu support multiple?
0588             DBusMenuShortcut dbusShortcut;
0589             dbusShortcut.append(shortcut); // don't let it unwrap the list we append
0590 
0591             result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut));
0592         }
0593     }
0594 
0595     bool enabled = true;
0596 
0597     const QString actionName = Utils::itemActionName(source);
0598 
0599     GMenuAction action;
0600     // if no action is specified this is fine but if there is an action we don't have
0601     // disable the menu entry
0602     bool actionOk = true;
0603     if (!actionName.isEmpty()) {
0604         actionOk = getAction(actionName, action);
0605         enabled = actionOk && action.enabled;
0606     }
0607 
0608     // we used to only send this if not enabled but then dbusmenuimporter does not
0609     // update the enabled state when it changes from disabled to enabled
0610     result.insert(QStringLiteral("enabled"), enabled);
0611 
0612     bool visible = true;
0613     const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString();
0614     if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) {
0615         visible = false;
0616     } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) {
0617         visible = false;
0618         // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate)
0619     } else if (hiddenWhen == QLatin1String("macos-menubar")) {
0620         visible = true;
0621     }
0622 
0623     result.insert(QStringLiteral("visible"), visible);
0624 
0625     QString icon = source.value(QStringLiteral("icon")).toString();
0626     if (icon.isEmpty()) {
0627         icon = source.value(QStringLiteral("verb-icon")).toString();
0628     }
0629 
0630     icon = Icons::actionIcon(actionName);
0631     if (!icon.isEmpty()) {
0632         result.insert(QStringLiteral("icon-name"), icon);
0633     }
0634 
0635     const QVariant target = source.value(QStringLiteral("target"));
0636 
0637     if (actionOk) {
0638         const auto actionStates = action.state;
0639         if (actionStates.count() == 1) {
0640             const auto &actionState = actionStates.first();
0641             // assume this is a checkbox
0642             if (!isMenu) {
0643                 if (actionState.type() == QVariant::Bool) {
0644                     result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox"));
0645                     result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0);
0646                 } else if (actionState.type() == QVariant::String) {
0647                     result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio"));
0648                     result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0);
0649                 }
0650             }
0651         }
0652     }
0653 
0654     return result;
0655 }