From: dish Date: Thu, 12 Sep 2024 05:19:32 +0000 (+0000) Subject: [bos #42003][EDF] (2024) Shortcuts improvements X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=HEAD;p=modules%2Fgui.git [bos #42003][EDF] (2024) Shortcuts improvements Multiple changes: 0) Add possibility to nest action into folders or folder-actions. Show shortcuts of different modules in separate tab; 1) Make undo/redo action meta-actions; 2) Add default icons; 3) Style descendant folders if ascendant key sequence is modified; Highlight tab if key sequence of a shortcut in a module is modified; 4) Show tooltips in Shortcut Editor; 5) Adjust column widths in Shortcut Editor; 6) Add search in Shortcut Editor; 7) Make logging clearer; 8) Add SUIT_ShortcutMgr::createAction(..); 9) Update doc; 10) Methods, which retrieve action assets, look into root module asset file if the action is meta-action; 11) Since action IDs were changed (at least they were prepended with folder IDs), but old IDs remain in user preference files, there were conflicts; To avoid this, name of shortcut section was appended with version of shortcut mgr; 12) Allow "Del" key to be set in Shortcut Editor. --- diff --git a/src/CAM/CAM_Module.cxx b/src/CAM/CAM_Module.cxx index a88c8e721..a56b92da8 100644 --- a/src/CAM/CAM_Module.cxx +++ b/src/CAM/CAM_Module.cxx @@ -984,19 +984,19 @@ int CAM_Module::actionId( const QAction* a ) const \param icon action icon \param menu menu text \param tip status bar tip - \param key will be disabled by SUIT_ShortcutMgr! + \param key May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true, the action will be toggled - \param reciever action activation signal receiver object + \param receiver action activation signal receiver object \param member action activation signal receiver slot \param inModuleActionID module-unique action ID */ QAction* CAM_Module::createAction( const int id, const QString& text, const QIcon& icon, const QString& menu, const QString& tip, const int key, - QObject* parent, const bool toggle, QObject* reciever, + QObject* parent, const bool toggle, QObject* receiver, const char* member, const QString& inModuleActionID ) { - return createAction( id, text, icon, menu, tip, QKeySequence(key), parent, toggle, reciever, member, inModuleActionID ); + return createAction( id, text, icon, menu, tip, QKeySequence(key), parent, toggle, receiver, member, inModuleActionID ); } /*! @@ -1012,16 +1012,16 @@ QAction* CAM_Module::createAction( const int id, const QString& text, const QIco \param icon action icon \param menu menu text \param tip status bar tip - \param key will be disabled by SUIT_ShortcutMgr! + \param key May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true, the action will be toggled - \param reciever action activation signal receiver object + \param receiver action activation signal receiver object \param member action activation signal receiver slot \param inModuleActionID module-unique action ID */ QAction* CAM_Module::createAction( const int id, const QString& text, const QIcon& icon, const QString& menu, const QString& tip, const QKeySequence& key, - QObject* parent, const bool toggle, QObject* reciever, + QObject* parent, const bool toggle, QObject* receiver, const char* member, const QString& inModuleActionID ) { const QString actionID = makeActionID(inModuleActionID); @@ -1030,8 +1030,8 @@ QAction* CAM_Module::createAction( const int id, const QString& text, const QIco connect( a, SIGNAL( triggered( bool ) ), this, SLOT( moduleActionActivated() ), Qt::UniqueConnection ); - if ( reciever && member ) - connect( a, SIGNAL( triggered( bool ) ), reciever, member ); + if ( receiver && member ) + connect( a, SIGNAL( triggered( bool ) ), receiver, member ); registerAction( id, a ); diff --git a/src/LightApp/LightApp_Application.cxx b/src/LightApp/LightApp_Application.cxx index a83faafc4..edd729cb7 100644 --- a/src/LightApp/LightApp_Application.cxx +++ b/src/LightApp/LightApp_Application.cxx @@ -646,7 +646,7 @@ void LightApp_Application::createActionForViewer( const int id, { QString vtlt = tr( QString( "NEW_WINDOW_%1" ).arg( suffix ).toLatin1().constData() ); QString tip = tr( "CREATING_NEW_WINDOW" ).arg( vtlt.remove( "&" ) ); - QAction* a = createAction(id, desktop(), false /*toggle*/, "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_" + suffix, + QAction* a = createAction(id, desktop(), false /*toggle*/, "/Window/New/ForViewer" + suffix, tip, vtlt, tip, QIcon(), this, SLOT(onNewWindow())); @@ -665,7 +665,7 @@ void LightApp_Application::createActions() // Preferences createAction( PreferencesId, tr( "TOT_DESK_PREFERENCES" ), QIcon(), tr( "MEN_DESK_PREFERENCES" ), tr( "PRP_DESK_PREFERENCES" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onPreferences() ), "/PRP_DESK_PREFERENCES" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onPreferences() ), "/File/Preferences" ); // Help menu @@ -791,10 +791,7 @@ void LightApp_Application::createActions() int windowMenu = createMenu( tr( "MEN_DESK_WINDOW" ), -1, MenuWindowId, 100 ); int newWinMenu = createMenu( tr( "MEN_DESK_NEWWINDOW" ), windowMenu, -1, 0 ); - createAction( CloseId, tr( "TOT_CLOSE" ), QIcon(), tr( "MEN_DESK_CLOSE" ), tr( "PRP_CLOSE" ), - Qt::CTRL+Qt::Key_F4, desk, false, this, SLOT( onCloseWindow() ) ); - - createAction( CloseId, desk, false /*toggle*/, "/PRP_CLOSE", + createAction( CloseId, desk, false /*toggle*/, "/Window/Close", tr( "TOT_CLOSE" ), tr( "MEN_DESK_CLOSE" ), tr( "PRP_CLOSE" ), QIcon(), this, SLOT( onCloseWindow() ) ); @@ -839,7 +836,7 @@ void LightApp_Application::createActions() createActionForViewer( NewQtViewId, newWinMenu, QString::number( 9 ) ); #endif - createAction( RenameId, desk, false /*toggle*/, "/PRP_RENAME", + createAction( RenameId, desk, false /*toggle*/, "/Window/Rename", tr( "TOT_RENAME" ), tr( "MEN_DESK_RENAME" ), tr( "PRP_RENAME" ), QIcon(), this, SLOT( onRenameWindow() ) ); @@ -859,7 +856,7 @@ void LightApp_Application::createActions() #endif // USE_SALOME_STYLE createAction( FullScreenId, tr( "TOT_FULLSCREEN" ), QIcon(), tr( "MEN_DESK_FULLSCREEN" ), tr( "PRP_FULLSCREEN" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onFullScreen() ), "/PRP_FULLSCREEN" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onFullScreen() ), "/View/FullScreen" ); int viewMenu = createMenu( tr( "MEN_DESK_VIEW" ), -1 ); diff --git a/src/LightApp/LightApp_Module.cxx b/src/LightApp/LightApp_Module.cxx index 903a037ca..a27c28845 100644 --- a/src/LightApp/LightApp_Module.cxx +++ b/src/LightApp/LightApp_Module.cxx @@ -455,9 +455,9 @@ QtxPopupMgr* LightApp_Module::popupMgr() QAction *disp = createAction( -1, tr( "TOP_SHOW" ), p, tr( "MEN_SHOW" ), tr( "STB_SHOW" ), - 0, d, false, this, SLOT( onShowHide() ), QString("#General/Object(s)/Show") ), + 0, d, false, this, SLOT( onShowHide() ), QString("Edit/#Object_Show") ), *erase = createAction( -1, tr( "TOP_HIDE" ), p, tr( "MEN_HIDE" ), tr( "STB_HIDE" ), - 0, d, false, this, SLOT( onShowHide() ) , QString("#General/Object(s)/Hide") ), + 0, d, false, this, SLOT( onShowHide() ) , QString("Edit/#Object_Hide") ), *dispOnly = createAction( -1, tr( "TOP_DISPLAY_ONLY" ), p, tr( "MEN_DISPLAY_ONLY" ), tr( "STB_DISPLAY_ONLY" ), 0, d, false, this, SLOT( onShowHide() ) ), *eraseAll = createAction( -1, tr( "TOP_ERASE_ALL" ), p, tr( "MEN_ERASE_ALL" ), tr( "STB_ERASE_ALL" ), diff --git a/src/LightApp/resources/LightApp.xml b/src/LightApp/resources/LightApp.xml index 3ba6a36e5..5e0d3cbf8 100644 --- a/src/LightApp/resources/LightApp.xml +++ b/src/LightApp/resources/LightApp.xml @@ -256,51 +256,53 @@ -
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/OCCViewer/OCCViewer_ViewWindow.cxx b/src/OCCViewer/OCCViewer_ViewWindow.cxx index 372b9145a..f83c4f8ad 100644 --- a/src/OCCViewer/OCCViewer_ViewWindow.cxx +++ b/src/OCCViewer/OCCViewer_ViewWindow.cxx @@ -1291,42 +1291,42 @@ void OCCViewer_ViewWindow::createActions() // Projections aAction = new QtxAction(tr("MNU_FRONT_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_FRONT" ) ), - tr( "MNU_FRONT_VIEW" ), 0, this, false, "/#Viewers/View/Set X-"); + tr( "MNU_FRONT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOX-"); aAction->setStatusTip(tr("DSC_FRONT_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onFrontView())); this->addAction(aAction); toolMgr()->registerAction( aAction, FrontId ); aAction = new QtxAction(tr("MNU_BACK_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_BACK" ) ), - tr( "MNU_BACK_VIEW" ), 0, this, false, "/#Viewers/View/Set X+"); + tr( "MNU_BACK_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOX+"); aAction->setStatusTip(tr("DSC_BACK_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onBackView())); this->addAction(aAction); toolMgr()->registerAction( aAction, BackId ); aAction = new QtxAction(tr("MNU_TOP_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_TOP" ) ), - tr( "MNU_TOP_VIEW" ), 0, this, false, "/#Viewers/View/Set Z-"); + tr( "MNU_TOP_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOZ-"); aAction->setStatusTip(tr("DSC_TOP_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onTopView())); this->addAction(aAction); toolMgr()->registerAction( aAction, TopId ); aAction = new QtxAction(tr("MNU_BOTTOM_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_BOTTOM" ) ), - tr( "MNU_BOTTOM_VIEW" ), 0, this, false, "/#Viewers/View/Set Z+"); + tr( "MNU_BOTTOM_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOZ+"); aAction->setStatusTip(tr("DSC_BOTTOM_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onBottomView())); this->addAction(aAction); toolMgr()->registerAction( aAction, BottomId ); aAction = new QtxAction(tr("MNU_LEFT_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_LEFT" ) ), - tr( "MNU_LEFT_VIEW" ), 0, this, false, "/#Viewers/View/Set Y+"); + tr( "MNU_LEFT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOY+"); aAction->setStatusTip(tr("DSC_LEFT_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onLeftView())); this->addAction(aAction); toolMgr()->registerAction( aAction, LeftId ); aAction = new QtxAction(tr("MNU_RIGHT_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_RIGHT" ) ), - tr( "MNU_RIGHT_VIEW" ), 0, this, false, "/#Viewers/View/Set Y-"); + tr( "MNU_RIGHT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOY-"); aAction->setStatusTip(tr("DSC_RIGHT_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onRightView())); this->addAction(aAction); @@ -1334,7 +1334,7 @@ void OCCViewer_ViewWindow::createActions() // rotate anticlockwise aAction = new QtxAction(tr("MNU_ANTICLOCKWISE_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_ANTICLOCKWISE" ) ), - tr( "MNU_ANTICLOCKWISE_VIEW" ), 0, this, false, "/#Viewers/View/Rotate anticlockwise"); + tr( "MNU_ANTICLOCKWISE_VIEW" ), 0, this, false, "/View/ViewPoint/#RotateAnticlockwise"); aAction->setStatusTip(tr("DSC_ANTICLOCKWISE_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onAntiClockWiseView())); this->addAction(aAction); @@ -1342,7 +1342,7 @@ void OCCViewer_ViewWindow::createActions() // rotate clockwise aAction = new QtxAction(tr("MNU_CLOCKWISE_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_CLOCKWISE" ) ), - tr( "MNU_CLOCKWISE_VIEW" ), 0, this, false, "/#Viewers/View/Rotate clockwise"); + tr( "MNU_CLOCKWISE_VIEW" ), 0, this, false, "/View/ViewPoint/#RotateClockwise"); aAction->setStatusTip(tr("DSC_CLOCKWISE_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onClockWiseView())); this->addAction(aAction); @@ -1380,7 +1380,7 @@ void OCCViewer_ViewWindow::createActions() // Reset aAction = new QtxAction(tr("MNU_RESET_VIEW"), aResMgr->loadPixmap( "OCCViewer", tr( "ICON_OCCVIEWER_VIEW_RESET" ) ), - tr( "MNU_RESET_VIEW" ), 0, this, false, "/#Viewers/View/Reset"); + tr( "MNU_RESET_VIEW" ), 0, this, false, "/View/ViewPoint/#Reset"); aAction->setStatusTip(tr("DSC_RESET_VIEW")); connect(aAction, SIGNAL(triggered()), this, SLOT(onResetView())); this->addAction(aAction); diff --git a/src/Qtx/Qtx.qrc b/src/Qtx/Qtx.qrc index dc31318e3..a7c688b1f 100644 --- a/src/Qtx/Qtx.qrc +++ b/src/Qtx/Qtx.qrc @@ -32,8 +32,6 @@ images/open.png images/close.png images/appicon.png - images/shortcut_disable.svg - images/shortcut_restore.svg diff --git a/src/Qtx/QtxAction.cxx b/src/Qtx/QtxAction.cxx index 4184b45cd..deba00490 100644 --- a/src/Qtx/QtxAction.cxx +++ b/src/Qtx/QtxAction.cxx @@ -121,7 +121,7 @@ QtxAction::QtxAction( QObject* parent, bool toggle, const QString& ID, \param text tooltip text \param icon iconset \param menuText menu text - \param accel will be disabled by SUIT_ShortcutMgr! + \param accel May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true the action will be a toggle action \param ID shortcut action identifier @@ -151,7 +151,7 @@ QtxAction::QtxAction( const QString& text, const QIcon& icon, const QString& men \param text tooltip text \param icon iconset \param menuText menu text - \param accel will be disabled by SUIT_ShortcutMgr! + \param accel May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true the action will be a toggle action \param ID shortcut action identifier @@ -180,7 +180,7 @@ QtxAction::QtxAction( const QString& text, const QIcon& icon, const QString& men \param text tooltip text \param menuText menu text - \param accel will be disabled by SUIT_ShortcutMgr! + \param accel May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true the action is a toggle action \param ID shortcut action identifier @@ -208,7 +208,7 @@ QtxAction::QtxAction( const QString& text, const QString& menuText, \param text tooltip text \param menuText menu text - \param accel will be disabled by SUIT_ShortcutMgr! + \param accel May be disabled by SUIT_ShortcutMgr! \param parent parent object \param toggle if \c true the action is a toggle action \param ID shortcut action identifier diff --git a/src/Qtx/QtxWorkstack.cxx b/src/Qtx/QtxWorkstack.cxx index c8a4f9ab4..b88a66250 100644 --- a/src/Qtx/QtxWorkstack.cxx +++ b/src/Qtx/QtxWorkstack.cxx @@ -1728,8 +1728,8 @@ QtxWorkstack::QtxWorkstack( QWidget* parent ) myWorkWin( 0 ), myWorkArea( 0 ) { - myActionsMap.insert( SplitVertical, new QtxAction( this, false /*toggle*/, "/PRP_DESK_WINDOW_VSPLIT", tr( "Split vertically" ) ) ); - myActionsMap.insert( SplitHorizontal, new QtxAction( this, false /*toggle*/, "/PRP_DESK_WINDOW_HSPLIT", tr( "Split horizontally" ) ) ); + myActionsMap.insert( SplitVertical, new QtxAction( this, false /*toggle*/, "/Window/SplitV", tr( "Split vertically" ) ) ); + myActionsMap.insert( SplitHorizontal, new QtxAction( this, false /*toggle*/, "/Window/SplitH", tr( "Split horizontally" ) ) ); myActionsMap.insert( Close, new QtxAction( this, false /*toggle*/, tr( "Close" ) ) ); myActionsMap.insert( Rename, new QtxAction( this, false /*toggle*/, tr( "Rename" ) ) ); diff --git a/src/Qtx/QtxWorkstackAction.cxx b/src/Qtx/QtxWorkstackAction.cxx index e441a5e59..58eba5628 100644 --- a/src/Qtx/QtxWorkstackAction.cxx +++ b/src/Qtx/QtxWorkstackAction.cxx @@ -125,13 +125,13 @@ QtxWorkstackAction::QtxWorkstackAction( QtxWorkstack* ws, QObject* parent ) if ( myWorkstack ) insertAction( myWorkstack->action( QtxWorkstack::SplitVertical ), SplitVertical ); else - insertAction( new QtxAction(this, false /*toggle*/, "/PRP_DESK_WINDOW_VSPLIT", + insertAction( new QtxAction(this, false /*toggle*/, "/Window/SplitV", tr( "Split the active window on two vertical parts" ), tr( "Split vertically" )), SplitVertical); if ( myWorkstack ) insertAction( myWorkstack->action( QtxWorkstack::SplitHorizontal ), SplitHorizontal ); else - insertAction( new QtxAction(this, false /*toggle*/, "/PRP_DESK_WINDOW_HSPLIT", + insertAction( new QtxAction(this, false /*toggle*/, "/Window/SplitH", tr( "Split the active window on two horizontal parts" ), tr( "Split horizontally" )), SplitHorizontal); connect( this, SIGNAL( triggered( int ) ), this, SLOT( onTriggered( int ) ) ); diff --git a/src/Qtx/images/shortcut_disable.svg b/src/Qtx/images/shortcut_disable.svg deleted file mode 100644 index cf661ff9f..000000000 --- a/src/Qtx/images/shortcut_disable.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/Qtx/images/shortcut_restore.svg b/src/Qtx/images/shortcut_restore.svg deleted file mode 100644 index 90dfc47d5..000000000 --- a/src/Qtx/images/shortcut_restore.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/STD/STD_Application.cxx b/src/STD/STD_Application.cxx index 6dceb529f..9a9db0144 100644 --- a/src/STD/STD_Application.cxx +++ b/src/STD/STD_Application.cxx @@ -157,12 +157,12 @@ void STD_Application::createActions() createAction( FileNewId, tr( "TOT_DESK_FILE_NEW" ), resMgr->loadPixmap( "STD", tr( "ICON_FILE_NEW" ) ), tr( "MEN_DESK_FILE_NEW" ), tr( "PRP_DESK_FILE_NEW" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onNewDoc() ), "/TOT_DESK_FILE_NEW" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onNewDoc() ), "/File/New" ); createAction( FileOpenId, tr( "TOT_DESK_FILE_OPEN" ), resMgr->loadPixmap( "STD", tr( "ICON_FILE_OPEN" ) ), tr( "MEN_DESK_FILE_OPEN" ), tr( "PRP_DESK_FILE_OPEN" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onOpenDoc() ), "/TOT_DESK_FILE_OPEN" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onOpenDoc() ), "/File/Open" ); createAction( FileReopenId, tr( "TOT_DESK_FILE_REOPEN" ), QIcon(), tr( "MEN_DESK_FILE_REOPEN" ), tr( "PRP_DESK_FILE_REOPEN" ), @@ -171,34 +171,34 @@ void STD_Application::createActions() createAction( FileCloseId, tr( "TOT_DESK_FILE_CLOSE" ), resMgr->loadPixmap( "STD", tr( "ICON_FILE_CLOSE" ) ), tr( "MEN_DESK_FILE_CLOSE" ), tr( "PRP_DESK_FILE_CLOSE" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onCloseDoc() ), "/TOT_DESK_FILE_CLOSE" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onCloseDoc() ), "/File/Close" ); //no need in this action for mono-study application as it is same as NewDoc action( FileCloseId )->setVisible( false ); createAction( FileExitId, tr( "TOT_DESK_FILE_EXIT" ), QIcon(), tr( "MEN_DESK_FILE_EXIT" ), tr( "PRP_DESK_FILE_EXIT" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onExit() ), "/TOT_DESK_FILE_EXIT" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onExit() ), "/File/Exit" ); createAction( FileSaveId, tr( "TOT_DESK_FILE_SAVE" ), resMgr->loadPixmap( "STD", tr( "ICON_FILE_SAVE" ) ), tr( "MEN_DESK_FILE_SAVE" ), tr( "PRP_DESK_FILE_SAVE" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onSaveDoc() ), "/TOT_DESK_FILE_SAVE" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onSaveDoc() ), "/File/Save" ); createAction( FileSaveAsId, tr( "TOT_DESK_FILE_SAVEAS" ), QIcon(), tr( "MEN_DESK_FILE_SAVEAS" ), tr( "PRP_DESK_FILE_SAVEAS" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onSaveAsDoc() ), "/TOT_DESK_FILE_SAVEAS"); + QKeySequence::UnknownKey, desk, false, this, SLOT( onSaveAsDoc() ), "/File/SaveAs"); createAction( EditCopyId, tr( "TOT_DESK_EDIT_COPY" ), resMgr->loadPixmap( "STD", tr( "ICON_EDIT_COPY" ) ), tr( "MEN_DESK_EDIT_COPY" ), tr( "PRP_DESK_EDIT_COPY" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onCopy() ), "/#TOT_DESK_EDIT_COPY" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onCopy() ), "/Edit/#Clipboard_Copy" ); createAction( EditPasteId, tr( "TOT_DESK_EDIT_PASTE" ), resMgr->loadPixmap( "STD", tr( "ICON_EDIT_PASTE" ) ), tr( "MEN_DESK_EDIT_PASTE" ), tr( "PRP_DESK_EDIT_PASTE" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onPaste() ), "/TOT_DESK_EDIT_PASTE" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onPaste() ), "/Edit/#Clipboard_Paste" ); - QAction* a = createAction( ViewStatusBarId, desk, true /*toggle*/, "/PRP_DESK_VIEW_STATUSBAR", + QAction* a = createAction( ViewStatusBarId, desk, true /*toggle*/, "/View/StatusBar_Toggle", tr( "TOT_DESK_VIEW_STATUSBAR" ), tr( "MEN_DESK_VIEW_STATUSBAR" ), tr( "PRP_DESK_VIEW_STATUSBAR" )); a->setChecked( desk->statusBar()->isVisibleTo( desk ) ); @@ -207,7 +207,7 @@ void STD_Application::createActions() createAction( NewWindowId, tr( "TOT_DESK_NEWWINDOW" ), QIcon(), tr( "MEN_DESK_NEWWINDOW" ), tr( "PRP_DESK_NEWWINDOW" ), 0, desk ); - createAction( HelpAboutId, desk, false /*toggle*/, "/PRP_DESK_HELP_ABOUT", + createAction( HelpAboutId, desk, false /*toggle*/, "/AboutDialog", tr( "TOT_DESK_HELP_ABOUT" ), tr( "MEN_DESK_HELP_ABOUT" ), tr( "PRP_DESK_HELP_ABOUT" ), resMgr->loadPixmap( "STD", tr( "ICON_DESK_ABOUT" ) ), this, SLOT( onHelpAbout() ) ); diff --git a/src/SUIT/CMakeLists.txt b/src/SUIT/CMakeLists.txt index 95015a57c..403e27172 100644 --- a/src/SUIT/CMakeLists.txt +++ b/src/SUIT/CMakeLists.txt @@ -55,8 +55,8 @@ SET(_moc_HEADERS SUIT_PreferenceMgr.h SUIT_SelectionMgr.h SUIT_Session.h + SUIT_ShortcutEditor.h SUIT_ShortcutMgr.h - SUIT_ShortcutTree.h SUIT_Study.h SUIT_TreeModel.h SUIT_ViewManager.h @@ -80,6 +80,8 @@ SET(_other_HEADERS SUIT_SmartPtr.h SUIT_Tools.h SUIT_TreeSync.h + Tools/SUIT_extensions.h + Tools/SUIT_SentenceMatcher.h ) # header files / to install @@ -87,6 +89,9 @@ SET(suit_HEADERS ${_moc_HEADERS} ${_other_HEADERS}) # --- resources --- +# resource files / to be processed by rcc +SET(_rcc_RESOURCES SUIT.qrc) + # resource files / to be processed by lrelease SET(_ts_RESOURCES resources/SUIT_msg_en.ts @@ -108,6 +113,9 @@ SET(_other_RESOURCES # sources / moc wrappings QT_WRAP_MOC(_moc_SOURCES ${_moc_HEADERS}) +# sources / rcc wrappings +QT_ADD_RESOURCES(_rcc_SOURCES ${_rcc_RESOURCES}) + # sources / static SET(_other_SOURCES SUIT_Accel.cxx @@ -137,18 +145,20 @@ SET(_other_SOURCES SUIT_SelectionMgr.cxx SUIT_Selector.cxx SUIT_Session.cxx + SUIT_ShortcutEditor.cxx SUIT_ShortcutMgr.cxx - SUIT_ShortcutTree.cxx SUIT_Study.cxx SUIT_Tools.cxx SUIT_TreeModel.cxx SUIT_ViewManager.cxx SUIT_ViewModel.cxx SUIT_ViewWindow.cxx + Tools/SUIT_extensions.cxx + Tools/SUIT_SentenceMatcher.cxx ) # sources / to compile -SET(suit_SOURCES ${_other_SOURCES} ${_moc_SOURCES}) +SET(suit_SOURCES ${_other_SOURCES} ${_moc_SOURCES} ${_rcc_SOURCES}) # --- rules --- diff --git a/src/SUIT/SUIT.qrc b/src/SUIT/SUIT.qrc new file mode 100644 index 000000000..eecdafa38 --- /dev/null +++ b/src/SUIT/SUIT.qrc @@ -0,0 +1,14 @@ + + +resources/default_action_icon.svg +resources/default_folder_action_icon.svg +resources/default_folder_icon.svg +resources/find.svg +resources/shortcut_disable.svg +resources/shortcut_restore.svg +resources/sort_ascending.svg +resources/sort_ascending_leading_key.svg +resources/sort_descending.svg +resources/sort_descending_leading_key.svg + + diff --git a/src/SUIT/SUIT_FindActionDialog.cxx b/src/SUIT/SUIT_FindActionDialog.cxx index 3d5c52cbd..c0eb6c1bd 100644 --- a/src/SUIT/SUIT_FindActionDialog.cxx +++ b/src/SUIT/SUIT_FindActionDialog.cxx @@ -18,6 +18,7 @@ // #include "SUIT_FindActionDialog.h" +#include "Tools/SUIT_extensions.h" #include #include @@ -42,8 +43,46 @@ SUIT_FindActionDialog::SUIT_FindActionDialog(QWidget* theParent) : QDialog(theParent) { - setMinimumWidth(500); + setMinimumWidth(800); setWindowTitle(tr("Find action")); + + QVBoxLayout* layout = new QVBoxLayout(this); + + const auto callback = std::bind(&SUIT_FindActionDialog::executeAction, this, std::placeholders::_1, std::placeholders::_2); + myFindActionWidget = new SUIT_FindActionWidget(this, std::move(callback), tr("Double click to start."), false /*theEnableItemsOfUnavailableActions*/); + layout->addWidget(myFindActionWidget); +} + +void SUIT_FindActionDialog::setActiveModuleID(const QString& theModuleID) +{ + myFindActionWidget->setIncludedModuleIDs({theModuleID}); +} + +void SUIT_FindActionDialog::executeAction(const QString& theModuleID, const QString& theInModuleActionID) +{ + bool atLeastOneActionTriggered = false; + const auto& actions = SUIT_ShortcutMgr::get()->getActions(theModuleID, theInModuleActionID); + for (const auto& action : actions) { + if (action->isEnabled()) { + action->trigger(); + atLeastOneActionTriggered = true; + } + } + + if (atLeastOneActionTriggered) + accept(); +} + + +SUIT_FindActionWidget::SUIT_FindActionWidget( + QWidget* theParent, + const std::function theCallback, + const QString& theActionItemToolTip, + bool theEnableItemsOfUnavailableActions, + bool theShowKeySequenceColumn, + const std::function(const QString&, const QString&)>& theKeySequenceGetter +) : QWidget(theParent), myCallback(theCallback) +{ QVBoxLayout* layout = new QVBoxLayout(this); myQueryLineEdit = new QLineEdit(this); @@ -56,14 +95,32 @@ SUIT_FindActionDialog::SUIT_FindActionDialog(QWidget* theParent) myIncludeUnavailableActionsCB = new QCheckBox(tr("Unavailable actions"), this); myIncludeUnavailableActionsCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); myIncludeUnavailableActionsCB->setCheckState(Qt::CheckState::Checked); - myActionSearcher.includeDisabledActions(true); + + { // Setup ActionSearcher. + if (theKeySequenceGetter) { + myActionSearcher.setKeySequenceGetter( + [theKeySequenceGetter](const QString& theModuleID, const QString& theInModuleActionID) { + return theKeySequenceGetter(theModuleID, theInModuleActionID).first; + }, + true /*doNotUpdateResults*/ + ); + } + + auto fieldsToMatch = std::set({ SUIT_ActionSearcher::MatchField::Name, SUIT_ActionSearcher::MatchField::ToolTip }); + if (theShowKeySequenceColumn) + fieldsToMatch.emplace(SUIT_ActionSearcher::MatchField::KeySequence); + + myActionSearcher.setFieldsToMatch(fieldsToMatch, true /*doNotUpdateResults*/); + myActionSearcher.includeDisabledActions(true, true /*doNotUpdateResults*/); + } + myIncludeInactiveModulesCB = new QCheckBox(tr("Inactive modules"), this); myIncludeInactiveModulesCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); myIncludeInactiveModulesCB->setCheckState(Qt::CheckState::Unchecked); searchOptionsLayout->addWidget(myIncludeUnavailableActionsCB); searchOptionsLayout->addWidget(myIncludeInactiveModulesCB); - myFoundActionsTree = new SUIT_FoundActionTree(this); + myFoundActionsTree = new SUIT_FoundActionTree(this, theActionItemToolTip, theEnableItemsOfUnavailableActions, theShowKeySequenceColumn, theKeySequenceGetter); layout->addWidget(myFoundActionsTree); connect(myQueryLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onQueryChanged(const QString&))); @@ -73,26 +130,50 @@ SUIT_FindActionDialog::SUIT_FindActionDialog(QWidget* theParent) myQueryLineEdit->installEventFilter(myFoundActionsTree); } -void SUIT_FindActionDialog::setActiveModuleID(const QString& theModuleID) +/*! \brief No need to call it, if expected search result changes are triggered by interaction with subwidgets. */ +void SUIT_FindActionWidget::updateUI() { - myActiveModuleID = theModuleID; - if(myActionSearcher.setIncludedModuleIDs(std::set({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID}))) + myFoundActionsTree->updateItems(myActionSearcher.getSearchResults()); +} + +void SUIT_FindActionWidget::setColumnWidth(int theColumnIdx, int theColumnWidth) +{ + myFoundActionsTree->setColumnWidth(theColumnIdx, theColumnWidth); +} + +void SUIT_FindActionWidget::showOptions(bool theToShow) +{ + myIncludeUnavailableActionsCB->setVisible(theToShow); + myIncludeInactiveModulesCB->setVisible(theToShow); +} + +void SUIT_FindActionWidget::setIncludedModuleIDs(const std::set& theModuleIDs, bool doNotUpdateResults) +{ + myIncludedModuleIDs = theModuleIDs; + myIncludedModuleIDs.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID); + + if (myActionSearcher.setIncludedModuleIDs(myIncludedModuleIDs, doNotUpdateResults)) updateUI(); } -void SUIT_FindActionDialog::onQueryChanged(const QString& theQuery) +const std::set& SUIT_FindActionWidget::getIncludedModuleIDs() const +{ + return myIncludedModuleIDs; +} + +void SUIT_FindActionWidget::onQueryChanged(const QString& theQuery) { if (myActionSearcher.setQuery(theQuery)) updateUI(); } -void SUIT_FindActionDialog::onSearchOptionUnavailableActionsChanged(int theState) +void SUIT_FindActionWidget::onSearchOptionUnavailableActionsChanged(int theState) { if (myActionSearcher.includeDisabledActions(theState == Qt::CheckState::Checked)) updateUI(); } -void SUIT_FindActionDialog::onSearchOptionInactiveModulesChanged(int theState) +void SUIT_FindActionWidget::onSearchOptionInactiveModulesChanged(int theState) { bool resultsChanged = false; if (theState == Qt::CheckState::Checked) { @@ -102,69 +183,72 @@ void SUIT_FindActionDialog::onSearchOptionInactiveModulesChanged(int theState) } else { myIncludeUnavailableActionsCB->setDisabled(false); - resultsChanged = myActionSearcher.setIncludedModuleIDs(std::set({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID})); + resultsChanged = myActionSearcher.setIncludedModuleIDs(myIncludedModuleIDs); } if (resultsChanged) updateUI(); } -void SUIT_FindActionDialog::updateUI() -{ - myFoundActionsTree->updateItems(myActionSearcher.getSearchResults()); -} +/*static*/ const QList> SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA = +{ + {SUIT_FoundActionTree::SortKey::MatchMetrics, Qt::SortOrder::AscendingOrder}, + {SUIT_FoundActionTree::SortKey::Name, Qt::SortOrder::AscendingOrder}, + {SUIT_FoundActionTree::SortKey::ToolTip, Qt::SortOrder::AscendingOrder}, + {SUIT_FoundActionTree::SortKey::ID, Qt::SortOrder::AscendingOrder}, + {SUIT_FoundActionTree::SortKey::KeySequence, Qt::SortOrder::AscendingOrder} +}; +/*! \brief Default key sequence getter. + \returns {keySequence, false} */ +/*static*/ std::pair SUIT_FoundActionTree::getKeySequenceFromShortcutMgr(const QString& theModuleID, const QString& theInModuleActionID) +{ + return std::pair(SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString(), false); +} -SUIT_FoundActionTree::SUIT_FoundActionTree(SUIT_FindActionDialog* theParent) -: QTreeWidget(theParent) +/*! \param theKeySequenceGetter If default or empty, SUIT_FoundActionTree::getKeySequenceFromShortcutMgr is used.*/ +SUIT_FoundActionTree::SUIT_FoundActionTree( + SUIT_FindActionWidget* theParent, + const QString& theActionItemToolTip, + bool theEnableItemsOfUnavailableActions, + bool theShowKeySequenceColumn, + const std::function(const QString&, const QString&)>& theKeySequenceGetter +) : QTreeWidget(theParent), myEnableItemsOfUnavailableActions(theEnableItemsOfUnavailableActions), +myActionItemToolTip(theActionItemToolTip), myShowKeySequenceColumn(theShowKeySequenceColumn), +myKeySequenceGetter(theKeySequenceGetter ? theKeySequenceGetter : std::bind(&SUIT_FoundActionTree::getKeySequenceFromShortcutMgr, std::placeholders::_1, std::placeholders::_2)) { - setColumnCount(2); + setColumnCount(myShowKeySequenceColumn ? 3 : 2); setSelectionMode(QAbstractItemView::SingleSelection); setSortingEnabled(false); header()->setSectionResizeMode(QHeaderView::Interactive); { QMap labelMap; - labelMap[SUIT_FoundActionTree::ElementIdx::Name] = SUIT_FindActionDialog::tr("Action"); - labelMap[SUIT_FoundActionTree::ElementIdx::ToolTip] = SUIT_FindActionDialog::tr("Description"); + labelMap[SUIT_FoundActionTree::ColumnIdx::Name] = SUIT_FindActionWidget::tr("Action"); + labelMap[SUIT_FoundActionTree::ColumnIdx::ToolTip] = SUIT_FindActionWidget::tr("Description"); + if (myShowKeySequenceColumn) + labelMap[SUIT_FoundActionTree::ColumnIdx::KeySequence] = SUIT_FindActionWidget::tr("Key sequence"); + setHeaderLabels(labelMap.values()); } setExpandsOnDoubleClick(false); // Implemented manually. setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 120); - setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 250); + setColumnWidth(SUIT_FoundActionTree::ColumnIdx::Name, 350); + setColumnWidth(SUIT_FoundActionTree::ColumnIdx::ToolTip, 500); + setColumnWidth(SUIT_FoundActionTree::ColumnIdx::KeySequence, 150); setMinimumHeight(300); setWindowFlags(windowFlags() | Qt::FramelessWindowHint); mySortKey = SUIT_FoundActionTree::SortKey::MatchMetrics; - mySortOrder = SUIT_FoundActionTree::SortOrder::Ascending; + mySortOrder = Qt::SortOrder::AscendingOrder; connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemExecuted(QTreeWidgetItem*, int))); } -/*! \brief Compensates lack of std::distance(), which is introduced in C++17. -\returns -1, if theIt does not belong to the */ -template -size_t indexOf( - const Container& theContainer, - const typename Container::iterator& theIt -) { - auto it = theContainer.begin(); - size_t distance = 0; - while (it != theContainer.end()) { - if (it == theIt) - return distance; - - it++; - distance++; - } - return -1; -} - -void SUIT_FoundActionTree::updateItems(const std::map>& theAssets) +void SUIT_FoundActionTree::updateItems(const std::map>& theActionAssetsAndSD) { std::set shownModuleIDs; // To sort module-items by their IDs. @@ -173,8 +257,8 @@ void SUIT_FoundActionTree::updateItems(const std::map(topLevelItem(moduleIdx)); myModuleItemExpansionStates[moduleItem->myModuleID] = moduleItem->isExpanded(); - const auto itUpdatedAssetsOfShownModule = theAssets.find(moduleItem->myModuleID); - if (itUpdatedAssetsOfShownModule == theAssets.end()) { + const auto itUpdatedAssetsOfShownModule = theActionAssetsAndSD.find(moduleItem->myModuleID); + if (itUpdatedAssetsOfShownModule == theActionAssetsAndSD.end()) { delete takeTopLevelItem(moduleIdx); continue; } @@ -189,24 +273,34 @@ void SUIT_FoundActionTree::updateItems(const std::map(parentWidget()); SUIT_FoundActionTreeAction* preselectedActionItem = nullptr; - for (const auto& moduleIDAndAssets : theAssets) { - const QString& moduleID = moduleIDAndAssets.first; - const auto& moduleAssets = moduleIDAndAssets.second; - if (moduleAssets.empty()) + for (const auto& moduleIDAndActionAssetsAndSD : theActionAssetsAndSD) { + const QString& moduleID = moduleIDAndActionAssetsAndSD.first; + const auto& moduleActionAssetsAndSD = moduleIDAndActionAssetsAndSD.second; + if (moduleActionAssetsAndSD.empty()) continue; const auto moduleItemAndIdx = findModuleItem(moduleID); SUIT_FoundActionTreeModule* moduleItem = moduleItemAndIdx.first; if (!moduleItem) { moduleItem = new SUIT_FoundActionTreeModule(moduleID); - moduleItem->setAssetsAndSearchData(SUIT_ActionSearcher::AssetsAndSearchData(shortcutMgr->getModuleAssets(moduleID)), lang); + + const auto moduleAssets = shortcutMgr->getModuleAssets(moduleID); + if (moduleAssets) + moduleItem->setAssets(*moduleAssets, lang); + else + ShCutDbg("SUIT_FoundActionTree can't retrieve module assets of \"" + moduleID + "\" from ShortcutMgr."); + + const auto& includedModuleIDs = findActionWidget->getIncludedModuleIDs(); + if (includedModuleIDs.find(moduleID) == includedModuleIDs.end()) + moduleItem->styleAsDimmed(); const auto emplaceRes = shownModuleIDs.emplace(moduleID); - insertTopLevelItem(indexOf(shownModuleIDs, emplaceRes.first), moduleItem); + insertTopLevelItem(::SUIT_tools::distanceFromBegin(shownModuleIDs, emplaceRes.first), moduleItem); moduleItem->setFlags(Qt::ItemIsEnabled); @@ -225,9 +319,9 @@ void SUIT_FoundActionTree::updateItems(const std::mapsetAssetsAndSearchData(assetsAndSearchData, lang); + if (myShowKeySequenceColumn) { + const auto KSAndToStyle = myKeySequenceGetter(moduleID, inModuleActionID); + actionItem->setKeySequence(KSAndToStyle.first); + actionItem->styleAsKeySequenceModified(KSAndToStyle.second); + } + sortedActionItems.emplace(actionItem); } SUIT_FoundActionTreeAction* preselectedActionItemCand = nullptr; for (const auto actionItem : sortedActionItems) { moduleItem->addChild(actionItem); + actionItem->setToolTip(myActionItemToolTip); // Consider first ranked available action in the module (if user did not collapsed it) as a candidate for preselected action. if (!preselectedActionItemCand && moduleItem->isExpanded() && actionItem->isEnabledBufferedValue()) @@ -262,7 +363,7 @@ void SUIT_FoundActionTree::updateItems(const std::mapkey(); const auto selectedItem = currentItem(); if ((key == Qt::Key_Enter || key == Qt::Key_Return) && selectedItem) - onItemExecuted(selectedItem, SUIT_FoundActionTree::ElementIdx::Name); + onItemExecuted(selectedItem, SUIT_FoundActionTree::ColumnIdx::Name); else QTreeWidget::keyPressEvent(theEvent); } @@ -330,7 +431,7 @@ bool approximatelyEqual(Float a, Float b, Float relativeTol = std::numeric_limit std::set> SUIT_FoundActionTree::createActionSetWithComparator() const { - QList> sortSchema = SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA; + QList> sortSchema = SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA; { for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) { if (itSameKey->first == mySortKey) { @@ -338,7 +439,7 @@ std::set(mySortKey, mySortOrder)); + sortSchema.push_front(std::pair(mySortKey, mySortOrder)); } const std::function comparator = @@ -355,13 +456,13 @@ std::set 0; + return keyAndOrder.second == Qt::SortOrder::AscendingOrder ? res < 0 : res > 0; } } else { const int res = collator.compare(fieldOfA.toString(), fieldOfB.toString()); if (res != 0) - return keyAndOrder.second == SUIT_FoundActionTree::SortOrder::Ascending ? res < 0 : res > 0; + return keyAndOrder.second == Qt::SortOrder::AscendingOrder ? res < 0 : res > 0; } } return false; @@ -372,70 +473,54 @@ std::set(parentWidget()); SUIT_FoundActionTreeItem* const item = static_cast(theItem); + if (item->type() == SUIT_FoundActionTreeItem::Type::Action) { SUIT_FoundActionTreeAction* const actionItem = static_cast(theItem); - if (actionItem->trigger()) - static_cast(parentWidget())->accept(); + parent->myCallback(actionItem->myModuleID, actionItem->myInModuleActionID); } else /* if (item->type() == SUIT_FoundActionTreeItem::Type::Module) */ { item->setExpanded(!item->isExpanded()); } } -/*static*/ const QList> SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA = -{ - {SUIT_FoundActionTree::SortKey::MatchMetrics, SUIT_FoundActionTree::SortOrder::Ascending}, - {SUIT_FoundActionTree::SortKey::Name, SUIT_FoundActionTree::SortOrder::Ascending}, - {SUIT_FoundActionTree::SortKey::ToolTip, SUIT_FoundActionTree::SortOrder::Ascending}, - {SUIT_FoundActionTree::SortKey::ID, SUIT_FoundActionTree::SortOrder::Ascending} -}; - +/*! \param theTree must not be nullptr. */ SUIT_FoundActionTreeItem::SUIT_FoundActionTreeItem(const QString& theModuleID) : QTreeWidgetItem(), myModuleID(theModuleID) { } QString SUIT_FoundActionTreeItem::name() const { - return text(SUIT_FoundActionTree::ElementIdx::Name); + return text(SUIT_FoundActionTree::ColumnIdx::Name); } QString SUIT_FoundActionTreeItem::toolTip() const { - return text(SUIT_FoundActionTree::ElementIdx::ToolTip); + return text(SUIT_FoundActionTree::ColumnIdx::ToolTip); +} + +void SUIT_FoundActionTreeItem::styleAsDimmed() { + static const QBrush greyedOutBrush = QBrush(Qt::gray); + setForeground(SUIT_FoundActionTree::ColumnIdx::Name, greyedOutBrush); + setForeground(SUIT_FoundActionTree::ColumnIdx::ToolTip, greyedOutBrush); } SUIT_FoundActionTreeModule::SUIT_FoundActionTreeModule(const QString& theModuleID) : SUIT_FoundActionTreeItem(theModuleID) { - QFont f = font(SUIT_FoundActionTree::ElementIdx::Name); + QFont f = font(SUIT_FoundActionTree::ColumnIdx::Name); f.setBold(true); - setFont(SUIT_FoundActionTree::ElementIdx::Name, f); - setText(SUIT_FoundActionTree::ElementIdx::Name, theModuleID); + setFont(SUIT_FoundActionTree::ColumnIdx::Name, f); + setText(SUIT_FoundActionTree::ColumnIdx::Name, theModuleID); } -void SUIT_FoundActionTreeModule::setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang) +void SUIT_FoundActionTreeModule::setAssets(const SUIT_ShortcutModuleAssets& theAssets, const QString& theLang) { - if (!theAssetsAndSD.myAssets) - return; - - setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon); - - const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(SUIT_FoundActionTree::ElementIdx::Name, myModuleID); - return; - } - - auto itLDA = ldaMap.find(theLang); - if (itLDA == ldaMap.end()) - itLDA = ldaMap.begin(); - - const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second; - const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName; - setText(SUIT_FoundActionTree::ElementIdx::Name, name); + setText(SUIT_FoundActionTree::ColumnIdx::Name, theAssets.bestName(theLang)); + setIcon(SUIT_FoundActionTree::ColumnIdx::Name, theAssets.myIcon); } QVariant SUIT_FoundActionTreeModule::getValue(SUIT_FoundActionTree::SortKey theKey) const @@ -454,17 +539,12 @@ QVariant SUIT_FoundActionTreeModule::getValue(SUIT_FoundActionTree::SortKey theK } } -bool SUIT_FoundActionTreeModule::isEnabled() const -{ - return true; -} - SUIT_FoundActionTreeAction::SUIT_FoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID) : SUIT_FoundActionTreeItem(theModuleID), myInModuleActionID(theInModuleActionID), myMatchMetrics(std::numeric_limits::infinity()), myIsEnabledBufferedValue(false) { - setText(SUIT_FoundActionTree::ElementIdx::Name, theInModuleActionID); + setText(SUIT_FoundActionTree::ColumnIdx::Name, theInModuleActionID); } /*static*/ SUIT_FoundActionTreeAction* SUIT_FoundActionTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID) @@ -482,42 +562,47 @@ void SUIT_FoundActionTreeAction::setAssetsAndSearchData(const SUIT_ActionSearche if (!theAssetsAndSD.myAssets) return; - setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon); - - const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(SUIT_FoundActionTree::ElementIdx::Name, myInModuleActionID); - return; - } - - auto itLDA = ldaMap.find(theLang); - if (itLDA == ldaMap.end()) - itLDA = ldaMap.begin(); - - const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second; - const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName; - setText(SUIT_FoundActionTree::ElementIdx::Name, name); - - setText(SUIT_FoundActionTree::ElementIdx::ToolTip, lda.myToolTip); + setText(SUIT_FoundActionTree::ColumnIdx::Name, theAssetsAndSD.myAssets->bestPath(theLang)); + setIcon(SUIT_FoundActionTree::ColumnIdx::Name, theAssetsAndSD.myAssets->icon()); + setText(SUIT_FoundActionTree::ColumnIdx::ToolTip, theAssetsAndSD.myAssets->bestToolTip(theLang)); + myMatchMetrics = theAssetsAndSD.matchMetrics(); +} +void SUIT_FoundActionTreeAction::setToolTip(const QString& theToolTip) +{ if (isEnabled()) { - setToolTip( - SUIT_FoundActionTree::ElementIdx::Name, - SUIT_FoundActionTree::tr("Double click to start") + QTreeWidgetItem::setToolTip( + SUIT_FoundActionTree::ColumnIdx::Name, + theToolTip ); - setToolTip( - SUIT_FoundActionTree::ElementIdx::ToolTip, - SUIT_FoundActionTree::tr("Double click to start") + QTreeWidgetItem::setToolTip( + SUIT_FoundActionTree::ColumnIdx::ToolTip, + theToolTip ); } - else { - static const QBrush greyedOutBrush = QBrush(Qt::gray); - setForeground(SUIT_FoundActionTree::ElementIdx::Name, greyedOutBrush); - setForeground(SUIT_FoundActionTree::ElementIdx::ToolTip, greyedOutBrush); - } + else + styleAsDimmed(); +} - myMatchMetrics = theAssetsAndSD.matchMetrics(); +void SUIT_FoundActionTreeAction::setKeySequence(const QString& theKeySequence) +{ + setText(SUIT_FoundActionTree::ColumnIdx::KeySequence, theKeySequence); +} + +QString SUIT_FoundActionTreeAction::keySequence() const +{ + return text(SUIT_FoundActionTree::ColumnIdx::KeySequence); +} + +void SUIT_FoundActionTreeAction::styleAsKeySequenceModified(bool theIsModified) +{ + static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen); + static const QBrush fgHighlitingBrush = QBrush(Qt::white); + static const QBrush noBrush = QBrush(); + + setBackground(SUIT_FoundActionTree::ColumnIdx::KeySequence, theIsModified ? bgHighlitingBrush : noBrush); + setForeground(SUIT_FoundActionTree::ColumnIdx::KeySequence, theIsModified ? fgHighlitingBrush : noBrush); } QVariant SUIT_FoundActionTreeAction::getValue(SUIT_FoundActionTree::SortKey theKey) const @@ -531,6 +616,8 @@ QVariant SUIT_FoundActionTreeAction::getValue(SUIT_FoundActionTree::SortKey theK return name(); case SUIT_FoundActionTree::SortKey::ToolTip: return toolTip(); + case SUIT_FoundActionTree::SortKey::KeySequence: + return keySequence(); default: return QString(); } @@ -538,20 +625,13 @@ QVariant SUIT_FoundActionTreeAction::getValue(SUIT_FoundActionTree::SortKey theK bool SUIT_FoundActionTreeAction::isEnabled() const { + const auto tree = static_cast(treeWidget()); + if (tree->myEnableItemsOfUnavailableActions) { + myIsEnabledBufferedValue = true; + return true; + } + const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID); myIsEnabledBufferedValue = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); }) != actions.end(); return myIsEnabledBufferedValue; -} - -bool SUIT_FoundActionTreeAction::trigger() const -{ - bool res = false; - const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID); - for (const auto& action : actions) { - if (action->isEnabled()) { - action->trigger(); - res = true; - } - } - return res; } \ No newline at end of file diff --git a/src/SUIT/SUIT_FindActionDialog.h b/src/SUIT/SUIT_FindActionDialog.h index 34ffbcb0e..a6fc81826 100644 --- a/src/SUIT/SUIT_FindActionDialog.h +++ b/src/SUIT/SUIT_FindActionDialog.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ class QLineEdit; class QLabel; class QPushButton; class QKeyEvent; +class SUIT_FindActionWidget; class SUIT_FoundActionTree; @@ -57,21 +59,58 @@ public: void setActiveModuleID(const QString& theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID); +private: + void executeAction(const QString& theModuleID, const QString& theInModuleActionID); + +private: + SUIT_FindActionWidget* myFindActionWidget; +}; + + +class SUIT_EXPORT SUIT_FindActionWidget : public QWidget +{ + Q_OBJECT + +public: + SUIT_FindActionWidget( + QWidget* theParent, + const std::function theCallback, + const QString& theActionItemToolTip, + bool theEnableItemsOfUnavailableActions = false, + bool theShowKeySequenceColumn = false, + const std::function(const QString&, const QString&)>& theKeySequenceGetter = std::function(const QString&, const QString&)>() + ); + SUIT_FindActionWidget(const SUIT_FindActionWidget&) = delete; + SUIT_FindActionWidget& operator=(const SUIT_FindActionWidget&) = delete; + virtual ~SUIT_FindActionWidget() = default; + + void showOptions(bool theToShow); + + /*! \param doNotUpdateResults Set to true to initialize the instance without unnecessary computations. */ + void setIncludedModuleIDs(const std::set& theModuleIDs = {}, bool doNotUpdateResults = false); + const std::set& getIncludedModuleIDs() const; + + void updateUI(); + + void setColumnWidth(int theColumnIdx, int theColumnWidth); + private slots: void onQueryChanged(const QString& theKeyword); void onSearchOptionUnavailableActionsChanged(int); void onSearchOptionInactiveModulesChanged(int); private: - void updateUI(); - QLineEdit* myQueryLineEdit; QCheckBox* myIncludeUnavailableActionsCB; QCheckBox* myIncludeInactiveModulesCB; SUIT_FoundActionTree* myFoundActionsTree; - QString myActiveModuleID; + std::set myIncludedModuleIDs; SUIT_ActionSearcher myActionSearcher; + +public: + /** void callback(theModuleID, theInModuleActionID) */ + const std::function myCallback; }; @@ -85,31 +124,37 @@ class SUIT_EXPORT SUIT_FoundActionTree : public QTreeWidget Q_OBJECT public: - enum ElementIdx { - Name = 0, - ToolTip = 1 + enum ColumnIdx { + Name, + ToolTip, + KeySequence }; enum class SortKey { MatchMetrics, ID, Name, - ToolTip + ToolTip, + KeySequence }; - enum class SortOrder { - Ascending, - Descending - }; + static const QList> DEFAULT_SORT_SCHEMA; + static std::pair getKeySequenceFromShortcutMgr(const QString& theModuleID, const QString& theInModuleActionID); - SUIT_FoundActionTree(SUIT_FindActionDialog* theParent); + SUIT_FoundActionTree( + SUIT_FindActionWidget* theParent, + const QString& theActionItemToolTip, + bool theEnableItemsOfUnavailableActions = false, + bool theShowKeySequenceColumn = false, + const std::function(const QString&, const QString&)>& theKeySequenceGetter = std::function(const QString&, const QString&)>() + ); SUIT_FoundActionTree(const SUIT_FoundActionTree&) = delete; SUIT_FoundActionTree& operator=(const SUIT_FoundActionTree&) = delete; virtual ~SUIT_FoundActionTree() = default; - void updateItems(const std::map>& theAssets); + void updateItems(const std::map>& theActionAssetsAndSD); - void sort(SUIT_FoundActionTree::SortKey theKey, SUIT_FoundActionTree::SortOrder theOrder); + void sort(SUIT_FoundActionTree::SortKey theKey, Qt::SortOrder theOrder); void keyPressEvent(QKeyEvent* theEvent); @@ -123,15 +168,20 @@ private: private slots: void onItemExecuted(QTreeWidgetItem* theWidgetItem, int theColIdx); -public: - static const QList> DEFAULT_SORT_SCHEMA; - private: SUIT_FoundActionTree::SortKey mySortKey; - SUIT_FoundActionTree::SortOrder mySortOrder; + Qt::SortOrder mySortOrder; /** {moduleID, isExpanded}[] */ std::map myModuleItemExpansionStates; + +public: + const QString myActionItemToolTip; + const bool myEnableItemsOfUnavailableActions; // If false, items of unavailable actions/modules are dimmed. + const bool myShowKeySequenceColumn; + + /** std::pair getKeySequence(theModuleID, theInModuleActionID). Returns true, if KeySequence column of an action item must be styled as modified. */ + const std::function(const QString&, const QString&)> myKeySequenceGetter; }; @@ -139,8 +189,8 @@ class SUIT_FoundActionTreeItem : public QTreeWidgetItem { public: enum Type { - Module = 0, - Action = 1, + Module, + Action }; protected: @@ -150,13 +200,12 @@ public: virtual ~SUIT_FoundActionTreeItem() = default; virtual SUIT_FoundActionTreeItem::Type type() const = 0; - virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang) = 0; QString name() const; QString toolTip() const; virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const = 0; - virtual bool isEnabled() const = 0; + virtual void styleAsDimmed(); public: const QString myModuleID; @@ -170,12 +219,9 @@ public: virtual ~SUIT_FoundActionTreeModule() = default; virtual SUIT_FoundActionTreeItem::Type type() const { return SUIT_FoundActionTreeItem::Type::Module; }; - /*! \brief Search data is unused. */ - virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang); + void setAssets(const SUIT_ShortcutModuleAssets& theAssets, const QString& theLang); virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const; - - virtual bool isEnabled() const; }; @@ -189,7 +235,11 @@ public: virtual ~SUIT_FoundActionTreeAction() = default; virtual SUIT_FoundActionTreeItem::Type type() const { return SUIT_FoundActionTreeItem::Type::Action; }; - virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang); + void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang); + void setToolTip(const QString& theToolTip); + void setKeySequence(const QString& theKeySequence); + QString keySequence() const; + void styleAsKeySequenceModified(bool theIsModified); virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const; double matchMetrics() const { return myMatchMetrics; }; @@ -197,8 +247,6 @@ public: virtual bool isEnabled() const; bool isEnabledBufferedValue() const { return myIsEnabledBufferedValue; }; - bool trigger() const; - const QString myInModuleActionID; private: diff --git a/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx b/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx index d05d51167..4451cb7b1 100644 --- a/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx +++ b/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx @@ -1,12 +1,12 @@ #include "SUIT_PagePrefShortcutTreeItem.h" -#include "SUIT_ShortcutTree.h" +#include "SUIT_ShortcutEditor.h" #include "SUIT_ShortcutMgr.h" /*! \brief Creates preference item for editing of key bindings - \param theParent parent preference item. Must not be nullptr. + \param theParent Must not be nullptr. */ SUIT_PagePrefShortcutTreeItem::SUIT_PagePrefShortcutTreeItem(QtxPreferenceItem* theParent) : QtxPagePrefItem(QString(), theParent) @@ -25,9 +25,8 @@ SUIT_PagePrefShortcutTreeItem::SUIT_PagePrefShortcutTreeItem(QtxPreferenceItem* } } - SUIT_ShortcutTree* tree = new SUIT_ShortcutTree(container); - tree->myModuleIDs = SUIT_ShortcutMgr::get()->getShortcutModuleIDs(); - setWidget(tree); + SUIT_ShortcutTabWidget* tabWidget = new SUIT_ShortcutTabWidget(SUIT_ShortcutMgr::get()->getShortcutModuleIDs(), container); + setWidget(tabWidget); } /*! @@ -37,7 +36,7 @@ SUIT_PagePrefShortcutTreeItem::SUIT_PagePrefShortcutTreeItem(QtxPreferenceItem* */ void SUIT_PagePrefShortcutTreeItem::retrieve() { - static_cast(widget())->setShortcutsFromManager(); + static_cast(widget())->setShortcutsFromManager(); } /*! @@ -47,7 +46,7 @@ void SUIT_PagePrefShortcutTreeItem::retrieve() */ void SUIT_PagePrefShortcutTreeItem::retrieveDefault() { - static_cast(widget())->setDefaultShortcuts(); + static_cast(widget())->setDefaultShortcuts(); } /*! @@ -58,7 +57,7 @@ void SUIT_PagePrefShortcutTreeItem::retrieveDefault() */ void SUIT_PagePrefShortcutTreeItem::store() { - static_cast(widget())->applyChangesToShortcutMgr(); + static_cast(widget())->applyChangesToShortcutMgr(); } /*static*/ std::map> SUIT_PagePrefShortcutTreeItem::shortcutContainers = diff --git a/src/SUIT/SUIT_PagePrefShortcutTreeItem.h b/src/SUIT/SUIT_PagePrefShortcutTreeItem.h index 653a1e08f..311ee1ad4 100644 --- a/src/SUIT/SUIT_PagePrefShortcutTreeItem.h +++ b/src/SUIT/SUIT_PagePrefShortcutTreeItem.h @@ -45,19 +45,17 @@ public: virtual void store(); private: - SUIT_ShortcutTree* myShortcutTree; - - // { root item (preference window), shortcut container of synchronized trees (widgets within the same window) } + // { root item (preference window), shortcut container of synchronized SUIT_ShortcutTabWidgets (widgets within the same window) } static std::map> shortcutContainers; /** Why is this? * Every QtxPagePrefMgr is eventually a preference window. Each preference window has button "Apply". * When the button is pressed, all descendants of the QtxPagePrefMgr store changes they carry into preferences. - * The pitfall with shortcut trees is as follows: made in independent shortcut trees, changes may conflict, - * and merge of such changes is ambiguous. And the solution is to keep shortcut trees within the same window - * synchronized - all changes being made in a tree of a synchronized bundle are projected to other trees from the bundle + * The pitfall with SUIT_ShortcutTabWidgets is as follows: made in independent instance of SUIT_ShortcutTabWidget, changes may conflict, + * and merge of such changes is ambiguous. And the solution is to keep SUIT_ShortcutTabWidgets within the same window + * synchronized - all changes being made in a SUIT_ShortcutTabWidget of a synchronized bundle are projected to other SUIT_ShortcutTabWidgets from the bundle * without interacting with SUIT_ShortcutMgr. * - * Every time shortcut preferences stored to the ShortcutMgr, all instances of SUIT_ShortcutTree are updated. + * Every time shortcut preferences stored to the ShortcutMgr, all instances of SUIT_ShortcutTabWidget are updated. */ }; diff --git a/src/SUIT/SUIT_ShortcutEditor.cxx b/src/SUIT/SUIT_ShortcutEditor.cxx new file mode 100644 index 000000000..d509caf14 --- /dev/null +++ b/src/SUIT/SUIT_ShortcutEditor.cxx @@ -0,0 +1,1195 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "SUIT_ShortcutEditor.h" +#include "Tools/SUIT_extensions.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + + +SUIT_KeySequenceEdit::SUIT_KeySequenceEdit(QWidget* parent) +: QFrame(parent) +{ + initialize(); + myKeySequenceLineEdit->installEventFilter(this); +} + +void SUIT_KeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence) +{ + myConfirmedKeySequenceString = theKeySequence.toString(); + myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); + myPrevKeySequenceString = myConfirmedKeySequenceString; +} + +void SUIT_KeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence) +{ + const QString keySequenceString = theKeySequence.toString(); + myKeySequenceLineEdit->setText(keySequenceString); + myPrevKeySequenceString = keySequenceString; +} + +QKeySequence SUIT_KeySequenceEdit::editedKeySequence() const +{ + return QKeySequence::fromString(myKeySequenceLineEdit->text()); +} + +bool SUIT_KeySequenceEdit::isKeySequenceModified() const +{ + return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence(); +} + +void SUIT_KeySequenceEdit::restoreKeySequence() +{ + myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); + myPrevKeySequenceString = myConfirmedKeySequenceString; +} + +/*static*/ QString SUIT_KeySequenceEdit::parseEvent(QKeyEvent* e) +{ + bool isShiftPressed = e->modifiers() & Qt::ShiftModifier; + bool isControlPressed = e->modifiers() & Qt::ControlModifier; + bool isAltPressed = e->modifiers() & Qt::AltModifier; + bool isMetaPressed = e->modifiers() & Qt::MetaModifier; + bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier! + int result=0; + if(isControlPressed) + result += Qt::CTRL; + if(isAltPressed) + result += Qt::ALT; + if(isShiftPressed) + result += Qt::SHIFT; + if(isMetaPressed) + result += Qt::META; + + int aKey = e->key(); + if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12)) || aKey == Qt::Key_Delete) + result += aKey; + + return QKeySequence(result).toString(); +} + +/*static*/ bool SUIT_KeySequenceEdit::isValidKey(int theKey) +{ + if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape || + ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) || + ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) || + ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 ) || + ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) || + ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_AsciiTilde ) ) + return true; + return false; +} + +void SUIT_KeySequenceEdit::onClear() +{ + myKeySequenceLineEdit->setText(""); + myPrevKeySequenceString = ""; + emit editingFinished(); +} + +void SUIT_KeySequenceEdit::onEditingFinished() +{ + if (myKeySequenceLineEdit->text().endsWith("+")) + myKeySequenceLineEdit->setText(myPrevKeySequenceString); + else + myPrevKeySequenceString = myKeySequenceLineEdit->text(); + emit editingFinished(); +} + +bool SUIT_KeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent) +{ + if (theObject == myKeySequenceLineEdit) { + if (theEvent->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(theEvent); + QString text = parseEvent(keyEvent); + if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace) + myKeySequenceLineEdit->setText(""); + if (!text.isEmpty()) + myKeySequenceLineEdit->setText(text); + + emit editingStarted(); + return true; + } + if (theEvent->type() == QEvent::KeyRelease) { + onEditingFinished(); + return true; + } + } + return false; +} + +void SUIT_KeySequenceEdit::initialize() +{ + QHBoxLayout* base = new QHBoxLayout( this ); + base->setMargin(0); + base->setSpacing(5); + + base->addWidget(myKeySequenceLineEdit = new QLineEdit(this)); + setFocusProxy(myKeySequenceLineEdit); + + QToolButton* clearBtn = new QToolButton(); + clearBtn->setIcon(QIcon(":/resources/shortcut_disable.svg")); + clearBtn->setToolTip(tr("Disable shortcut.")); + base->addWidget(clearBtn); + + QToolButton* restoreBtn = new QToolButton(); + restoreBtn->setIcon(QIcon(":/resources/shortcut_restore.svg")); + restoreBtn->setToolTip(tr("Restore the currently applied key sequence.")); + base->addWidget(restoreBtn); + + myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear())); + connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked())); + connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); +} + + +SUIT_EditKeySequenceDialog::SUIT_EditKeySequenceDialog(SUIT_ShortcutTabWidget* theParent) +: QDialog(theParent) +{ + setMinimumWidth(500); + setWindowTitle(tr("Change key sequence")); + QVBoxLayout* layout = new QVBoxLayout(this); + myActionName = new QLabel(this); + myActionName->setTextFormat(Qt::RichText); + myKeySequenceEdit = new SUIT_KeySequenceEdit(this); + myTextEdit = new QTextEdit(this); + layout->addWidget(myActionName); + layout->addWidget(myKeySequenceEdit); + layout->addWidget(myTextEdit); + myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + myTextEdit->setReadOnly(true); + myTextEdit->setAcceptRichText(true); + myTextEdit->setPlaceholderText(tr("No conflicts.")); + setFocusProxy(myKeySequenceEdit); + + QHBoxLayout* buttonLayout = new QHBoxLayout(this); + layout->addLayout(buttonLayout); + QPushButton* confirmButton = new QPushButton(tr("Confirm"), this); + confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QPushButton* cancelButton = new QPushButton(tr("Cancel"), this); + cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + buttonLayout->addStretch(); + buttonLayout->addWidget(confirmButton); + buttonLayout->addWidget(cancelButton); + + connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted())); + connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); + connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr())); + connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); +} + +void SUIT_EditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID) +{ + myModuleID = theModuleID; + myInModuleActionID = theInModuleActionID; +} + +const QString& SUIT_EditKeySequenceDialog::moduleID() const { return myModuleID; } +const QString& SUIT_EditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; } + +void SUIT_EditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip) +{ + myActionName->setText("" + theModuleName + "  " + theActionName); + myActionName->setToolTip(theActionToolTip); +} + +void SUIT_EditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence) +{ + myKeySequenceEdit->setConfirmedKeySequence(theSequence); +} + +QKeySequence SUIT_EditKeySequenceDialog::editedKeySequence() const +{ + return myKeySequenceEdit->editedKeySequence(); +} + +int SUIT_EditKeySequenceDialog::exec() +{ + myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason); + return QDialog::exec(); +} + +void SUIT_EditKeySequenceDialog::onEditingStarted() +{ + myTextEdit->setEnabled(false); +} + +void SUIT_EditKeySequenceDialog::onEditingFinished() +{ + updateConflictsMessage(); +} + +void SUIT_EditKeySequenceDialog::onRestoreFromShortcutMgr() +{ + const auto mgr = SUIT_ShortcutMgr::get(); + myKeySequenceEdit->setEditedKeySequence(mgr->getKeySequence(myModuleID, myInModuleActionID)); + updateConflictsMessage(); +} + +void SUIT_EditKeySequenceDialog::updateConflictsMessage() +{ + myTextEdit->setEnabled(true); + QTextDocument* doc = myTextEdit->document(); + if (!doc) { + doc = new QTextDocument(myTextEdit); + myTextEdit->setDocument(doc); + } + + if (!myKeySequenceEdit->isKeySequenceModified()) { + doc->clear(); + return; + } + + const QKeySequence newKeySequence = editedKeySequence(); + + const auto tabWidget = static_cast(parentWidget()); + /** {moduleID, inModuleActionID}[] */ + std::set> conflicts = tabWidget->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence); + if (!conflicts.empty()) { + const auto mgr = SUIT_ShortcutMgr::get(); + + QString report = "" + tr("These shortcuts will be disabled on confirm:") + ""; + { + report += "
    "; + for (const auto& conflict : conflicts) { + const QString conflictingModuleName = mgr->getModuleName(conflict.first); + const auto actionAssets = SUIT_ShortcutMgr::get()->getActionAssets(conflict.first, conflict.second); +#ifdef SHORTCUT_MGR_DBG + if (!actionAssets) + ShCutDbg("SUIT_EditKeySequenceDialog::updateConflictsMessage(): action assets of \"" + conflict.first + "/" + conflict.second + "\" are absent in SUIT_ShortcutMgr."); +#endif + const QString conflictingActionName = actionAssets ? actionAssets->bestPath() : conflict.second; + report += "
  • " + conflictingModuleName + "  " + conflictingActionName + "
  • "; + } + report += "
"; + } + doc->setHtml(report); + } + else /* if no conflicts */ { + doc->clear(); + } +} + +void SUIT_EditKeySequenceDialog::onConfirm() +{ + if (myKeySequenceEdit->isKeySequenceModified()) + accept(); + else + reject(); +} + + +/*static*/ std::map> SUIT_ShortcutTabWidget::instances = +std::map>(); + +SUIT_ShortcutTabWidget::SUIT_ShortcutTabWidget( + const std::set& theIDsOfModulesToShow, + std::shared_ptr theContainer, + QWidget* theParent +) : QTabWidget(theParent), +myModuleIDs(theIDsOfModulesToShow.empty() ? std::set({SUIT_ShortcutMgr::ROOT_MODULE_ID}) : theIDsOfModulesToShow), +myShortcutContainer(theContainer ? theContainer : std::shared_ptr(new SUIT_ShortcutContainer())), +myIsPopulated(false) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + myEditDialog = new SUIT_EditKeySequenceDialog(this); + myFindActionWidget = new SUIT_FindActionWidget( + this, + std::bind(&SUIT_ShortcutTabWidget::jumpToTreeItem, this, std::placeholders::_1, std::placeholders::_2) /*call back on item double clicked*/, + tr("Double click to edit key sequence."), + true /*theEnableItemsOfUnavailableActions*/, + true /*theShowKeySequenceColumn*/, + std::bind(&SUIT_ShortcutTabWidget::getConfirmedKeySequence, this, std::placeholders::_1, std::placeholders::_2) /*theKeySequenceGetter*/ + ); + myFindActionWidget->showOptions(false); + myFindActionWidget->setIncludedModuleIDs(myModuleIDs, true /*doNotUpdateResults*/); + + const auto tabBar = new SUIT_ShortcutTabWidgetBar(this); + setTabBar(tabBar); + + insertTab(0, myFindActionWidget, QIcon(":/resources/find.svg"), ""/*Tab name*/); + setTabToolTip(0, tr("Find shortcut")); + + SUIT_ShortcutTabWidget::instances[myShortcutContainer.get()].emplace(this); +} + +SUIT_ShortcutTabWidget::~SUIT_ShortcutTabWidget() +{ + SUIT_ShortcutTabWidget::instances[myShortcutContainer.get()].erase(this); + if (SUIT_ShortcutTabWidget::instances[myShortcutContainer.get()].empty()) + SUIT_ShortcutTabWidget::instances.erase(myShortcutContainer.get()); +} + +void SUIT_ShortcutTabWidget::setShortcutsFromManager() +{ + *myShortcutContainer = SUIT_ShortcutMgr::get()->getShortcutContainer(); + // nb! ShortcutMgr never removes shortcuts from its container, only disables. + + updateTabs(false /*theHighlightModifiedTreeItems*/, true /*theUpdateSyncTabs*/); +} + +void SUIT_ShortcutTabWidget::setDefaultShortcuts() +{ + SUIT_ShortcutContainer defaultShortcuts; + SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/); + + myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/); + // nb! SUIT_ShortcutContainer never erases shortcuts, only disables. + + updateTabs(true /*theHighlightModifiedTreeItems*/, true /*theUpdateSyncTabs*/); +} + +void SUIT_ShortcutTabWidget::applyChangesToShortcutMgr() +{ + const auto mgr = SUIT_ShortcutMgr::get(); + mgr->mergeShortcutContainer(*myShortcutContainer); + + // Update non-synchronized with this instances. + for (const auto& containerAndSyncTabWidgets : SUIT_ShortcutTabWidget::instances) { + if (containerAndSyncTabWidgets.first == myShortcutContainer.get()) + continue; // Synchronized instances are updated in SUIT_ShortcutTabWidget::updateTabs. + + const std::set& syncTabWidgets = containerAndSyncTabWidgets.second; + const auto itFirstSyncTabWidget = syncTabWidgets.begin(); + if (itFirstSyncTabWidget == syncTabWidgets.end()) + continue; + + // No need to update each synchronized instance with the same SUIT_ShortcutContainer. + // Update just one of them, SUIT_ShortcutTabWidget::setShortcutsFromManager does rest. + (*itFirstSyncTabWidget)->setShortcutsFromManager(); + const auto editDialog = (*itFirstSyncTabWidget)->myEditDialog; + editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); + editDialog->updateConflictsMessage(); + } +} + +std::shared_ptr SUIT_ShortcutTabWidget::shortcutContainer() const +{ + return myShortcutContainer; +} + +void SUIT_ShortcutTabWidget::onTreeItemDoubleClicked(SUIT_ShortcutTree* theTree, SUIT_ShortcutTreeItem* theItem) +{ + const auto mgr = SUIT_ShortcutMgr::get(); + + if (!theItem->isAction()) + return; + + myEditDialog->setModuleAndActionID(theTree->moduleID(), theItem->inModuleID()); + myEditDialog->setModuleAndActionName(theTree->name(), theItem->path(), theItem->toolTip()); + myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(theItem->keySequence())); + myEditDialog->updateConflictsMessage(); + const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted; + + if (!somethingChanged) + return; + + const QKeySequence newKeySequence = myEditDialog->editedKeySequence(); + + /** { moduleID, inModuleID }[] */ + std::set> disabledActionIDs = myShortcutContainer->setShortcut(theTree->moduleID(), theItem->inModuleID(), newKeySequence, true /*override*/); + + /** { moduleID, {inModuleID, keySequence}[] }[] */ + std::map> changes; + changes[theTree->moduleID()][theItem->inModuleID()] = newKeySequence.toString(); + for (const auto moduleAndActionID : disabledActionIDs) { + changes[moduleAndActionID.first][moduleAndActionID.second] = QString(); + } + + std::map moduleModifiedStatuses; + + // Set new key sequences to shortcut items. + for (const auto& moduleIDAndChanges : changes) { + const QString& moduleID = moduleIDAndChanges.first; + bool moduleModified = false; + + const auto itModuleTree = myModuleTrees.find(moduleID); + if (itModuleTree == myModuleTrees.end()) + continue; + + SUIT_ShortcutTree* const moduleTree = itModuleTree->second; + + /** {inModuleID, newKeySequence}[] */ + const std::map& moduleChanges = moduleIDAndChanges.second; + for (const auto& inModuleIDAndKS : moduleChanges) { + const QString& inModuleID = inModuleIDAndKS.first; + const QString& keySequence = inModuleIDAndKS.second; + + SUIT_ShortcutTreeItem* const item = moduleTree->findItem(SUIT_ShortcutMgr::splitIntoTokens(inModuleID)); + if (!item) { + ShCutDbg("No SUIT_ShortcutTreeItem for \"" + moduleID + "/" + inModuleID + "\" is found."); + continue; + } + + item->setKeySequence(keySequence); + + const QKeySequence& appliedKeySequence = mgr->getKeySequence(moduleID, inModuleID); + const bool keySequenceModified = QKeySequence::fromString(keySequence) != appliedKeySequence; + item->styleAsKeySequenceModified(keySequenceModified); + moduleModified = moduleModified || keySequenceModified; + } + + moduleModifiedStatuses[indexOf(moduleTree)] = moduleModified; + } + static_cast(tabBar())->mergeTabModifiedStatuses(moduleModifiedStatuses); + + // Key sequences have been changed. Update search tab. + myFindActionWidget->updateUI(); +} + +void SUIT_ShortcutTabWidget::jumpToTreeItem(const QString& theModuleID, const QString& theInModuleID) +{ + const auto itModuleTree = myModuleTrees.find(theModuleID); + if (itModuleTree == myModuleTrees.end()) { + ShCutDbg("SUIT_ShortcutTabWidget::jumpToTreeItem( moduleID = " + theModuleID + ", inModuleID = " + theInModuleID + "): the tab widget is set to not show the module."); + return; + } + + const auto moduleTree = itModuleTree->second; + const auto item = moduleTree->findItem(SUIT_ShortcutMgr::splitIntoTokens(theInModuleID)); + if (!item) { + ShCutDbg("SUIT_ShortcutTabWidget::jumpToTreeItem( moduleID = " + theModuleID + ", inModuleID = " + theInModuleID + "): tree widget does not have such item."); + return; + } + + setCurrentWidget(moduleTree); + moduleTree->scrollToItem(item); + moduleTree->setCurrentItem(item); + onTreeItemDoubleClicked(moduleTree, item); +} + +std::pair SUIT_ShortcutTabWidget::getConfirmedKeySequence(const QString& theModuleID, const QString& theInModuleActionID) const +{ + const QKeySequence& ksInMgr = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID); + const auto itModuleTree = myModuleTrees.find(theModuleID); + if (itModuleTree == myModuleTrees.end()) + std::pair(QString(), !ksInMgr.isEmpty()); + + const auto moduleTree = itModuleTree->second; + const auto item = moduleTree->findItem(SUIT_ShortcutMgr::splitIntoTokens(theInModuleActionID)); + if (!item) + std::pair(QString(), !ksInMgr.isEmpty()); + + const QString itemKSString = item->keySequence(); + return std::pair(itemKSString, itemKSString != ksInMgr.toString()); +} + +void SUIT_ShortcutTabWidget::updateTabs(bool theHighlightModifiedTreeItems, bool theUpdateSyncTabWidgets) +{ + const auto mgr = SUIT_ShortcutMgr::get(); + const QString lang = SUIT_ShortcutMgr::currentLang(); + + std::map moduleModifiedStatuses; + + for (const QString& moduleID : myModuleIDs) { + const auto moduleAssets = mgr->getModuleAssets(moduleID); + + if (!moduleAssets) { + ShCutDbg("SUIT_ShortcutTabWidget::updateTabs: SUIT_ShortcutMgr does not have assets of module \"" + moduleID + "\"." ); + const auto itModuleIDAndTree = myModuleTrees.find(moduleID); + if (itModuleIDAndTree != myModuleTrees.end()) { + SUIT_ShortcutTree* const moduleTree = itModuleIDAndTree->second; + removeTab(indexOf(moduleTree)); + myModuleTrees.erase(itModuleIDAndTree); + delete moduleTree; + } + continue; + } + + auto itModuleIDAndTree = myModuleTrees.find(moduleID); + if (itModuleIDAndTree == myModuleTrees.end()) { + SUIT_ShortcutTree* const moduleTree = new SUIT_ShortcutTree(this, moduleAssets, lang); + itModuleIDAndTree = myModuleTrees.emplace(moduleID, moduleTree).first; + + const int tabIndex = ::SUIT_tools::distanceFromBegin(myModuleIDs, myModuleIDs.find(moduleID)); + insertTab(tabIndex + 1 /*The first tab is SUIT_FindActionWidget.*/, moduleTree, moduleAssets->myIcon, moduleTree->name()); + } + + SUIT_ShortcutTree* const moduleTree = itModuleIDAndTree->second; + const bool isTreeShortcutModified = moduleTree->updateItems(theHighlightModifiedTreeItems); + + moduleModifiedStatuses[indexOf(moduleTree)] = isTreeShortcutModified; + } + + if (!myIsPopulated) { + setCurrentWidget(myModuleTrees.begin()->second); // Set active first tab with shortcut tree. + myIsPopulated = true; + + // Adjust widths of Find tab' columns. + for (int colIdx = 0; colIdx < SUIT_ShortcutTree::ColumnIdx::NotExist; colIdx++) { + int colWidth = 0; + for (const auto& moduleIDAndTree : myModuleTrees) { + const SUIT_ShortcutTree* const moduleTree = moduleIDAndTree.second; + const int treeColWidth = moduleTree->columnWidth(colIdx); + if (treeColWidth > colWidth) + colWidth = treeColWidth; + } + myFindActionWidget->setColumnWidth(SUIT_ShortcutTree::toFoundActionTreeColIdx(SUIT_ShortcutTree::ColumnIdx(colIdx)), colWidth); + } + } + + static_cast(tabBar())->mergeTabModifiedStatuses(moduleModifiedStatuses); + + // Key sequences have been changed. Update search tab. + myFindActionWidget->updateUI(); + + if (theUpdateSyncTabWidgets) { + const std::set& syncTabWidgets = SUIT_ShortcutTabWidget::instances[myShortcutContainer.get()]; + for (const auto syncTabWidget : syncTabWidgets) { + if (syncTabWidget == this) + continue; + + syncTabWidget->updateTabs(theHighlightModifiedTreeItems, false /*theUpdateSyncTabWidgets*/); + const auto editDialog = syncTabWidget->myEditDialog; + editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); + editDialog->updateConflictsMessage(); + } + } +} + + +SUIT_ShortcutTabWidgetBar::SUIT_ShortcutTabWidgetBar(SUIT_ShortcutTabWidget* theTabWidget) : QTabBar(theTabWidget) {} + +void SUIT_ShortcutTabWidgetBar::mergeTabModifiedStatuses(const std::map& theTabModifiedStatuses) +{ + for (const auto& tabModifiedStatus : theTabModifiedStatuses) { + myTabModifiedStatuses[tabModifiedStatus.first] = tabModifiedStatus.second; + } +} + +void SUIT_ShortcutTabWidgetBar::paintEvent(QPaintEvent* theEvent) +{ + QTabBar::paintEvent(theEvent); + + QPainter painter(this); + QStyleOptionTab option; + + for (const auto& tabModifiedStatus : myTabModifiedStatuses) { + const int tabIdx = tabModifiedStatus.first; + const bool isKeySequenceModified = tabModifiedStatus.second; + + initStyleOption(&option, tabIdx); + if (isKeySequenceModified) + painter.fillRect(tabRect(tabIdx), Qt::darkGreen); + else + painter.fillRect(tabRect(tabIdx), palette().background()); + + style()->drawControl(QStyle::CE_TabBarTab, &option, &painter, this); + } + + const int activeTabIdx = static_cast(parentWidget())->currentIndex(); + const auto itActiveTabModifiedStatus = myTabModifiedStatuses.find(activeTabIdx); + if (itActiveTabModifiedStatus != myTabModifiedStatuses.end()) { + const bool isActiveTabModified = itActiveTabModifiedStatus->second; + if (isActiveTabModified) { + setStyleSheet( + "QTabBar::tab:selected {" + " background: darkgreen;" + " color: white;" + "}" + ); + } + else { + setStyleSheet( + "QTabBar::tab:selected {}" + ); + } + } +} + + +/*static*/ SUIT_FoundActionTree::ColumnIdx SUIT_ShortcutTree::toFoundActionTreeColIdx(const SUIT_ShortcutTree::ColumnIdx theColumnIdx) +{ + switch (theColumnIdx) { + case SUIT_ShortcutTree::ColumnIdx::Name: + return SUIT_FoundActionTree::ColumnIdx::Name; + case SUIT_ShortcutTree::ColumnIdx::ToolTip: + return SUIT_FoundActionTree::ColumnIdx::ToolTip; + case SUIT_ShortcutTree::ColumnIdx::KeySequence: + return SUIT_FoundActionTree::ColumnIdx::KeySequence; + default: + throw std::invalid_argument("SUIT_ShortcutTree::toFoundActionTreeColIdx(const SUIT_ShortcutTree::ColumnIdx): not handled ColumnIdx case."); + } +} + +/*static*/ SUIT_ShortcutTree::SortKey SUIT_ShortcutTree::toSortKey(const SUIT_ShortcutTree::ColumnIdx theColumnIdx) +{ + switch (theColumnIdx) { + case SUIT_ShortcutTree::ColumnIdx::Name: + return SUIT_ShortcutTree::SortKey::Name; + case SUIT_ShortcutTree::ColumnIdx::ToolTip: + return SUIT_ShortcutTree::SortKey::ToolTip; + case SUIT_ShortcutTree::ColumnIdx::KeySequence: + return SUIT_ShortcutTree::SortKey::KeySequence; + default: + throw std::invalid_argument("SUIT_ShortcutTree::toSortKey(const SUIT_ShortcutTree::ColumnIdx): not handled ColumnIdx case."); + } +} + +/*static*/ SUIT_ShortcutTree::ColumnIdx SUIT_ShortcutTree::toColumnIdx(const SUIT_ShortcutTree::SortKey theSortKey) +{ + switch (theSortKey) + { + case SUIT_ShortcutTree::SortKey::Name: + return SUIT_ShortcutTree::ColumnIdx::Name; + case SUIT_ShortcutTree::SortKey::ToolTip: + return SUIT_ShortcutTree::ColumnIdx::ToolTip; + case SUIT_ShortcutTree::SortKey::KeySequence: + return SUIT_ShortcutTree::ColumnIdx::KeySequence; + default: + return SUIT_ShortcutTree::ColumnIdx::NotExist; + } +} + +/*static*/ const QList> SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA = +{ + {SUIT_ShortcutTree::SortKey::Name, Qt::SortOrder::AscendingOrder}, + {SUIT_ShortcutTree::SortKey::ToolTip, Qt::SortOrder::AscendingOrder}, + {SUIT_ShortcutTree::SortKey::KeySequence, Qt::SortOrder::AscendingOrder}, + {SUIT_ShortcutTree::SortKey::ID, Qt::SortOrder::AscendingOrder} +}; + +SUIT_ShortcutTree::SUIT_ShortcutTree( + SUIT_ShortcutTabWidget* theParent, + std::shared_ptr theAssets, + const QString& theLang +) : QTreeWidget(theParent), +myParentTabWidget(theParent), myAssets(theAssets), +myLeadingSortKey(SUIT_ShortcutTree::SortKey::Name), myIsPopulated(false) +{ + for (const auto& keyAndOrder : SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA) { + mySortSchema[keyAndOrder.first] = keyAndOrder.second; + } + + setLang(theLang); + + setSelectionMode(QAbstractItemView::SingleSelection); + setColumnCount(int(SUIT_ShortcutTree::ColumnIdx::NotExist) - 1); + setHeaderItem(new SUIT_ShortcutTreeHeaderItem(mySortSchema, SUIT_ShortcutTree::toColumnIdx(myLeadingSortKey))); + setSortingEnabled(false); // Custom sorting is engaged. + header()->setSectionResizeMode(QHeaderView::Interactive); + header()->setSectionsClickable(true); + + setExpandsOnDoubleClick(false); // Open shortcut editor, if item is action, on double click instead. + setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + this->installEventFilter(this); + + connect(header(), SIGNAL(sectionClicked(int)), this, SLOT(onHeaderItemClicked(int))); + connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), this, SLOT(onSortColOrOrderChanged(int, Qt::SortOrder))); + connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); + connect(this, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(onItemExpanded(QTreeWidgetItem*))); + connect(this, SIGNAL(itemCollapsed(QTreeWidgetItem*)), this, SLOT(onItemCollapsed(QTreeWidgetItem*))); +} + +SUIT_ShortcutTabWidget* SUIT_ShortcutTree::parentTabWidget() const +{ + return myParentTabWidget; // static_cast(QWidget::parentWidget()) somehow is not the same. +} + +const SUIT_ShortcutContainer& SUIT_ShortcutTree::shortcutContainer() const +{ + return *(myParentTabWidget->myShortcutContainer); +} + +const QString& SUIT_ShortcutTree::moduleID() const +{ + return myAssets->myModuleID; +} + +void SUIT_ShortcutTree::setLang(const QString& theLang) +{ + myName = myAssets->bestName(theLang); + myToolTip = myAssets->bestToolTip(theLang); +} + +const QString& SUIT_ShortcutTree::name() const +{ + return myName; +} + +const QString& SUIT_ShortcutTree::toolTip() const +{ + return myToolTip; +} + +void SUIT_ShortcutTree::sort(SUIT_ShortcutTree::SortKey theLeadingKey, Qt::SortOrder theOrder) +{ + if (theLeadingKey == myLeadingSortKey && theOrder == mySortSchema[theLeadingKey]) + return; + + myLeadingSortKey = theLeadingKey; + mySortSchema[theLeadingKey] = theOrder; + + const auto selectedItem = currentItem(); + sortRecursive(invisibleRootItem()); + if (selectedItem) { + scrollToItem(selectedItem); + setCurrentItem(selectedItem); + } +} + +void SUIT_ShortcutTree::sortRecursive(QTreeWidgetItem* theParentItem) +{ + const auto sortedChildren = getSortedChildren(theParentItem); + + theParentItem->takeChildren(); + + for (const auto childItem : sortedChildren) { + theParentItem->addChild(childItem); + childItem->setExpanded(childItem->isExpanded()); + sortRecursive(childItem); + } +} + +bool SUIT_ShortcutTree::updateItems(bool theHighlightModified) +{ + const std::shared_ptr moduleAssets = SUIT_ShortcutMgr::get()->getModuleAssets(moduleID()); + const bool isKeySequenceModified = updateChildrenItemsRecursively(theHighlightModified, invisibleRootItem(), true /*theItemIsInvisibleRoot*/); + + if (!myIsPopulated) { // Do not adjust column widths more than once to not reset user-made adjustments. + for (int colIdx = 0; colIdx < columnCount(); colIdx++) { + resizeColumnToContents(colIdx); + } + myIsPopulated = true; + } + + return isKeySequenceModified; +} + +bool SUIT_ShortcutTree::updateChildrenItemsRecursively( + bool theHighlightModified, + QTreeWidgetItem* const theItem, + bool theItemIsInvisibleRoot +) { + const auto mgr = SUIT_ShortcutMgr::get(); + const QString lang = SUIT_ShortcutMgr::currentLang(); + + // { IDLastToken, assets }. + const auto& childrenAssets = theItemIsInvisibleRoot ? myAssets->children() : static_cast(theItem)->myAssets->children(); + const auto& moduleShortcuts = shortcutContainer().getModuleShortcutsInversed(moduleID()); + + bool isKSModified = false; + + // Update exisiting child items. + for (int childItemIdx = 0; childItemIdx < theItem->childCount(); childItemIdx++) { + const auto childItem = static_cast(theItem->child(childItemIdx)); + const auto itChildAssets = childrenAssets.find(childItem->lastTokenOfID()); + if (itChildAssets == childrenAssets.end()) { + // Assets of the child item has been removed from ShortcutMgr - impossible. + ShCutDbg("Invalid logics: assets of \"" + moduleID() + "/" + childItem->inModuleID() + "\" are missing in ShortcutMgr."); + continue; + } + + if (childItem->isAction()) { + const bool isChildKSModified = updateItemShortcut(childItem, moduleShortcuts, theHighlightModified); + isKSModified = isKSModified || isChildKSModified; + } + + const bool isDescendantKSModified = updateChildrenItemsRecursively(theHighlightModified, childItem); + isKSModified = isKSModified || isDescendantKSModified; + } + + // Add new child items. + if (theItem->childCount() < childrenAssets.size()) { + auto sortedChildren = getSortedChildren(theItem); + for (const auto& childTokenAndAssets : childrenAssets) { + const QString& childToken = childTokenAndAssets.first; + const auto& childAssets = childTokenAndAssets.second; + + const auto predicate = [&childToken](const SUIT_ShortcutTreeItem* const theItem) -> bool { + return theItem->lastTokenOfID() == childToken; + }; + + if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) { + if (!childAssets->isAction() && childAssets->children().empty()) + continue; // Do not create empty pure folders. + + const auto newChildItem = new SUIT_ShortcutTreeItem(childAssets, lang); + if (newChildItem->isAction()) { + const bool isChildKSModified = updateItemShortcut(newChildItem, moduleShortcuts, theHighlightModified); + isKSModified = isKSModified || isChildKSModified; + } + + insertChild(theItem, sortedChildren, newChildItem); + newChildItem->setExpanded(true); // Make tree expanded on first show. + const bool isDescendantKSModified = updateChildrenItemsRecursively(theHighlightModified, newChildItem); + isKSModified = isKSModified || isDescendantKSModified; + } + } + } + + return isKSModified; +} + +bool SUIT_ShortcutTree::updateItemShortcut( + SUIT_ShortcutTreeItem* const theItem, + const std::map& theModuleShortcuts, + bool theHighlightIfModified +) const { + static const QKeySequence NO_KEYSEQUENCE = QKeySequence(); + + const auto itShortcut = theModuleShortcuts.find(theItem->inModuleID()); + const QKeySequence& newKeySequence = itShortcut == theModuleShortcuts.end() ? NO_KEYSEQUENCE : itShortcut->second; + const QString newKeySequenceString = newKeySequence.toString(); + + if (theItem->keySequence() != newKeySequenceString) + theItem->setKeySequence(newKeySequenceString); + + const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID(), theItem->inModuleID()); + const bool isKeySequenceModified = newKeySequence != appliedKeySequence; + if (theHighlightIfModified) + theItem->styleAsKeySequenceModified(isKeySequenceModified); + else + theItem->styleAsKeySequenceModified(false); + + return isKeySequenceModified; +} + +SUIT_ShortcutTreeItem* SUIT_ShortcutTree::findItem(QStringList theRelativeIDTokens, const QTreeWidgetItem* theAncestorItem) const +{ + if (theRelativeIDTokens.isEmpty()) + return nullptr; + + if (!theAncestorItem) + theAncestorItem = invisibleRootItem(); + + const QString& token = theRelativeIDTokens.front(); + for (int childIdx = 0; childIdx < theAncestorItem->childCount(); childIdx++) { + const auto childItem = static_cast(theAncestorItem->child(childIdx)); + if (childItem->lastTokenOfID() == token) { + if (theRelativeIDTokens.length() > 1) { + theRelativeIDTokens.pop_front(); + return findItem(std::move(theRelativeIDTokens), childItem); + } + else + return childItem; + } + } + + return nullptr; +} + +std::set> SUIT_ShortcutTree::getSortedChildren(QTreeWidgetItem* theParentItem) +{ + auto sortSchema = SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA; + for (auto itKeyAndOrder = sortSchema.begin(); itKeyAndOrder != sortSchema.end();) { + if (itKeyAndOrder->first == myLeadingSortKey) + sortSchema.erase(itKeyAndOrder); + else + itKeyAndOrder->second = mySortSchema[itKeyAndOrder->first]; + + itKeyAndOrder++; + } + sortSchema.push_front(std::pair(myLeadingSortKey, mySortSchema[myLeadingSortKey])); + + const std::function comparator = + [this, sortSchema](const SUIT_ShortcutTreeItem* theItemA, const SUIT_ShortcutTreeItem* theItemB) { + // Folders first. + if (theItemA->isFolder()) { + if (!theItemB->isFolder()) + return true; + } + else { + if (theItemB->isFolder()) + return false; + } + + const auto collator = QCollator(); + for (const auto& keyAndOrder : sortSchema) { + const QString valA = theItemA->getValue(keyAndOrder.first); + const QString valB = theItemB->getValue(keyAndOrder.first); + + // Empty lines last. + if (valA.isEmpty()) { + if (!valB.isEmpty()) + return false; + } + else { + if (valB.isEmpty()) + return true; + } + + const int res = collator.compare(valA, valB); + if (res != 0) + return keyAndOrder.second == Qt::SortOrder::AscendingOrder ? res < 0 : res > 0; + } + return false; + }; + + std::set> sortedChildren(comparator); + for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) { + SUIT_ShortcutTreeItem* const childItem = static_cast(theParentItem->child(childIdx)); + sortedChildren.emplace(childItem); + } + return sortedChildren; +} + +void SUIT_ShortcutTree::insertChild( + QTreeWidgetItem* theParentItem, + std::set>& theSortedChildren, + SUIT_ShortcutTreeItem* theChildItem +) { + const auto emplaceRes = theSortedChildren.emplace(theChildItem); + theParentItem->insertChild(::SUIT_tools::distanceFromBegin(theSortedChildren, emplaceRes.first), theChildItem); +} + +void SUIT_ShortcutTree::onItemExpanded(QTreeWidgetItem* theItem) +{ + static_cast(theItem)->setExpanded(true); +} + +void SUIT_ShortcutTree::onItemCollapsed(QTreeWidgetItem* theItem) +{ + static_cast(theItem)->setExpanded(false); +} + +void SUIT_ShortcutTree::onHeaderItemClicked(int theColIdx) +{ + const auto customHeaderItem = static_cast(headerItem()); + + const auto theLeadingColIdx = static_cast(theColIdx); + const auto theNewLeadingSortKey = SUIT_ShortcutTree::toSortKey(theLeadingColIdx); + if (myLeadingSortKey == theNewLeadingSortKey) { // Click on active column. Change order. + const auto newSortOrder = mySortSchema[myLeadingSortKey] == Qt::SortOrder::AscendingOrder ? Qt::SortOrder::DescendingOrder : Qt::SortOrder::AscendingOrder; + sort(myLeadingSortKey, newSortOrder); + } + else /* if click on inactive column. Change sorting key, preserve order. */ + sort(theNewLeadingSortKey, mySortSchema[theNewLeadingSortKey]); + + customHeaderItem->setSortSchemaAndLeadingColIdx(mySortSchema, theLeadingColIdx); +} + +void SUIT_ShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx) +{ + parentTabWidget()->onTreeItemDoubleClicked(this, static_cast(theItem)); +} + + +SUIT_ShortcutTreeItem::SUIT_ShortcutTreeItem(const std::shared_ptr theAssets, const QString& theLang) +: QTreeWidgetItem(), myAssets(theAssets), myIsExpanded(QTreeWidgetItem::isExpanded()) +{ + setFlags(Qt::ItemIsEnabled); + setIcon(SUIT_ShortcutTree::ColumnIdx::Name, myAssets->icon()); + setLang(theLang); + + if (myAssets->isFolder()) + styleAsFolder(true); +} + +void SUIT_ShortcutTreeItem::setLang(const QString& theLang) +{ + setText(SUIT_ShortcutTree::ColumnIdx::Name, myAssets->bestName(theLang)); + myPath = myAssets->bestPath(theLang); + + myToolTip = myAssets->bestToolTip(theLang); + if (!myToolTip.isEmpty() && myToolTip.at(myToolTip.length()-1) != ".") + myToolTip += "."; + + setText(SUIT_ShortcutTree::ColumnIdx::ToolTip, myToolTip); + + if (isAction()) { + if (myToolTip.isEmpty()) + setToolTip( + SUIT_ShortcutTree::ColumnIdx::Name, + SUIT_ShortcutTree::tr("Double click to edit key sequence.") + ); + else + setToolTip( + SUIT_ShortcutTree::ColumnIdx::Name, + myToolTip + "\n" + SUIT_ShortcutTree::tr("Double click to edit key sequence.") + ); + } + else { + setToolTip( + SUIT_ShortcutTree::ColumnIdx::Name, + myToolTip + ); + } +} + +const QString& SUIT_ShortcutTreeItem::moduleID() const +{ + return myAssets->myModuleID; +} + +const QString& SUIT_ShortcutTreeItem::inModuleID() const +{ + return myAssets->myInModuleID; +} + +const QString& SUIT_ShortcutTreeItem::lastTokenOfID() const +{ + return myAssets->myIDLastToken; +} + +bool SUIT_ShortcutTreeItem::isAction() const +{ + return myAssets->isAction(); +} + +bool SUIT_ShortcutTreeItem::isFolder() const +{ + return myAssets->isFolder(); +} + +QString SUIT_ShortcutTreeItem::name() const +{ + return text(SUIT_ShortcutTree::ColumnIdx::Name); +} + +const QString& SUIT_ShortcutTreeItem::path() const +{ + return myPath; +} + +const QString& SUIT_ShortcutTreeItem::toolTip() const +{ + return myToolTip; +} + +QString SUIT_ShortcutTreeItem::getValue(SUIT_ShortcutTree::SortKey theKey) const +{ + switch (theKey) { + case SUIT_ShortcutTree::SortKey::ID: + return inModuleID(); + case SUIT_ShortcutTree::SortKey::Name: + return name(); + case SUIT_ShortcutTree::SortKey::ToolTip: + return myToolTip; + case SUIT_ShortcutTree::SortKey::KeySequence: + return keySequence(); + default: + return QString(); + } +} + +void SUIT_ShortcutTreeItem::setKeySequence(const QString& theKeySequence) +{ + if (!isAction()) + return; + + setText(SUIT_ShortcutTree::ColumnIdx::KeySequence, theKeySequence); +} + +QString SUIT_ShortcutTreeItem::keySequence() const +{ + if (!isAction()) + return QString(); + + return text(SUIT_ShortcutTree::ColumnIdx::KeySequence); +} + +void SUIT_ShortcutTreeItem::styleAsFolder(bool theIsFolder) +{ + QFont f = font(SUIT_ShortcutTree::ColumnIdx::Name); + f.setBold(theIsFolder); + setFont(SUIT_ShortcutTree::ColumnIdx::Name, f); +} + +void SUIT_ShortcutTreeItem::styleAsKeySequenceModified(bool theIsModified) +{ + if (!isAction()) + return; + + static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen); + static const QBrush fgHighlitingBrush = QBrush(Qt::white); + static const QBrush noBrush = QBrush(); + + setBackground(SUIT_ShortcutTree::ColumnIdx::KeySequence, theIsModified ? bgHighlitingBrush : noBrush); + setForeground(SUIT_ShortcutTree::ColumnIdx::KeySequence, theIsModified ? fgHighlitingBrush : noBrush); + + { // Style ascendant folders as "descendant' key sequence is modified". + SUIT_ShortcutTreeItem* ascendant = dynamic_cast(parent()); + while (ascendant) { + ascendant->setBackground(SUIT_ShortcutTree::ColumnIdx::Name, theIsModified ? bgHighlitingBrush : noBrush); + ascendant->setForeground(SUIT_ShortcutTree::ColumnIdx::Name, theIsModified ? fgHighlitingBrush : noBrush); + ascendant = dynamic_cast(ascendant->parent()); + } + } +} + +void SUIT_ShortcutTreeItem::setExpanded(bool theVal) +{ + myIsExpanded = theVal; + QTreeWidgetItem::setExpanded(myIsExpanded); +} + +bool SUIT_ShortcutTreeItem::isExpanded() const +{ + return myIsExpanded; +} + + +SUIT_ShortcutTreeHeaderItem::SUIT_ShortcutTreeHeaderItem(const std::map& theSortSchema, SUIT_ShortcutTree::ColumnIdx theLeadingColIdx) +: QTreeWidgetItem() +{ + setText(SUIT_ShortcutTree::ColumnIdx::Name, SUIT_ShortcutTree::tr("Action")); + setText(SUIT_ShortcutTree::ColumnIdx::ToolTip, SUIT_ShortcutTree::tr("Description")); + setText(SUIT_ShortcutTree::ColumnIdx::KeySequence, SUIT_ShortcutTree::tr("Key sequence")); + setSortSchemaAndLeadingColIdx(theSortSchema, theLeadingColIdx); +} + +void SUIT_ShortcutTreeHeaderItem::setSortSchemaAndLeadingColIdx(const std::map& theSortSchema, SUIT_ShortcutTree::ColumnIdx theLeadingColIdx) +{ + static const QIcon ICON_ASCENDING_ORDER = QIcon(":/resources/sort_ascending.svg"); + static const QIcon ICON_DESCENDING_ORDER = QIcon(":/resources/sort_descending.svg"); + static const QIcon ICON_ASCENDING_ORDER_LEADING = QIcon(":/resources/sort_ascending_leading_key.svg"); + static const QIcon ICON_DESCENDING_ORDER_LEADING = QIcon(":/resources/sort_descending_leading_key.svg"); + + for (const auto& keyAndOrder : theSortSchema) { + const auto colIdx = SUIT_ShortcutTree::toColumnIdx(keyAndOrder.first); + if (colIdx >= SUIT_ShortcutTree::ColumnIdx::NotExist) + continue; + + if (colIdx == theLeadingColIdx) + setIcon(colIdx, keyAndOrder.second == Qt::SortOrder::AscendingOrder ? ICON_ASCENDING_ORDER_LEADING : ICON_DESCENDING_ORDER_LEADING); + else + setIcon(colIdx, keyAndOrder.second == Qt::SortOrder::AscendingOrder ? ICON_ASCENDING_ORDER : ICON_DESCENDING_ORDER); + } +} \ No newline at end of file diff --git a/src/SUIT/SUIT_ShortcutEditor.h b/src/SUIT/SUIT_ShortcutEditor.h new file mode 100644 index 000000000..2c564189b --- /dev/null +++ b/src/SUIT/SUIT_ShortcutEditor.h @@ -0,0 +1,391 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef SUIT_SHORTCUTEDITOR_H +#define SUIT_SHORTCUTEDITOR_H + +#include "SUIT.h" +#include +#include +#include +#include +#include +#include +#include +#include "SUIT_ShortcutMgr.h" +#include "SUIT_FindActionDialog.h" +#include +#include +#include +#include + + +class QLineEdit; +class QLabel; +class QPushButton; +class QTreeWidgetItem; + +class SUIT_EXPORT SUIT_KeySequenceEdit : public QFrame +{ + Q_OBJECT + +public: + SUIT_KeySequenceEdit(QWidget* = nullptr); + virtual ~SUIT_KeySequenceEdit() = default; + + /*! \brief Set a key sequence to edit. */ + void setConfirmedKeySequence(const QKeySequence&); + void setEditedKeySequence(const QKeySequence&); + QKeySequence editedKeySequence() const; + + /*! \returns true, if the edited key sequence differs from confirmed one. */ + bool isKeySequenceModified() const; + + /*! \brief Set confirmed key sequence to line editor. */ + void restoreKeySequence(); + + /*! \brief Extracts key sequnce string from theEvent. */ + static QString parseEvent(QKeyEvent* theEvent); + + /*! \brief Check if the key event contains a 'valid' key. + \param theKey Code of the key. */ + static bool isValidKey(int theKey); + +signals: + void editingStarted(); + + + void editingFinished(); + void restoreFromShortcutMgrClicked(); + +private slots: + /*! \brief Called when "Clear" button is clicked. */ + void onClear(); + + /*! \brief Called when myKeySequenceLineEdit loses focus. */ + void onEditingFinished(); + +protected: + /*! \returns \c true if further event processing should be stopped. */ + virtual bool eventFilter(QObject* theObject, QEvent* theEvent); + +private: + void initialize(); + +private: + QLineEdit* myKeySequenceLineEdit; + QString myConfirmedKeySequenceString; + + // Last valid key sequence string from myKeySequenceLineEdit. + QString myPrevKeySequenceString; +}; + +class SUIT_ShortcutTabWidget; +class SUIT_ShortcutTree; +class SUIT_ShortcutTreeItem; +class QTextEdit; + + +class SUIT_EXPORT SUIT_EditKeySequenceDialog : public QDialog +{ + Q_OBJECT + +public: + /*! \param theParent must not be nullptr. */ + SUIT_EditKeySequenceDialog(SUIT_ShortcutTabWidget* theParent); + SUIT_EditKeySequenceDialog(const SUIT_EditKeySequenceDialog&) = delete; + SUIT_EditKeySequenceDialog& operator=(const SUIT_EditKeySequenceDialog&) = delete; + virtual ~SUIT_EditKeySequenceDialog() = default; + + void setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID); + const QString& moduleID() const; + const QString& inModuleActionID() const; + + void setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip = ""); + + void setConfirmedKeySequence(const QKeySequence& theSequence); + QKeySequence editedKeySequence() const; + + /*! \brief Updates message with list of actions, whose shortcuts will be disabled on Confirm. */ + void updateConflictsMessage(); + + int exec(); + +private slots: + void onEditingStarted(); + void onEditingFinished(); + void onRestoreFromShortcutMgr(); + void onConfirm(); + +private: + QString myModuleID; + QString myInModuleActionID; + QLabel* myActionName; + SUIT_KeySequenceEdit* myKeySequenceEdit; + QTextEdit* myTextEdit; +}; + + +/*! \brief Main element of Shotcut Editor GUI. Each tab presents a shortcut tree of a module. */ +class SUIT_EXPORT SUIT_ShortcutTabWidget : public QTabWidget +{ + Q_OBJECT + + friend class SUIT_ShortcutTree; + +private: + /** + * Ensures that, if several SUIT_ShortcutTab instances coexist, + * all of them are updated when one of them applies pending changes to SUIT_ShortcutMgr. + * + * Sharing of SUIT_ShortcutContainer allows to keep trees in tabs synchronized even without + * applying changes to SUIT_ShortcutMgr. Why? See SUIT_PagePrefShortcutTreeItem. + * + * Access is not synchronized in assumption, that all instances live in the same thread. + */ + static std::map> instances; + +public: + /*! \param theContainer Share the same container between several trees, + to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr. + Pass nullptr to create non-synchronized tab widget. */ + SUIT_ShortcutTabWidget( + const std::set& theIDsOfModulesToShow, + std::shared_ptr theContainer = std::shared_ptr(), + QWidget* theParent = nullptr + ); + + SUIT_ShortcutTabWidget& operator=(const SUIT_ShortcutTree&) = delete; + virtual ~SUIT_ShortcutTabWidget(); + + /*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */ + void setShortcutsFromManager(); + + /*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */ + void setDefaultShortcuts(); + + /*! \brief Applies pending changes to ShortcutMgr. Updates other instances of SUIT_ShortcutTabWidget. */ + void applyChangesToShortcutMgr(); + + std::shared_ptr shortcutContainer() const; + + void onTreeItemDoubleClicked(SUIT_ShortcutTree* theTree, SUIT_ShortcutTreeItem* theItem); + +private: + void jumpToTreeItem(const QString& theModuleID, const QString& theInModuleID); + + /*! \returns Key sequence from the tree and true, if it differs from a keysequence in ShortcutMgr (applied key sequence). */ + std::pair getConfirmedKeySequence(const QString& theModuleID, const QString& theInModuleActionID) const; + + /*! \param If theUpdateSyncTabWidgets, ShortcutTabWidgets sharing the same shortcut container are updated. */ + void updateTabs(bool theHighlightModifiedTreeItems, bool theUpdateSyncTabWidgets); + +public: + /** Keeps IDs of modules, which will are shown on setShortcutsFromManager(). */ + const std::set myModuleIDs; + +private: + /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. Never nullpt. */ + const std::shared_ptr myShortcutContainer; + + SUIT_FindActionWidget* myFindActionWidget; + + /** {moduleID, SUIT_ShortcutTree}. All tabs share the same SUIT_ShortcutContainer. */ + std::map myModuleTrees; + + SUIT_EditKeySequenceDialog* myEditDialog; + bool myIsPopulated; // If SUIT_ShortcutTabWidget::updateTabs(bool, bool) was called at least once. +}; + + +class SUIT_ShortcutTabWidgetBar : public QTabBar +{ + Q_OBJECT + +public: + SUIT_ShortcutTabWidgetBar(SUIT_ShortcutTabWidget* theTabWidget); + + void mergeTabModifiedStatuses(const std::map& theTabModifiedStatuses); + +protected: + void paintEvent(QPaintEvent* theEvent); + +private: + /** {tabIdx, isKeySequenceModified}[] */ + std::map myTabModifiedStatuses; +}; + + +class SUIT_EXPORT SUIT_ShortcutTree : public QTreeWidget +{ + Q_OBJECT + + friend class SUIT_ShortcutTabWidget; + +public: + enum ColumnIdx { + Name, + KeySequence, // Empty, if item is pure folder item. + ToolTip, + ///////////////////////////////////////////////////////////// + NotExist // Columns with the index and greater are not exist. + }; + + enum class SortKey { + ID, + Name, + ToolTip, + KeySequence, + }; + + static SUIT_FoundActionTree::ColumnIdx toFoundActionTreeColIdx(const SUIT_ShortcutTree::ColumnIdx theColumnIdx); + static SUIT_ShortcutTree::SortKey toSortKey(const SUIT_ShortcutTree::ColumnIdx theColumnIdx); + static SUIT_ShortcutTree::ColumnIdx toColumnIdx(const SUIT_ShortcutTree::SortKey theSortKey); + static const QList> DEFAULT_SORT_SCHEMA; + +private: + /*! + \param theParent must not be nullptr. + \param theAssets must not be nullptr. */ + SUIT_ShortcutTree( + SUIT_ShortcutTabWidget* theParent, + std::shared_ptr theAssets, + const QString& theLang + ); + SUIT_ShortcutTree(const SUIT_ShortcutTree&) = delete; + SUIT_ShortcutTree& operator=(const SUIT_ShortcutTree&) = delete; + +public: + virtual ~SUIT_ShortcutTree() = default; + + SUIT_ShortcutTabWidget* parentTabWidget() const; + const SUIT_ShortcutContainer& shortcutContainer() const; + const QString& moduleID() const; + + void setLang(const QString& theLang); + const QString& name() const; + const QString& toolTip() const; + + void sort(SUIT_ShortcutTree::SortKey theLeadingKey, Qt::SortOrder theOrder); + +private: + void sortRecursive(QTreeWidgetItem* theParentItem); + + /*! \returns True, if at least one key sequence is modified (differs from applied one). */ + bool updateItems(bool theHighlightModified); + + /*! \returns True, if at least one key sequence is modified (differs from applied one). */ + bool updateChildrenItemsRecursively( + bool theHighlightModified, + QTreeWidgetItem* const theItem, + bool theItemIsInvisibleRoot = false + ); + + /*! \returns True, if key sequence is modified (differs from applied one). */ + bool updateItemShortcut( + SUIT_ShortcutTreeItem* const theItem, + const std::map& theModuleShortcuts, + bool theHighlightModified + ) const; + + SUIT_ShortcutTreeItem* findItem(QStringList theRelativeIDTokens, const QTreeWidgetItem* theAncestorItem = nullptr) const; + + /*! \returns Children of theParentItem being sorted according to current sort mode and order. */ + std::set> getSortedChildren(QTreeWidgetItem* theParentItem); + + /*! \brief Inserts theChildItem to theParentItem and theSortedChildren. + Does not check whether theSortedChildren are actually child items of theParentItem. + Does not check whether current item sort schema is same as one of theSortedChildren. */ + void insertChild( + QTreeWidgetItem* theParentItem, + std::set>& theSortedChildren, + SUIT_ShortcutTreeItem* theChildItem + ); + +private slots: + void onItemExpanded(QTreeWidgetItem* theItem); + void onItemCollapsed(QTreeWidgetItem* theItem); + void onHeaderItemClicked(int theColIdx); + void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx); + +public: + const std::shared_ptr myAssets; + +private: + SUIT_ShortcutTabWidget* const myParentTabWidget; // Because dynamic_cast(QWidget::parentWidget()) == nullptr. + + SUIT_ShortcutTree::SortKey myLeadingSortKey; + std::map mySortSchema; + + QString myName; + QString myToolTip; + + bool myIsPopulated; // If SUIT_ShortcutTree::updateItems(bool) was called at least once. +}; + + +class SUIT_ShortcutTreeItem : public QTreeWidgetItem +{ +public: + /*! \param theAssets must not be nullptr. */ + SUIT_ShortcutTreeItem(const std::shared_ptr theAssets, const QString& theLang); + + void setLang(const QString& theLang); + + const QString& moduleID() const; + const QString& inModuleID() const; + const QString& lastTokenOfID() const; + bool isAction() const; + bool isFolder() const; + + QString name() const; + const QString& path() const; + const QString& toolTip() const; + QString getValue(SUIT_ShortcutTree::SortKey theKey) const; + void setKeySequence(const QString& theKeySequence); + QString keySequence() const; + + void styleAsFolder(bool theIsFolder); + void styleAsKeySequenceModified(bool theIsModified); + + void setExpanded(bool theVal); + bool isExpanded() const; + +public: + const std::shared_ptr myAssets; + bool myIsExpanded; + +private: + QString myPath; + QString myToolTip; +}; + + +class SUIT_ShortcutTreeHeaderItem : public QTreeWidgetItem +{ +public: + SUIT_ShortcutTreeHeaderItem(const std::map& theSortSchema, SUIT_ShortcutTree::ColumnIdx theLeadingColIdx); + SUIT_ShortcutTreeHeaderItem(const SUIT_ShortcutTreeHeaderItem&) = delete; + SUIT_ShortcutTreeHeaderItem& operator=(const SUIT_ShortcutTreeHeaderItem&) = delete; + ~SUIT_ShortcutTreeHeaderItem() = default; + + void setSortSchemaAndLeadingColIdx(const std::map& theSortSchema, SUIT_ShortcutTree::ColumnIdx theLeadingColIdx); +}; + +#endif // SUIT_SHORTCUTEDITOR_H diff --git a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md index 13241772d..041d2679b 100644 --- a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md +++ b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md @@ -1,20 +1,11 @@ # ShortcutMgr ReadMe + At first, read SUIT_ShortcutMgr class documentation in code. -Hot keys must be considered as resources, being shared between all components of an application. E.g. it is unacceptable to have 'Close file' and 'Redo' actions being assigned to the same key sequence. When the SHAPER module is active, the application desktop is active too. The desktop has own hot keys, and they must not interfere with ones of SHAPER. Since the task implies granting users a right to assign shortcuts on their will, the application must track all assigned shortcuts of all modules, prevent intolerable user shortcut modifications and govern actual binding of QActions with key sequences. +For all identified actions, assets must be placed into asset files. People who do/refine localizations should keep this in mind and also process JSON files, which are referred in resource files in sections `
`. -`SUIT_ShortcutMgr` handles shortcuts of SALOME desktop and all modules. It is solely responsible and capable to dynamically bind actions with key sequences and (de)serialize shortcut preferences using `SUIT_ResourceMgr`. `SUIT_ShortcutContainer` encapsulates logics of conflict detecting and resolving. `SUIT_ShortcutTree` widget provides GUI to change shortcut preferences conveniently: it allows to remap plenty of shortcuts without applying, displays conflict-resolving dialog, highlights modifications until they are applied (saved into preference files). +To alleviate process of composing resource and asset files, a development tool `DevTools` has been made. See documentations of the class in code. -To (de)serialize shortcut preferences without dependence on language environment, shortcuts must be stored as pairs {action ID, key sequence}, where action IDs must be application-unique. - -Since desktop shortcuts may also be changed and interfere with shortcuts of modules, `SUIT_ShortcutTree` should always display desktop shortcuts and shortcuts of all modules altogether, even if some modules are inactive. It means, that `SUIT_ShortcutTree` must be fed not only with shortcut data {action ID, key sequence}[], but also with dictionaries {action ID, action name}[]. `SUIT_ShortcutTree` also requires other action assets - tool tip and icon path. - -Assets of actions may be retrieved from instances of actions, but there is a pitfall: if a module has not been activated yet, its actions have not been initialized either. -Qt Linguist is no help in this case too. To retrieve an action name using `QObject::tr(actionID)`, the `tr(const char*)` method must be called with instance of the class, which is designated as a context for the actionID in *.ts files. And contexts are usually descendants of SUIT_Application and CAM_Module. Again, until a module instance is created, there is no way for `SUIT_ShortcutMgr` to get even a name of a context-class, which an action with an ID belongs to, without any additional data. Straightforward mechanism for loading of action assets in advance has been devised: for all actions, which are bound by default or may be bound by user to hotkeys, assets must be placed into asset files. People who do/refine localizations should keep this in mind and also process JSON files, which are referred in resource files in sections `
`. - -To alleviate process of composing resource and asset files, a development tool `DevTools` has been made. It grabs all shortcuts and assets (except icon path) of intercepted at runtime actions with valid IDs and dumps shortcuts into XML files with identical to project-conventional resource files structure and assets into properly-formatted JSON files. Run modules/features of interest, switch application language, and if IDs or names in the selected language of some actions of the module are not added yet to preference files, these dump files will be appended with new data – shortcuts and assets, if the last ones are provided in *.ts files. -The tool also logs assets of intercepted actions with invalid IDs. The intent is as follows: run modules/features of interest at several languages in exactly the same sequence of actions, paste content of resulting language-dedicated *.csv files to corresponding sheet of [“ShortcutMgr. Resource generator.xlsx”](../../tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx). Then come up with proper IDs for the actions, type the action IDs and their module IDs into corresponding columns and take away ready resource and asset items. - -Shortcuts and assets for all actions of SHAPER and GEOM and those actions of desktop, which were bound (hardcoded) to non-empty key sequences, have been added to resource files. Now they are available for hot key remapping via GUI, no conflicts guaranteed. Any hardcoded shortcut is disabled, unless the same shortcut persists in resource files. +Shortcuts and assets for all actions of SHAPER and GEOM and those actions of desktop, which were bound (hardcoded) to non-empty key sequences, have been added to preference and asset files. Now they are available for hot key remapping via GUI, no conflicts guaranteed. A hardcoded shortcut may be changed or disabled, if preference files override its key sequence. ## Conflicts between shortcuts of desktop and modules, except SHAPER and GEOM @@ -62,7 +53,7 @@ Then help menu has different shortcuts if the application is run in EN or FR. 2. After every occurrence of `QPushButton` creation type something like this: ``` // Occurrence -const auto helpButton = new QPushButton(tr("&Help"); +const auto helpButton = new QPushButton(tr("&Help")); SUIT_ShortcutMgr::get()->registerButtonActions("/#AltHelp", *helpButton); ---------------------------------------------------- @@ -74,7 +65,7 @@ void SUIT_ShortcutMgr::registerButtonActions(const QString& theActionID, const Q } ---------------------------------------------------- // Resource file -
+
``` @@ -82,6 +73,5 @@ Thus, ampersand-shortcuts will appear and be treated in shortcut editor as regul If the second option is preferable, should different ampersand-shortcuts for every target language be placed in resource files? ## Minor issues -1. `SUIT_ShortcutTree` widget does not take the whole available height of preference window, it only takes as mush as its items require. -2. Selection of `SUIT_ShortcutTree`' item shadows "modified" highlighter. Can be fixed by replacing base `QTreeWidget` of `SUIT_ShortcutTree` with `QTreeView`, or may be by applying some style sheet. -3. `SUIT_ShortcutMgr` introduces concept of module, but the first module class is `CAM_Module` is introduced along with `CAM_Application`, which is descendant of `SUIT_Application`. +1. Selection of `SUIT_ShortcutTree`' item shadows "modified" highlighter. Selection of module tab in `SUIT_ShortcutTabWidget` shadows "modified" highlighter. +2. `SUIT_ShortcutMgr` introduces concept of module, but the first module class is `CAM_Module` is introduced along with `CAM_Application`, which is descendant of `SUIT_Application`. diff --git a/src/SUIT/SUIT_ShortcutMgr.cxx b/src/SUIT/SUIT_ShortcutMgr.cxx index 95cede6d2..d22c05a64 100644 --- a/src/SUIT/SUIT_ShortcutMgr.cxx +++ b/src/SUIT/SUIT_ShortcutMgr.cxx @@ -25,6 +25,8 @@ #include "SUIT_Session.h" #include "SUIT_ResourceMgr.h" #include "SUIT_MessageBox.h" +#include "Tools/SUIT_extensions.h" +#include "Tools/SUIT_SentenceMatcher.h" #include #include @@ -37,16 +39,14 @@ #include #include #include -#include -#include -#include #include - - #include #include #include +#include + + const std::wstring SHORTCUT_MGR_LOG_PREFIX = L"SHORTCUT_MGR_DBG: "; bool ShCutDbg(const QString& theString) { @@ -83,26 +83,17 @@ static const QString TOKEN_SEPARATOR = QString("/"); static const QString META_ACTION_PREFIX = QString("#"); /** Prefix of names of shortcut setting sections in preference files. */ -static const QString SECTION_NAME_PREFIX = QString("shortcuts"); +static const QString SECTION_NAME_PREFIX = QString("shortcuts_vA1.0"); const QString DEFAULT_LANG = QString("en"); -const QStringList LANG_PRIORITY_LIST = QStringList({DEFAULT_LANG, "fr"}); const QString LANG_SECTION = QString("language"); static const QString SECTION_NAME_ACTION_ASSET_FILE_PATHS = QString("action_assets"); -/** - * Uncomment this, to start collecting all shortcuts and action assets (1), - * from instances of QtxActions, if a shortcut or action assets are absent in resource/asset files. - * - * (1) Set required language in the application settings and run features of interest. - * For all actions from these features, their assets will be dumped to appropriate places in dump files. - * - * Content of dump files is appended on every run. Files are located in "/shortcut_mgr_dev/". -*/ +/** Uncomment this to enable DevTools. */ // #define SHORTCUT_MGR_DEVTOOLS #ifdef SHORTCUT_MGR_DEVTOOLS #include @@ -117,10 +108,27 @@ static const QString SECTION_NAME_ACTION_ASSET_FILE_PATHS = QString("action_asse #include #endif // QT_NO_DOM -/*! \brief Generates XML files with appearing at runtime shortcuts, - using key sequences of QActions passed to the shortcut manager, - and JSON files with assets of QtxActions passed to the shortcut manager. - Content of these files can be easily copied to resource/asset files. */ +/*! \brief + Alleviates making actions, coded without taking into account SUIT_ShortcutMgr action requirements, dynamically assignable to shortcuts. + + Generates XML files with key sequences of actions, registered by the shortcut manager, + if these actions are anonymous or their shortcuts are absent in default preference files. + Generates JSON files with assets (using fieilds of action instances) of identified actions, registered by the shortcut manager, + if their assets are absent in assets files. + Content of these files can be easily copied to preference/asset files. + The files are dumped in "/shortcut_mgr_dev/". Content of dump files is appended on every run. + + HOW TO USE + 1) Run application and touch features of interest. + 2) Look into *actions_with_invalid_IDs.csv files. + 3) Identify where from those actions come from. + Also look for DevTools::SECTION_NAME_ACTION_IDS_TO_THROW_EXCEPTION and DevTools::SECTION_NAME_ACTION_TOOLTIPS_TO_THROW_EXCEPTION. + 4) Assign valid IDs to actions of interest in the code and rebuild. + 5) Delete all dump files. + 6) Go to apllication preferences and set language of interest (if these features are localized in that language). Run the features again. Repeat. + 7) Files *_assets*.json hold assets, composed using runtime data, of those identified actions, whose assets are absent in project asset files. + Files *_shortcuts.xml hold shortcuts of those identified actions, which are absent in default preference files. + */ class DevTools { private: @@ -177,6 +185,7 @@ public: } void collectAssets( + const SUIT_ShortcutModuleAssets& theModuleAssetsInResources, const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang, @@ -184,27 +193,37 @@ public: ) { if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) { QString actionID = SUIT_ShortcutMgr::makeActionID(SUIT_ShortcutMgr::ROOT_MODULE_ID, theInModuleActionID); - // { actionID, assets } [] - auto& moduleAssets = myAssetsOfMetaActions[theModuleID]; - auto& actionAssets = moduleAssets[actionID]; - actionAssets.myLangDependentAssets[theLang].myName = theAction->text(); - actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip(); + auto itModuleAssets = myAssetsOfMetaActions.find(theModuleID); + if (itModuleAssets == myAssetsOfMetaActions.end()) { + itModuleAssets = myAssetsOfMetaActions.emplace(theModuleID, SUIT_ShortcutModuleAssets::create(SUIT_ShortcutMgr::ROOT_MODULE_ID)).first; + } + auto& moduleAssets = itModuleAssets->second; + + auto actionAssets = moduleAssets->descendantItem(theInModuleActionID); + actionAssets->myLangDependentAssets[theLang].myName = theAction->text(); + actionAssets->myLangDependentAssets[theLang].myToolTip = theAction->statusTip(); const QString fileName = theModuleID + DevTools::ASSETS_OF_META_SUFFIX; - writeToJSONFile(fileName, actionID, actionAssets); + writeToJSONFile(fileName, *moduleAssets); } else { QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID); - // { actionID, assets } [] - auto& moduleAssets = myAssets[theModuleID]; - auto& actionAssets = moduleAssets[actionID]; - actionAssets.myLangDependentAssets[theLang].myName = theAction->text(); - actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip(); + auto itModuleAssets = myAssets.find(theModuleID); + if (itModuleAssets == myAssets.end()) { + itModuleAssets = myAssets.emplace(theModuleID, SUIT_ShortcutModuleAssets::create(theModuleID)).first; + } + auto& moduleAssets = itModuleAssets->second; + + moduleAssets->merge(theModuleAssetsInResources, true); + auto actionAssets = moduleAssets->descendantItem(theInModuleActionID, true); + actionAssets->myLangDependentAssets[theLang].myName = theAction->text(); + actionAssets->myLangDependentAssets[theLang].myToolTip = theAction->statusTip(); + const QString fileName = theModuleID + DevTools::ASSETS_SUFFIX; - writeToJSONFile(fileName, actionID, actionAssets); + writeToJSONFile(fileName, *moduleAssets); } } @@ -224,15 +243,16 @@ public: return; } - const QString lang = resMgr->stringValue(LANG_SECTION, LANG_SECTION); - if (lang.isEmpty()) - return; + const QString lang = SUIT_ShortcutMgr::currentLang(); + const auto moduleAndActionAssetsInResources = SUIT_ShortcutMgr::getActionAssetsFromResources(theAction->ID()); + const auto& moduleAssetsInResources = moduleAndActionAssetsInResources.first; + const auto& actionAssetsInResources = moduleAndActionAssetsInResources.second; - const auto& assetsInResources = SUIT_ShortcutMgr::getActionAssetsFromResources(theAction->ID()); - if (assetsInResources.first && assetsInResources.second.myLangDependentAssets.find(lang) != assetsInResources.second.myLangDependentAssets.end()) + if (actionAssetsInResources && actionAssetsInResources->myLangDependentAssets.find(lang) != actionAssetsInResources->myLangDependentAssets.end()) return; - collectAssets(moduleIDAndActionID.first, moduleIDAndActionID.second, lang, theAction); + if (moduleAssetsInResources) + collectAssets(*moduleAssetsInResources, moduleIDAndActionID.first, moduleIDAndActionID.second, lang, theAction); } } @@ -348,7 +368,7 @@ private: } /*! Appends new entries to content of dump files. */ - bool writeToJSONFile(const QString& theFileName, const QString& theActionID, const SUIT_ActionAssets& theAssets) + bool writeToJSONFile(const QString& theFileName, const SUIT_ShortcutModuleAssets& theModuleAssetsInResources) { const auto itFileAndDoc = myJSONFilesAndDocs.find(theFileName); if (itFileAndDoc == myJSONFilesAndDocs.end()) { @@ -393,14 +413,17 @@ private: QFile* const file = fileAndDoc.first; QJsonDocument* const document = fileAndDoc.second; - QJsonObject rootJSON = document->object(); - QJsonObject actionAssetsJSON = rootJSON[theActionID].toObject(); - SUIT_ActionAssets actionAssets; - actionAssets.fromJSON(actionAssetsJSON); - actionAssets.merge(theAssets, true /*theOverride*/); - actionAssets.toJSON(actionAssetsJSON); - rootJSON[theActionID] = actionAssetsJSON; - document->setObject(rootJSON); + const QString& moduleID = theModuleAssetsInResources.myModuleID; + + QJsonObject documentJSONObject = document->object(); + QJsonObject moduleAssetsJSONObject = documentJSONObject[moduleID].toObject(); + const auto dumpedModuleAssets = SUIT_ShortcutModuleAssets::create(moduleID); + dumpedModuleAssets->fromJSON(moduleAssetsJSONObject); + dumpedModuleAssets->merge(theModuleAssetsInResources, true /*theOverride*/); + dumpedModuleAssets->toJSON(moduleAssetsJSONObject); + + documentJSONObject[moduleID] = moduleAssetsJSONObject; + document->setObject(documentJSONObject); file->resize(0); QTextStream outstream(file); @@ -456,17 +479,40 @@ public: static DevTools* instance; static const QString XML_SECTION_TOKENS_SEPARATOR; + + /** Add such section to preference file to throw exceptions upon registration of actions with listed IDs. + * Makes it easier to find the code where actions are created. + * +
+ ... + + ... +
+ */ + static const QString SECTION_NAME_ACTION_IDS_TO_THROW_EXCEPTION; + + /** Add such section to preference file to throw exceptions upon registration of actions with listed tool tips. + * Makes it easier to find the code where actions are created. + * +
+ ... + + ... +
+ */ + static const QString SECTION_NAME_ACTION_TOOLTIPS_TO_THROW_EXCEPTION; + /** { moduleID, { inModuleActionID, keySequence }[] }[]. keySequence can be empty. */ std::map> myShortcuts; /** { moduleID, { inModuleActionID, keySequence }[] }[]. keySequence can be empty. */ std::map> myShortcutsOfMetaActions; - /** { moduleID, { actionID, assets }[] }[] */ - std::map> myAssets; + /** { moduleID, moduleAssets }[] */ + std::map> myAssets; - /** { moduleID, { actionID, assets }[] }[] */ - std::map> myAssetsOfMetaActions; + /** { moduleID, moduleAssets }[] */ + std::map> myAssetsOfMetaActions; #ifndef QT_NO_DOM // { filename, {file, domDoc} }[] @@ -485,6 +531,8 @@ public: /*static*/ const QString DevTools::SHORTCUTS_OF_META_SUFFIX = "_shortcuts_of_meta_actions"; /*static*/ const QString DevTools::ASSETS_SUFFIX = "_assets"; /*static*/ const QString DevTools::ASSETS_OF_META_SUFFIX = "_assets_of_meta_actions"; +/*static*/ const QString DevTools::SECTION_NAME_ACTION_IDS_TO_THROW_EXCEPTION = "SHORTCUT_MGR_DEVTOOLS_Action_IDs_to_throw_exception"; +/*static*/ const QString DevTools::SECTION_NAME_ACTION_TOOLTIPS_TO_THROW_EXCEPTION = "SHORTCUT_MGR_DEVTOOLS_Action_tooltips_to_throw_exception"; #endif // SHORTCUT_MGR_DEVTOOLS @@ -753,13 +801,86 @@ QString SUIT_ShortcutContainer::toString() const return text; } -/*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME = "name"; -/*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP = "tooltip"; +std::map>> SUIT_ShortcutContainer::merge( + const SUIT_ShortcutContainer& theOther, + bool theOverride, + bool theTreatAbsentIncomingAsDisabled +) { + std::map>> changesOfThis; + + const SUIT_ShortcutContainer copyOfThisBeforeMerge = *this; // TODO Get rid of whole container copying. -bool SUIT_ActionAssets::LangDependentAssets::fromJSON(const QJsonObject& theJsonObject) + for (const auto& shortcutsInversedOfOtherPair : theOther.myShortcutsInversed) { + const QString& moduleIDOther = shortcutsInversedOfOtherPair.first; + const auto& shortcutsInversedOther = shortcutsInversedOfOtherPair.second; + for (const auto& shortcutInversedOther : shortcutsInversedOther) { + const QString& inModuleActionIDOther = shortcutInversedOther.first; + const QKeySequence& keySequenceOther = shortcutInversedOther.second; + const QKeySequence& keySequenceThis = getKeySequence(moduleIDOther, inModuleActionIDOther); + if (theOverride) { + if (hasShortcut(moduleIDOther, inModuleActionIDOther) && keySequenceThis == keySequenceOther) { + continue; + } + else /* if this has no shortcut for the action or if this has a shortcut for the action, but the key sequence differs. */ { + const auto disabledActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, true); + //changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(keySequenceThis, keySequenceOther); + changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(copyOfThisBeforeMerge.getKeySequence(moduleIDOther, inModuleActionIDOther), keySequenceOther); + for (const auto& disabledActionOfThis : disabledActionsOfThis) { + changesOfThis[disabledActionOfThis.first][disabledActionOfThis.second] = std::pair(keySequenceOther, NO_KEYSEQUENCE); + } + } + } + else /* if (!theOverride) */ { + if (hasShortcut(moduleIDOther, inModuleActionIDOther)) + continue; + else { + const auto conflictingActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, false); + if (conflictingActionsOfThis.empty()) { + changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(NO_KEYSEQUENCE, keySequenceOther); + } + else /* if this has no shortcut for the action, but the incoming key sequence conflicts with others shortcuts. */ { + changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(NO_KEYSEQUENCE, NO_KEYSEQUENCE); + } + } + } + } + } + + if (theOverride && theTreatAbsentIncomingAsDisabled) { + // Disable existing shortcuts, if they are absent in theOther. + for (auto& shortcutsInversedPair : myShortcutsInversed) { + const QString& moduleID = shortcutsInversedPair.first; + auto& moduleShortcutsInversed = shortcutsInversedPair.second; + for (auto& inversedShortcut : moduleShortcutsInversed) { + if (theOther.hasShortcut(moduleID, inversedShortcut.first)) + continue; + + if (inversedShortcut.second.isEmpty()) + continue; // Existing shortcut is already disabled. + + auto itShortcutsPair = myShortcuts.find(moduleID); + if (itShortcutsPair == myShortcuts.end()) + continue; // The check is an overhead in an error-free designed class, but let be just in case. + + auto& moduleShortcuts = itShortcutsPair->second; + moduleShortcuts.erase(inversedShortcut.second); + changesOfThis[moduleID][inversedShortcut.first] = std::pair(inversedShortcut.second, NO_KEYSEQUENCE); + inversedShortcut.second = NO_KEYSEQUENCE; + } + } + } + + return changesOfThis; +} + + +/*static*/ const QString SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_NAME = "name"; +/*static*/ const QString SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_TOOLTIP = "tooltip"; + +bool SUIT_ShortcutAssets::LangDependentAssets::fromJSON(const QJsonObject& theJsonObject) { - myName = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME].toString(); - myToolTip = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP].toString(); + myName = theJsonObject[SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_NAME].toString(); + myToolTip = theJsonObject[SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_TOOLTIP].toString(); if (myName.isEmpty()) myName = myToolTip; @@ -767,56 +888,340 @@ bool SUIT_ActionAssets::LangDependentAssets::fromJSON(const QJsonObject& theJson return !myName.isEmpty(); } -void SUIT_ActionAssets::LangDependentAssets::toJSON(QJsonObject& oJsonObject) const +void SUIT_ShortcutAssets::LangDependentAssets::toJSON(QJsonObject& oJsonObject) const { - oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME] = myName; - oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP] = myToolTip; + if (!myName.isEmpty()) + oJsonObject[SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_NAME] = myName; + + if (!myToolTip.isEmpty()) + oJsonObject[SUIT_ShortcutAssets::LangDependentAssets::PROP_ID_TOOLTIP] = myToolTip; } -/*static*/ const QString SUIT_ActionAssets::STRUCT_ID = "SUIT_ActionAssets"; -/*static*/ const QString SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS = "langDependentAssets"; -/*static*/ const QString SUIT_ActionAssets::PROP_ID_ICON_PATH = "iconPath"; -bool SUIT_ActionAssets::fromJSON(const QJsonObject& theJsonObject) +/*static*/ const QString SUIT_ShortcutAssets::PROP_ID_LANG_DEPENDENT_ASSETS = "langDependentAssets"; +/*static*/ const QString SUIT_ShortcutAssets::PROP_ID_ICON_PATH = "iconPath"; +/*static*/ const QString SUIT_ShortcutAssets::PROP_ID_CHILDREN = "children"; + +SUIT_ShortcutAssets::SUIT_ShortcutAssets(const QString& theModuleID) +: myModuleID(theModuleID) { - myLangDependentAssets.clear(); + if (!SUIT_ShortcutMgr::isModuleIDValid(myModuleID)) + throw std::invalid_argument("SUIT_ShortcutAssets: invalid module ID \"" + myModuleID.toStdString() + "\"."); +} + +SUIT_ShortcutAssets::~SUIT_ShortcutAssets() +{} + +const std::map>& SUIT_ShortcutAssets::children() const +{ + return myChildren; +} + +std::shared_ptr SUIT_ShortcutAssets::findDescendantItem(const QString& theRelativeID) const +{ + QStringList tokens = SUIT_ShortcutMgr::splitIntoTokens(theRelativeID); + if (tokens.isEmpty()) + return std::shared_ptr(nullptr); // Without the check *this with casted-away constness is returned. + + std::shared_ptr descendant = std::const_pointer_cast(shared_from_this()); + while (!tokens.isEmpty()) { + const QString& token = tokens.front(); + + const auto itChild = descendant->myChildren.find(token); + if (itChild == descendant->myChildren.end()) + return std::shared_ptr(nullptr); - auto lda = SUIT_ActionAssets::LangDependentAssets(); - const auto& langToLdaJson = theJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS].toObject(); + descendant = itChild->second; + tokens.pop_front(); + } + return std::static_pointer_cast(descendant); +} + +std::shared_ptr SUIT_ShortcutAssets::descendantItem(const QString& theRelativeID, bool theIsAction) +{ + QStringList tokens = SUIT_ShortcutMgr::splitIntoTokens(theRelativeID); + if (tokens.isEmpty()) + return std::shared_ptr(nullptr); // Without the check return value type must be SUIT_ShortcutAssets*. + + std::shared_ptr descendant = std::const_pointer_cast(shared_from_this()); + while (!tokens.isEmpty()) { + const QString token = tokens.front(); + tokens.pop_front(); + + auto itChild = descendant->myChildren.find(token); + if (itChild == descendant->myChildren.end()) { + const bool childIsAction = tokens.isEmpty() ? theIsAction : false; // Make missing ancestors pure folders. + auto child = SUIT_ShortcutItemAssets::create(descendant, token, childIsAction); + if (!child) + return std::shared_ptr(nullptr); + + itChild = descendant->myChildren.emplace(token, std::move(child)).first; + } + descendant = itChild->second; + } + return std::static_pointer_cast(descendant); +} + +bool SUIT_ShortcutAssets::fromJSONOwnProps(const QJsonObject& theJsonObject, const std::set& theLangs) +{ + myLangDependentAssets.clear(); + auto lda = SUIT_ShortcutAssets::LangDependentAssets(); + const auto& langToLdaJson = theJsonObject[SUIT_ShortcutAssets::PROP_ID_LANG_DEPENDENT_ASSETS].toObject(); for (const QString& lang : langToLdaJson.keys()) { + if (!theLangs.empty() && theLangs.find(lang) == theLangs.end()) + continue; + if (!lda.fromJSON(langToLdaJson[lang].toObject())) continue; myLangDependentAssets[lang] = lda; } - myIconPath = theJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH].toString(); + myIconPath = theJsonObject[SUIT_ShortcutAssets::PROP_ID_ICON_PATH].toString(); + const bool otherPropsParsed = fromJSONOtherProps(theJsonObject); + + return !myLangDependentAssets.empty() || !myIconPath.isEmpty() || otherPropsParsed; +} + +bool SUIT_ShortcutAssets::fromJSON(const QJsonObject& theJsonObject, bool theParseDescendants, const std::set& theLangs) +{ + for (const auto& IDLTAndChild : myChildren) { + IDLTAndChild.second->myParent.reset(); + } + myChildren.clear(); + + const bool ownPropsParsed = fromJSONOwnProps(theJsonObject, theLangs); + + if (theParseDescendants) { + const auto& childrenJsonObject = theJsonObject[SUIT_ShortcutAssets::PROP_ID_CHILDREN].toObject(); + for (const QString& IDLastToken : childrenJsonObject.keys()) { + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(IDLastToken)) { + Warning("SUIT_ActionAssets::fromJSON: child assets with invalid IDLastToken \"" + IDLastToken + "\" have been encountered. *this is " + description()); + continue; + } + + auto child = SUIT_ShortcutItemAssets::create(shared_from_this(), IDLastToken, false /*theIsAction*/); + child->fromJSON(childrenJsonObject[IDLastToken].toObject(), true /*theParseDescendants*/, theLangs); + myChildren.emplace(IDLastToken, std::move(child)); + } + } + + return ownPropsParsed || !myChildren.empty(); +} + +bool SUIT_ShortcutAssets::fromJSON(const QJsonObject& theJsonObject, const QString& theRelativeID, const std::set& theLangs) +{ + for (const auto& IDLTAndChild : myChildren) { + IDLTAndChild.second->myParent.reset(); + } + myChildren.clear(); + + const bool ownPropsParsed = fromJSONOwnProps(theJsonObject, theLangs); + + QStringList tokens = SUIT_ShortcutMgr::splitIntoTokens(theRelativeID); + if (!tokens.isEmpty()) { + const QString token = tokens.front(); + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(token)) + Warning("SUIT_ActionAssets::fromJSON: child assets with invalid IDLastToken \"" + token + "\" is requested. *this is " + description()); + else { + const auto& childrenJsonObject = theJsonObject[SUIT_ShortcutAssets::PROP_ID_CHILDREN].toObject(); + const auto itChildJSONValue = childrenJsonObject.find(token); + if (itChildJSONValue != childrenJsonObject.end()) { + auto child = SUIT_ShortcutItemAssets::create(shared_from_this(), token, false /*theIsAction*/); + tokens.pop_front(); + child->fromJSON(itChildJSONValue->toObject(), SUIT_ShortcutMgr::joinIntoRelativeID(tokens), theLangs); + myChildren.emplace(token, std::move(child)); + } + } + } + + return ownPropsParsed || !myChildren.empty(); +} + +void SUIT_ShortcutAssets::toJSON(QJsonObject& oJsonObject) const +{ + if (!myLangDependentAssets.empty()) { + auto langDependentAssetsJSON = QJsonObject(); + for (const auto& langAndLDA : myLangDependentAssets) { + auto langDependentAssetsItemJSON = QJsonObject(); + langAndLDA.second.toJSON(langDependentAssetsItemJSON); + langDependentAssetsJSON[langAndLDA.first] = langDependentAssetsItemJSON; + } + oJsonObject[SUIT_ShortcutAssets::PROP_ID_LANG_DEPENDENT_ASSETS] = langDependentAssetsJSON; + } + + if (!myIconPath.isEmpty()) + oJsonObject[SUIT_ShortcutAssets::PROP_ID_ICON_PATH] = myIconPath; + + toJSONVirtual(oJsonObject); + + if (!myChildren.empty()) { + auto childrenJSON = QJsonObject(); + for (const auto& IDLastTokenAndChild : myChildren) { + auto childJSON = QJsonObject(); + IDLastTokenAndChild.second->toJSON(childJSON); + childrenJSON[IDLastTokenAndChild.first] = childJSON; + } + oJsonObject[SUIT_ShortcutItemAssets::PROP_ID_CHILDREN] = childrenJSON; + } +} + +void SUIT_ShortcutAssets::merge(const SUIT_ShortcutAssets& theOther, bool theOverride) +{ + if (&theOther == this) + return; + + if (myModuleID != theOther.myModuleID) { + Warning("SUIT_ShortcutAssets::merge: attempt to merge assets with different module ID."); + return; + } + + for (const auto& otherLangAndLDA : theOther.myLangDependentAssets) { + const QString& lang = otherLangAndLDA.first; + const auto& otherLDA = otherLangAndLDA.second; + auto& thisLDA = myLangDependentAssets[lang]; + + if (thisLDA.myName.isEmpty() || theOverride && !otherLDA.myName.isEmpty()) + thisLDA.myName = otherLDA.myName; + + if (thisLDA.myToolTip.isEmpty() || theOverride && !otherLDA.myToolTip.isEmpty()) + thisLDA.myToolTip = otherLDA.myToolTip; + } + + if (myIconPath.isEmpty() || theOverride && !theOther.myIconPath.isEmpty()) { + myIconPath = theOther.myIconPath; + if (!myIcon.isNull()) + loadIcon(true /*theReload*/); + } + + for (const auto& otherChildIDLTAndChild : theOther.myChildren) { + const QString& childIDLastToken = otherChildIDLTAndChild.first; + const SUIT_ShortcutItemAssets& otherChild = *(otherChildIDLTAndChild.second); + + const auto itChild = myChildren.find(childIDLastToken); + if (itChild == myChildren.end()) { + auto child = SUIT_ShortcutItemAssets::create(shared_from_this(), childIDLastToken, false /*theIsAction*/); + child->merge(otherChild, theOverride); + myChildren.emplace(childIDLastToken, std::move(child)); + } + else { + std::shared_ptr child = itChild->second; + child->merge(otherChild, theOverride); + } + } +} + +void SUIT_ShortcutAssets::merge(SUIT_ShortcutAssets&& theOther, bool theOverride) +{ + if (&theOther == this) + return; + + if (myModuleID != theOther.myModuleID) { + Warning("SUIT_ShortcutAssets::merge: attempt to merge assets with different module ID."); + return; + } + + for (auto itOtherLangAndLDA = theOther.myLangDependentAssets.begin(); itOtherLangAndLDA != theOther.myLangDependentAssets.end();) { + const QString& lang = itOtherLangAndLDA->first; + const auto& otherLDA = itOtherLangAndLDA->second; + + const auto itThisLDA = myLangDependentAssets.find(lang); + if (itThisLDA == myLangDependentAssets.end()) { + myLangDependentAssets.emplace(lang, std::move(otherLDA)); + itOtherLangAndLDA = theOther.myLangDependentAssets.erase(itOtherLangAndLDA); + continue; + } + + auto& thisLDA = itThisLDA->second; + + if (thisLDA.myName.isEmpty() || theOverride && !otherLDA.myName.isEmpty()) + thisLDA.myName = otherLDA.myName; + + if (thisLDA.myToolTip.isEmpty() || theOverride && !otherLDA.myToolTip.isEmpty()) + thisLDA.myToolTip = otherLDA.myToolTip; + + itOtherLangAndLDA++; + } + + if (myIconPath.isEmpty() || theOverride && !theOther.myIconPath.isEmpty()) { + myIconPath = std::move(theOther.myIconPath); + if (!myIcon.isNull()) { + if (theOther.myIcon.isNull()) + loadIcon(true /*theReload*/); + else + myIcon = std::move(theOther.myIcon); + } + } + + for (auto itOtherChildIDLTAndChild = theOther.myChildren.begin(); itOtherChildIDLTAndChild != theOther.myChildren.end(); ) { + const QString& childIDLastToken = itOtherChildIDLTAndChild->first; + const auto& otherChild = itOtherChildIDLTAndChild->second; + + const auto itChild = myChildren.find(childIDLastToken); + if (itChild == myChildren.end()) { + auto child = otherChild; // Acquire ownership. + child->myParent = shared_from_this(); + itOtherChildIDLTAndChild = theOther.myChildren.erase(itOtherChildIDLTAndChild); + myChildren.emplace(childIDLastToken, std::move(child)); + } + else { + std::shared_ptr child = itChild->second; + child->merge(*otherChild, theOverride); + itOtherChildIDLTAndChild++; + } + } +} - return !myLangDependentAssets.empty(); +void SUIT_ShortcutAssets::loadIcon(bool theReload) +{ + if (!myIconPath.isEmpty() && (myIcon.isNull() || theReload)) + myIcon = QIcon(::SUIT_tools::substituteVars(myIconPath)); +} + +void SUIT_ShortcutAssets::forEachDescendant(const std::function& theFunc) const +{ + for (const auto& childIDLTAndChild : myChildren) { + SUIT_ShortcutItemAssets& child = *(childIDLTAndChild.second); + theFunc(child); + child.forEachDescendant(theFunc); + } } -void SUIT_ActionAssets::toJSON(QJsonObject& oJsonObject) const +void SUIT_ShortcutAssets::forEachDescendant(const std::function& theFunc) const { - auto langDependentAssetsJSON = QJsonObject(); + for (const auto& childIDLTAndChild : myChildren) { + const SUIT_ShortcutItemAssets& child = *(childIDLTAndChild.second); + theFunc(child); + child.forEachDescendant(theFunc); + } +} - auto langDependentAssetsItemJSON = QJsonObject(); - for (const auto& langAndLDA : myLangDependentAssets) { - langAndLDA.second.toJSON(langDependentAssetsItemJSON); - langDependentAssetsJSON[langAndLDA.first] = langDependentAssetsItemJSON; +void SUIT_ShortcutAssets::forEachDescendant(const std::function)>& theFunc) const +{ + for (const auto& childIDLTAndChild : myChildren) { + const std::shared_ptr& child = childIDLTAndChild.second; + theFunc(child); + child->forEachDescendant(theFunc); } - oJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS] = langDependentAssetsJSON; +} - oJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH] = myIconPath; +void SUIT_ShortcutAssets::forEachDescendant(const std::function)>& theFunc) const +{ + for (const auto& childIDLTAndChild : myChildren) { + const std::shared_ptr& child = childIDLTAndChild.second; + theFunc(child); + child->forEachDescendant(theFunc); + } } -QString SUIT_ActionAssets::toString() const +QString SUIT_ShortcutAssets::toString() const { QJsonObject jsonObject; toJSON(jsonObject); return QString::fromStdString(QJsonDocument(jsonObject).toJson(QJsonDocument::Indented).toStdString()); } -QStringList SUIT_ActionAssets::getLangs() const +QStringList SUIT_ShortcutAssets::getLangs() const { QStringList langs; @@ -827,7 +1232,7 @@ QStringList SUIT_ActionAssets::getLangs() const return langs; } -void SUIT_ActionAssets::clearAllLangsExcept(const QString& theLang) +void SUIT_ShortcutAssets::clearAllLangsExcept(const QString& theLang) { for (auto it = myLangDependentAssets.begin(); it != myLangDependentAssets.end();) { if (it->first == theLang) @@ -837,103 +1242,275 @@ void SUIT_ActionAssets::clearAllLangsExcept(const QString& theLang) } } -void SUIT_ActionAssets::merge(const SUIT_ActionAssets& theOther, bool theOverride) +const SUIT_ShortcutAssets::LangDependentAssets* SUIT_ShortcutAssets::bestLangDependentAssets(QString theLang) const { - for (const auto& otherLangAndLDA : theOther.myLangDependentAssets) { - const QString& lang = otherLangAndLDA.first; - const auto& otherLDA = otherLangAndLDA.second; - auto& thisLDA = myLangDependentAssets[lang]; + if (theLang.isEmpty()) + theLang = SUIT_ShortcutMgr::currentLang(); - if (thisLDA.myName.isEmpty() || theOverride && !otherLDA.myName.isEmpty()) - thisLDA.myName = otherLDA.myName; + auto langPriorityList = QStringList({DEFAULT_LANG}); + langPriorityList.push_front(theLang); + langPriorityList.removeDuplicates(); - if (thisLDA.myToolTip.isEmpty() || theOverride && !otherLDA.myToolTip.isEmpty()) - thisLDA.myToolTip = otherLDA.myToolTip; + for (const QString& lang : langPriorityList) { + const auto it = myLangDependentAssets.find(lang); + if (it != myLangDependentAssets.end()) + return &(it->second); } - if (theOverride) - myIconPath = theOther.myIconPath; + // Return any other LDA, if exist. + if (!myLangDependentAssets.empty()) + return &(myLangDependentAssets.begin()->second); + + return nullptr; } -std::map>> SUIT_ShortcutContainer::merge( - const SUIT_ShortcutContainer& theOther, - bool theOverride, - bool theTreatAbsentIncomingAsDisabled -) { - std::map>> changesOfThis; +const QString& SUIT_ShortcutAssets::bestToolTip(const QString& theLang) const +{ + static const QString dummyString = QString(); + const auto bestLDA = bestLangDependentAssets(theLang); + if (bestLDA) + return bestLDA->myToolTip; + else + return dummyString; +} - const SUIT_ShortcutContainer copyOfThisBeforeMerge = *this; // TODO Get rid of whole container copying. - for (const auto& shortcutsInversedOfOtherPair : theOther.myShortcutsInversed) { - const QString& moduleIDOther = shortcutsInversedOfOtherPair.first; - const auto& shortcutsInversedOther = shortcutsInversedOfOtherPair.second; - for (const auto& shortcutInversedOther : shortcutsInversedOther) { - const QString& inModuleActionIDOther = shortcutInversedOther.first; - const QKeySequence& keySequenceOther = shortcutInversedOther.second; - const QKeySequence& keySequenceThis = getKeySequence(moduleIDOther, inModuleActionIDOther); - if (theOverride) { - if (hasShortcut(moduleIDOther, inModuleActionIDOther) && keySequenceThis == keySequenceOther) { - continue; - } - else /* if this has no shortcut for the action or if this has a shortcut for the action, but the key sequence differs. */ { - const auto disabledActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, true); - //changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(keySequenceThis, keySequenceOther); - changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(copyOfThisBeforeMerge.getKeySequence(moduleIDOther, inModuleActionIDOther), keySequenceOther); - for (const auto& disabledActionOfThis : disabledActionsOfThis) { - changesOfThis[disabledActionOfThis.first][disabledActionOfThis.second] = std::pair(keySequenceOther, NO_KEYSEQUENCE); - } - } +SUIT_ShortcutModuleAssets::SUIT_ShortcutModuleAssets(const QString& theModuleID) +: SUIT_ShortcutAssets(theModuleID) +{} + +/*static*/ std::shared_ptr SUIT_ShortcutModuleAssets::create(const QString& theModuleID) +{ + if (!SUIT_ShortcutMgr::isModuleIDValid(theModuleID)) + return std::shared_ptr(nullptr); + + return std::shared_ptr(new SUIT_ShortcutModuleAssets(theModuleID)); +} + +const QString& SUIT_ShortcutModuleAssets::bestName(const QString& theLang) const +{ + const auto bestLDA = bestLangDependentAssets(theLang); + if (bestLDA) + return bestLDA->myName; + else + return myModuleID; +} + +QString SUIT_ShortcutModuleAssets::description() const +{ + return "SUIT_ShortcutModuleAssets \"" + myModuleID + "\"."; +} + + +/*static*/ void SUIT_ShortcutItemAssets::loadDefaultIcons() +{ + SUIT_ShortcutItemAssets::DEFAUT_ICON_ACTION = QIcon(":/resources/default_action_icon.svg"); + SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER = QIcon(":/resources/default_folder_icon.svg"); + SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER_ACTION = QIcon(":/resources/default_folder_action_icon.svg"); +} + +/*static*/ const QString SUIT_ShortcutItemAssets::PROP_ID_IS_ACTION = "isAction"; +/*static*/ QIcon SUIT_ShortcutItemAssets::DEFAUT_ICON_ACTION = QIcon(); +/*static*/ QIcon SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER = QIcon(); +/*static*/ QIcon SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER_ACTION = QIcon(); + +SUIT_ShortcutItemAssets::SUIT_ShortcutItemAssets(std::shared_ptr theModule, const QString& theIDLastToken, bool theIsAction) +: SUIT_ShortcutAssets(theModule->myModuleID), myParent(theModule), myIsAction(theIsAction), myIDLastToken(theIDLastToken), myInModuleID(theIDLastToken), myDepth(myInModuleID.split(TOKEN_SEPARATOR).length()) +{} + +SUIT_ShortcutItemAssets::SUIT_ShortcutItemAssets(std::shared_ptr theParentItem, const QString& theIDLastToken, bool theIsAction) +: SUIT_ShortcutAssets(theParentItem->myModuleID), myParent(theParentItem), myIsAction(theIsAction), + myIDLastToken(theIDLastToken), myInModuleID(theParentItem->myInModuleID + TOKEN_SEPARATOR + theIDLastToken), myDepth(SUIT_ShortcutMgr::splitIntoTokens(myInModuleID).length()) +{} + +/*static*/ std::shared_ptr SUIT_ShortcutItemAssets::create(std::shared_ptr theParentItemOrModule, const QString& theIDLastToken, bool theIsAction) +{ + auto res = std::shared_ptr(nullptr); + + if (!theParentItemOrModule) + return res; + + switch (theParentItemOrModule->type()) { + case SUIT_ShortcutAssets::Type::Module: + { + const auto moduleAssets = std::static_pointer_cast(theParentItemOrModule); + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(theIDLastToken)) { + Warning(QString("SUIT_ShortcutItemAssets::create: invalid inModuleID (last token of ID of root item) \"") + theIDLastToken + "\". Parent is module \"" + moduleAssets->myModuleID + "\"."); + return res; } - else /* if (!theOverride) */ { - if (hasShortcut(moduleIDOther, inModuleActionIDOther)) - continue; - else { - const auto conflictingActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, false); - if (conflictingActionsOfThis.empty()) { - changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(NO_KEYSEQUENCE, keySequenceOther); - } - else /* if this has no shortcut for the action, but the incoming key sequence conflicts with others shortcuts. */ { - changesOfThis[moduleIDOther][inModuleActionIDOther] = std::pair(NO_KEYSEQUENCE, NO_KEYSEQUENCE); - } - } + + res.reset(new SUIT_ShortcutItemAssets(moduleAssets, theIDLastToken, theIsAction)); + return res; + }; + case SUIT_ShortcutAssets::Type::Item: + { + const auto itemAssets = std::static_pointer_cast(theParentItemOrModule); + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(theIDLastToken)) { + Warning(QString("SUIT_ShortcutItemAssets::create: invalid last token of ID \"") + theIDLastToken + "\". Parent is item \"" + itemAssets->actionID() + "\"."); + return res; } + + res.reset(new SUIT_ShortcutItemAssets(itemAssets, theIDLastToken, theIsAction)); + return res; + }; + default: + return res; + } +} + +int SUIT_ShortcutItemAssets::depth() const +{ + return myDepth; +} + +void SUIT_ShortcutItemAssets::merge(const SUIT_ShortcutItemAssets& theOther, bool theOverride) +{ + if (&theOther == this) + return; + + if (myInModuleID != theOther.myInModuleID) { + Warning("SUIT_ShortcutItemAssets::merge: attempt to merge assets with different IDs: \"" + theOther.myModuleID + TOKEN_SEPARATOR + theOther.myInModuleID + "\" into \"" + myModuleID + TOKEN_SEPARATOR + myInModuleID + "\"."); + return; + } + + SUIT_ShortcutAssets::merge(theOther, theOverride); + + myIsAction = myIsAction || theOther.myIsAction; +} + +void SUIT_ShortcutItemAssets::merge(SUIT_ShortcutItemAssets&& theOther, bool theOverride) +{ + if (&theOther == this) + return; + + if (myInModuleID != theOther.myInModuleID) { + Warning("SUIT_ShortcutItemAssets::merge: attempt to merge assets with different IDs: \"" + theOther.myModuleID + TOKEN_SEPARATOR + theOther.myInModuleID + "\" into \"" + myModuleID + TOKEN_SEPARATOR + myInModuleID + "\"."); + return; + } + + SUIT_ShortcutAssets::merge(theOther, theOverride); + + myIsAction = myIsAction || theOther.myIsAction; +} + +bool SUIT_ShortcutItemAssets::fromJSONOtherProps(const QJsonObject& theJsonObject) +{ + myIsAction = theJsonObject[SUIT_ShortcutItemAssets::PROP_ID_IS_ACTION].toBool(true); + return !myIsAction; +} + +void SUIT_ShortcutItemAssets::toJSONVirtual(QJsonObject& oJsonObject) const +{ + if (!myIsAction) + oJsonObject[SUIT_ShortcutItemAssets::PROP_ID_IS_ACTION] = false; +} + +const QString& SUIT_ShortcutItemAssets::bestName(const QString& theLang) const +{ + const auto bestLDA = bestLangDependentAssets(theLang); + if (bestLDA) + return bestLDA->myName; + else + return myIDLastToken; +} + +const QString& SUIT_ShortcutItemAssets::bestPath(QString theLang) const +{ + if (theLang.isEmpty()) + theLang = SUIT_ShortcutMgr::currentLang(); + + auto itBestPath = myBestPaths.find(theLang); + if (itBestPath != myBestPaths.end()) + return itBestPath->second; + + QString path = bestName(theLang); + std::shared_ptr ancestor = parent(); + int ancestorDepth = depth() - 1; + while (ancestor && ancestor->type() == SUIT_ShortcutAssets::Type::Item) { + const auto ancestorItem = std::static_pointer_cast(ancestor); + path = ancestorItem->bestName(theLang) + TOKEN_SEPARATOR + path; + ancestor = ancestorItem->parent(); + ancestorDepth--; + } + + if (ancestorDepth > 0) { + Warning(QString("SUIT_ShortcutItemAssets::bestPath: instance \"") + actionID() + "\" is dangling at depth " + QString::number(ancestorDepth) + ". "); + auto tokens = SUIT_ShortcutMgr::splitIntoTokens(myInModuleID); + for (int i = ancestorDepth; i > 0; i--) { + path = tokens[i] + TOKEN_SEPARATOR + path; } } - if (theOverride && theTreatAbsentIncomingAsDisabled) { - // Disable existing shortcuts, if they are absent in theOther. - for (auto& shortcutsInversedPair : myShortcutsInversed) { - const QString& moduleID = shortcutsInversedPair.first; - auto& moduleShortcutsInversed = shortcutsInversedPair.second; - for (auto& inversedShortcut : moduleShortcutsInversed) { - if (theOther.hasShortcut(moduleID, inversedShortcut.first)) - continue; + itBestPath = myBestPaths.emplace(theLang, path).first; + return itBestPath->second; +} - if (inversedShortcut.second.isEmpty()) - continue; // Existing shortcut is already disabled. +QString SUIT_ShortcutItemAssets::description() const +{ + return QString("SUIT_ShortcutItemAssets ") + (myIsAction ? "" : "(not action)") + " \"" + actionID() + "\"."; +} - auto itShortcutsPair = myShortcuts.find(moduleID); - if (itShortcutsPair == myShortcuts.end()) - continue; // The check is an overhead in an error-free designed class, but let be just in case. +bool SUIT_ShortcutItemAssets::isAction() const +{ + return myIsAction; +} - auto& moduleShortcuts = itShortcutsPair->second; - moduleShortcuts.erase(inversedShortcut.second); - changesOfThis[moduleID][inversedShortcut.first] = std::pair(inversedShortcut.second, NO_KEYSEQUENCE); - inversedShortcut.second = NO_KEYSEQUENCE; - } +bool SUIT_ShortcutItemAssets::isFolder() const +{ + return !myIsAction || !children().empty(); +} + +std::shared_ptr SUIT_ShortcutItemAssets::parent() const +{ + return myParent.lock(); +} + +std::shared_ptr SUIT_ShortcutItemAssets::module() const +{ + std::shared_ptr ancestor = parent(); + while (ancestor) { + if (ancestor->type() == SUIT_ShortcutAssets::Type::Module) + return std::static_pointer_cast(ancestor); + else if (ancestor->type() == SUIT_ShortcutAssets::Type::Item) + ancestor = std::static_pointer_cast(ancestor)->parent(); + else { + Warning("SUIT_ShortcutItemAssets::module: unexpected SUIT_ShortcutAssets::Type has been encountered. Fix the method!"); + return std::shared_ptr(nullptr); } } + return std::shared_ptr(nullptr); +} - return changesOfThis; +QString SUIT_ShortcutItemAssets::actionID() const +{ + return myModuleID + TOKEN_SEPARATOR + myInModuleID; +} + +const QIcon& SUIT_ShortcutItemAssets::icon() const +{ + if (myIcon.isNull()) { + if (myIsAction) { + if (children().empty()) + return SUIT_ShortcutItemAssets::DEFAUT_ICON_ACTION; + else + return SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER_ACTION; + } + else + return SUIT_ShortcutItemAssets::DEFAUT_ICON_FOLDER; + } + else + return myIcon; } SUIT_ShortcutMgr* SUIT_ShortcutMgr::myShortcutMgr = nullptr; SUIT_ShortcutMgr::SUIT_ShortcutMgr() -: QObject(), myActiveModuleIDs({SUIT_ShortcutMgr::ROOT_MODULE_ID}) +: QObject(), myActiveModuleIDs({SUIT_ShortcutMgr::ROOT_MODULE_ID}), myAssetsLoaded(false) { + Q_INIT_RESOURCE( SUIT ); qApp->installEventFilter( this ); + SUIT_ShortcutItemAssets::loadDefaultIcons(); } SUIT_ShortcutMgr::~SUIT_ShortcutMgr() @@ -944,8 +1521,11 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() /*static*/ void SUIT_ShortcutMgr::Init() { if( myShortcutMgr == nullptr) { + ShCutDbg("SUIT_ShortcutMgr initialization has started."); myShortcutMgr = new SUIT_ShortcutMgr(); + myShortcutMgr->setAssetsFromResources(); myShortcutMgr->setShortcutsFromPreferences(); + ShCutDbg("SUIT_ShortcutMgr initialization has finished."); } } @@ -992,17 +1572,31 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() return true; } +/*static*/ bool SUIT_ShortcutMgr::isInModuleIDTokenValid(const QString& theInModuleIDToken) +{ + if (theInModuleIDToken.contains(TOKEN_SEPARATOR)) + return false; + + const QString simplifiedToken = theInModuleIDToken.simplified(); + return !simplifiedToken.isEmpty() && + simplifiedToken == theInModuleIDToken && + simplifiedToken != META_ACTION_PREFIX; +} + +/*static*/ bool SUIT_ShortcutMgr::isInModuleIDTokenMeta(const QString& theInModuleIDToken) +{ + return theInModuleIDToken.startsWith(META_ACTION_PREFIX); +} + /*static*/ bool SUIT_ShortcutMgr::isInModuleActionIDValid(const QString& theInModuleActionID) { - QStringList tokens = theInModuleActionID.split(TOKEN_SEPARATOR); - for (QStringList::size_type i = 0; i < tokens.length(); i++) { - const QString simplifiedToken = tokens[i].simplified(); - if ( - simplifiedToken.isEmpty() || - simplifiedToken != tokens[i] || - i == 0 && simplifiedToken == META_ACTION_PREFIX || - i != 0 && simplifiedToken.startsWith(META_ACTION_PREFIX) - ) + const QStringList tokens = SUIT_ShortcutMgr::splitIntoTokens(theInModuleActionID); + + if (tokens.isEmpty()) + return false; + + for (const QString& token : tokens) { + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(token)) return false; } return true; @@ -1010,7 +1604,11 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() /*static*/ bool SUIT_ShortcutMgr::isInModuleMetaActionID(const QString& theInModuleActionID) { - return theInModuleActionID.startsWith(META_ACTION_PREFIX); + const QStringList tokens = SUIT_ShortcutMgr::splitIntoTokens(theInModuleActionID); + if (tokens.isEmpty()) + return false; + + return SUIT_ShortcutMgr::isInModuleIDTokenMeta(tokens.back()); } /*static*/ std::pair SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(const QString& theActionID) @@ -1027,14 +1625,8 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() res.first = tokens[0]; tokens.pop_front(); - for (QStringList::size_type i = 0; i < tokens.length(); i++) { - const QString simplifiedToken = tokens[i].simplified(); - if ( - simplifiedToken.isEmpty() || - simplifiedToken != tokens[i] || - i == 0 && simplifiedToken == META_ACTION_PREFIX || - i != 0 && simplifiedToken.startsWith(META_ACTION_PREFIX) - ) + for (const QString& token : tokens) { + if (!SUIT_ShortcutMgr::isInModuleIDTokenValid(token)) return std::pair(); } res.second = tokens.join(TOKEN_SEPARATOR); @@ -1042,6 +1634,19 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() return res; } +/*static*/ QStringList SUIT_ShortcutMgr::splitIntoTokens(const QString& theRelativeID) +{ + if (theRelativeID.isEmpty()) + return QStringList(); + + return theRelativeID.split(TOKEN_SEPARATOR); +} + +/*static*/ QString SUIT_ShortcutMgr::joinIntoRelativeID(const QStringList& theTokens) +{ + return theTokens.join(TOKEN_SEPARATOR); +} + /*static*/ bool SUIT_ShortcutMgr::isActionIDValid(const QString& theActionID) { return !SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID).second.isEmpty(); @@ -1060,7 +1665,7 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() /*static*/ void SUIT_ShortcutMgr::fillContainerFromPreferences(SUIT_ShortcutContainer& theContainer, bool theDefaultOnly) { - ShCutDbg() && ShCutDbg("Retrieving preferences from resources."); + ShCutDbg() && ShCutDbg(QString("SUIT_ShortcutMgr::fillContainerFromPreferences(theContainer, theDefaultOnly = ") + (theDefaultOnly ? "true" : "false") + ") started."); SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { @@ -1112,7 +1717,7 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() resMgr->value(sectionName, inModuleActionID, keySequenceString); const auto keySequence = SUIT_ShortcutMgr::toKeySequenceIfValid(keySequenceString); - ShCutDbg() && ShCutDbg("Shortcut discovered: \"" + moduleID + "\"\t\"" + inModuleActionID + "\"\t\"" + keySequenceString + "\"."); + ShCutDbg() && ShCutDbg("Shortcut parsed: \"" + moduleID + "\"\t\"" + inModuleActionID + "\"\t\"" + keySequenceString + "\"."); if ( !SUIT_ShortcutMgr::isInModuleActionIDValid(inModuleActionID) || @@ -1201,100 +1806,95 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr() resMgr->setWorkingMode(resMgrWorkingModeBefore); ShCutDbg() && ShCutDbg("theContainer holds following shortcuts:\n" + theContainer.toString()); + ShCutDbg() && ShCutDbg(QString("SUIT_ShortcutMgr::fillContainerFromPreferences(theContainer, theDefaultOnly = ") + (theDefaultOnly ? "true" : "false") + ") finished."); } -QString substituteBashVars(const QString& theString) +/*static*/ std::pair, std::shared_ptr> +SUIT_ShortcutMgr::getActionAssetsFromResources(const QString& theActionID, const std::set& theLangs) { - QString res = theString; - const auto env = QProcessEnvironment::systemEnvironment(); - int pos = 0; - QRegExp rx("\\$\\{([^\\}]+)\\}"); // Match substrings enclosed by "${" and "}". - rx.setMinimal(true); // Set search to non-greedy. - while((pos = rx.indexIn(res, pos)) != -1) { - QString capture = rx.cap(1); - QString subst = env.value(capture); - ShCutDbg("capture = " + capture); - ShCutDbg("subst = " + subst); - res.replace("${" + capture + "}", subst); - pos += rx.matchedLength(); - } - return res; -} + ShCutDbg("SUIT_ShortcutMgr::getActionAssetsFromResources(\"" + theActionID + "\") START"); -QString substitutePowerShellVars(const QString& theString) -{ - QString res = theString; - int pos = 0; - QRegExp rx("%([^%]+)%"); // Match substrings enclosed by "%". - rx.setMinimal(true); // Set search to non-greedy. - while((pos = rx.indexIn(res, pos)) != -1) { - QString capture = rx.cap(1); - QString subst = Qtx::getenv(capture.toUtf8().constData()); - ShCutDbg("capture = " + capture); - ShCutDbg("subst = " + subst); - res.replace("%" + capture + "%", subst); - pos += rx.matchedLength(); + SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); + if (!resMgr) { + Warning("SUIT_ShortcutMgr::getActionAssetsFromResources: can't retrieve resource manager!"); + return std::pair, std::shared_ptr>(nullptr, nullptr); } - return res; -} -QString substituteVars(const QString& theString) -{ - QString str = substituteBashVars(theString); - return substitutePowerShellVars(str); -} + const auto splittedActionID = splitIntoModuleIDAndInModuleID(theActionID); + if (splittedActionID.second.isEmpty()) { + Warning(QString("SUIT_ShortcutMgr::getActionAssetsFromResources: \"") + theActionID + "\" is invalid."); + return std::pair, std::shared_ptr>(nullptr, nullptr); + } -/*static*/ std::pair SUIT_ShortcutMgr::getActionAssetsFromResources(const QString& theActionID) -{ - auto res = std::pair(false, SUIT_ActionAssets()); + const QString& moduleID = SUIT_ShortcutMgr::isInModuleMetaActionID(splittedActionID.second) ? SUIT_ShortcutMgr::ROOT_MODULE_ID : splittedActionID.first; + const QString& inModuleID = splittedActionID.second; - SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); - if (!resMgr) { - Warning("SUIT_ShortcutMgr can't retrieve resource manager!"); - return res; - } + std::shared_ptr combinedModuleAssets(nullptr); QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS); +#ifdef SHORTCUT_MGR_DBG + ShCutDbg("Asset file paths: " + actionAssetFilePaths.join(", ") + "."); +#endif + for (const QString& actionAssetFilePath : actionAssetFilePaths) { - const QString path = substituteVars(actionAssetFilePath); + const QString path = ::SUIT_tools::substituteVars(actionAssetFilePath); + ShCutDbg("Parsing asset file \"" + path + "\"."); QFile actionAssetFile(path); if (!actionAssetFile.open(QIODevice::ReadOnly)) { - Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!"); + Warning("SUIT_ShortcutMgr::getActionAssetsFromResources: can't open action asset file \"" + path + "\"!"); continue; } QJsonParseError jsonError; QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError); actionAssetFile.close(); - if(jsonError.error != QJsonParseError::NoError) { - Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!"); + if (jsonError.error != QJsonParseError::NoError) { + Warning("SUIT_ShortcutMgr::getActionAssetsFromResources: error during parsing of action asset file \"" + path + "\"!"); continue; } - if(!document.isObject()) { - Warning("SUIT_ShortcutMgr: empty action asset file \"" + path + "\"!"); + if (!document.isObject()) { + Warning("SUIT_ShortcutMgr::getActionAssetsFromResources: invalid asset file \"" + path + "\"!"); continue; } - QJsonObject object = document.object(); - if (object.keys().indexOf(theActionID) == -1) + QJsonObject documentJSONObject = document.object(); + const auto itModuleJSONValue = documentJSONObject.find(moduleID); + if (itModuleJSONValue == documentJSONObject.end()) continue; - SUIT_ActionAssets actionAssets; - if (!actionAssets.fromJSON(object[theActionID].toObject())) { - ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + theActionID + "\"."); + if (!itModuleJSONValue->isObject()) { + ShCutDbg("SUIT_ShortcutMgr::getActionAssetsFromResources: file \"" + path + "\" contains invalid value with module ID \"" + moduleID + "\"."); continue; } - res.second.merge(actionAssets, true); + if (!combinedModuleAssets) + combinedModuleAssets = SUIT_ShortcutModuleAssets::create(moduleID); + + const auto fileModuleAssets = SUIT_ShortcutModuleAssets::create(moduleID); + const bool somethingUsefulIsParsed = fileModuleAssets->fromJSON(itModuleJSONValue->toObject(), inModuleID, theLangs); + + if (!somethingUsefulIsParsed) + continue; + + combinedModuleAssets->merge(*fileModuleAssets, true); } - res.first = true; - return res; + if (combinedModuleAssets) { +#ifdef SHORTCUT_MGR_DBG + ShCutDbg("SUIT_ShortcutMgr::getActionAssetsFromResources: module assets " + combinedModuleAssets->toString()); + ShCutDbg("SUIT_ShortcutMgr::getActionAssetsFromResources(\"" + theActionID + "\") END\n"); +#endif + return std::pair, std::shared_ptr>(combinedModuleAssets, combinedModuleAssets->findDescendantItem(inModuleID)); + } + else { + ShCutDbg("SUIT_ShortcutMgr::getActionAssetsFromResources(\"" + theActionID + "\") END\n"); + return std::pair, std::shared_ptr>(nullptr, nullptr); + } } -/*static*/ QString SUIT_ShortcutMgr::getLang() +/*static*/ QString SUIT_ShortcutMgr::currentLang() { SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { @@ -1308,6 +1908,22 @@ QString substituteVars(const QString& theString) void SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAction) { +#ifdef SHORTCUT_MGR_DEVTOOLS + SUIT_ResourceMgr* const resMgr = SUIT_Session::session()->resourceMgr(); + if (!resMgr) { + Warning("SHORTCUT_MGR_DEVTOOLS: can't retrieve resource manager to check throw lists."); + } + else { + const auto actionIDsToThrow = resMgr->parameters(DevTools::SECTION_NAME_ACTION_IDS_TO_THROW_EXCEPTION); + if (actionIDsToThrow.contains(theActionID)) + throw std::invalid_argument(QString("SHORTCUT_MGR_DEVTOOLS: action with ID \"" + theActionID + "\" from throw list has been registered.").toStdString()); + + const auto actionToolTipsToThrow = resMgr->parameters(DevTools::SECTION_NAME_ACTION_TOOLTIPS_TO_THROW_EXCEPTION); + if (actionToolTipsToThrow.contains(theAction->toolTip())) + throw std::invalid_argument(QString("SHORTCUT_MGR_DEVTOOLS: action with tool tip \"" + theAction->toolTip() + "\" from throw list has been registered.").toStdString()); + } +#endif //SHORTCUT_MGR_DEVTOOLS + const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(theActionID); const QString& moduleID = moduleIDAndActionID.first; const QString& inModuleActionID = moduleIDAndActionID.second; @@ -1319,7 +1935,7 @@ void SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAc } { // If an action with the same memory address was registered earlier, - // clear all data about it to start registering procedure from scratch. + // clear all data about it to start registration from scratch. auto itPreviousModuleAndActionID = myActionIDs.find(theAction); if (itPreviousModuleAndActionID != myActionIDs.end()) { // Clear the data from myActions. @@ -1359,19 +1975,43 @@ void SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAc connect(theAction, SIGNAL(destroyed(QObject*)), this, SLOT (onActionDestroyed(QObject*))); + // Check if assets are provided. + if (myAssetsLoaded) { + const auto& actionAssets = getActionAssets(moduleID, inModuleActionID, true /*theTryToCreateRuntimeAssetsIfAbsent*/); + if (!actionAssets) + Warning("Assets of action \"" + moduleID + TOKEN_SEPARATOR + inModuleActionID + "\" are not provided in asset files and could not be created using runtime data."); + } +#ifdef SHORTCUT_MGR_DBG + else + ShCutDbg("Action \"" + moduleID + TOKEN_SEPARATOR + inModuleActionID + "\" has been registered, but ShortcutMgr has not yet loaded asset files."); +#endif //SHORTCUT_MGR_DBG + + // Assign key sequence from preferences. if (myShortcutContainer.hasShortcut(moduleID, inModuleActionID)) { const QKeySequence& keySequence = getKeySequence(moduleID, inModuleActionID); theAction->setShortcut(keySequence); + // Do not log/warn if action arrived with different key sequence. There are to many of them. } - else { - ShCutDbg( - "Action with ID \"" + + else { // Unbind any key sequence, if it was bound outside of the class and interferes with other shortcuts. + const QKeySequence actionKeySequence = theAction->shortcut(); + auto conflicts = myShortcutContainer.setShortcut(moduleID, inModuleActionID, actionKeySequence, false); + if (!conflicts.empty()) + theAction->setShortcut(NO_KEYSEQUENCE); + +#ifdef SHORTCUT_MGR_DBG + if (myAssetsLoaded) { + QString report = "Shortcut \"" + (SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) ? SUIT_ShortcutMgr::ROOT_MODULE_ID + TOKEN_SEPARATOR + inModuleActionID : theActionID) + - "\" is not added to default resource files." - ); - auto conflicts = myShortcutContainer.setShortcut(moduleID, inModuleActionID, theAction->shortcut(), false); - if (!conflicts.empty()) - theAction->setShortcut(NO_KEYSEQUENCE); // Unbind any key sequence, if it was bound outside of the class and interferes with other shortcuts. + "\"->\"" + actionKeySequence.toString() + "\" is not added to default preference files. "; + + if (!conflicts.empty()) + report += "The shortcut conflicted with other shortcuts and has been disabled."; + + report += QString("The ") + (conflicts.empty() ? "" : "disabled ") + "shortcut has been added to user preference files."; + + ShCutDbg(report); + } +#endif //SHORTCUT_MGR_DBG } } @@ -1535,7 +2175,7 @@ const SUIT_ShortcutContainer& SUIT_ShortcutMgr::getShortcutContainer() const return myShortcutContainer; } -void SUIT_ShortcutMgr::mergeShortcutContainer(const SUIT_ShortcutContainer& theContainer, bool theOverride, bool theTreatAbsentIncomingAsDisabled) +void SUIT_ShortcutMgr::mergeShortcutContainer(const SUIT_ShortcutContainer& theContainer, bool theOverride, bool theTreatAbsentIncomingAsDisabled, bool theSaveToPreferences) { ShCutDbg() && ShCutDbg("ShortcutMgr merges shortcut container..."); const auto changes = myShortcutContainer.merge(theContainer, theOverride, theTreatAbsentIncomingAsDisabled); @@ -1610,7 +2250,8 @@ void SUIT_ShortcutMgr::mergeShortcutContainer(const SUIT_ShortcutContainer& theC enableAnonymousShortcutsClashingWith(keySequence, !ksIsBoundToAnActionInActiveModule); } - SUIT_ShortcutMgr::saveShortcutsToPreferences(changes); + if (theSaveToPreferences) + SUIT_ShortcutMgr::saveShortcutsToPreferences(changes); } const QKeySequence& SUIT_ShortcutMgr::getKeySequence(const QString& theModuleID, const QString& theInModuleActionID) const @@ -1633,111 +2274,158 @@ std::set SUIT_ShortcutMgr::getIDsOfInterferingModules(const QString& th return myShortcutContainer.getIDsOfInterferingModules(theModuleID); } -std::shared_ptr SUIT_ShortcutMgr::getModuleAssets(const QString& theModuleID) const +std::shared_ptr SUIT_ShortcutMgr::getModuleAssets(const QString& theModuleID) const { const auto itModuleAssets = myModuleAssets.find(theModuleID); - if (itModuleAssets == myModuleAssets.end()) { - auto assets = std::shared_ptr(new SUIT_ActionAssets()); - auto lda = SUIT_ActionAssets::LangDependentAssets(); - lda.myName = theModuleID; // At least something meaningful. + if (itModuleAssets == myModuleAssets.end()) + return std::shared_ptr(nullptr); - assets->myLangDependentAssets.emplace(SUIT_ShortcutMgr::getLang(), lda); - return assets; - } return itModuleAssets->second; } QString SUIT_ShortcutMgr::getModuleName(const QString& theModuleID, const QString& theLang) const { const auto assets = getModuleAssets(theModuleID); - const auto& ldaMap = assets->myLangDependentAssets; - if (ldaMap.empty()) + if (!assets) return theModuleID; - auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang); - if (itLang == ldaMap.end()) - itLang = ldaMap.begin(); // Get name in any language. - - const auto& name = itLang->second.myName; - return name.isEmpty() ? theModuleID : name; + return assets->bestName(theLang); } -std::shared_ptr SUIT_ShortcutMgr::getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const +std::shared_ptr SUIT_ShortcutMgr::getActionAssets(const QString& theModuleID, const QString& theInModuleActionID, bool theTryToCreateRuntimeAssetsIfAbsent) const { const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID); if (actionID.isEmpty()) { ShCutDbg() && ShCutDbg("Can't get action assets: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid."); - return std::shared_ptr(nullptr); + return std::shared_ptr(nullptr); } - return getActionAssets(actionID); + return getActionAssets(actionID, theTryToCreateRuntimeAssetsIfAbsent); } -std::shared_ptr SUIT_ShortcutMgr::getActionAssets(const QString& theActionID) const +std::shared_ptr SUIT_ShortcutMgr::getActionAssets(const QString& theActionID, bool theTryToCreateRuntimeAssetsIfAbsent) const { const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID); - const QString& moduleID = moduleIDAndActionID.first; + const QString& moduleID = SUIT_ShortcutMgr::isInModuleMetaActionID(moduleIDAndActionID.second) ? SUIT_ShortcutMgr::ROOT_MODULE_ID : moduleIDAndActionID.first; const QString& inModuleActionID = moduleIDAndActionID.second; if (inModuleActionID.isEmpty()) { ShCutDbg() && ShCutDbg("Attempt to get assets of an action with invalid ID \"" + theActionID + "\"."); - return std::shared_ptr(nullptr); + return std::shared_ptr(nullptr); } - const auto itModuleActionAssets = myActionAssets.find(moduleID); - if (itModuleActionAssets == myActionAssets.end()) - return std::shared_ptr(nullptr); - else { - const auto moduleActionAssets = itModuleActionAssets->second; - const auto itActionAssets = moduleActionAssets.find(inModuleActionID); - if (itActionAssets == moduleActionAssets.end()) - return std::shared_ptr(nullptr); - else - return itActionAssets->second; + auto itModuleAssets = myModuleAssets.find(moduleID); + if (itModuleAssets != myModuleAssets.end()) { + const std::shared_ptr moduleAssets = itModuleAssets->second; + auto assets = moduleAssets->findDescendantItem(inModuleActionID); + if (assets) + return assets; + } + + if (!myAssetsLoaded) { + Warning("SUIT_ShortcutMgr::getActionAssets(\"" + theActionID + "\") has been called prior to loading of asset files."); + return std::shared_ptr(nullptr); + } + else { // Action assets are not provided in asset files. + Warning("SUIT_ShortcutMgr::getActionAssets(..): Assets of \"" + theActionID + "\" are not provided in asset files."); + if (theTryToCreateRuntimeAssetsIfAbsent) { + const auto runtimeAssets = createRuntimeActionAssets(moduleID, inModuleActionID); + if (runtimeAssets) { + if (itModuleAssets == myModuleAssets.end()) + itModuleAssets = myModuleAssets.emplace(moduleID, std::move(SUIT_ShortcutModuleAssets::create(moduleID))).first; + + auto& moduleAssets = itModuleAssets->second; + auto actionAssets = moduleAssets->descendantItem(inModuleActionID, true /*theIsAction*/); + + actionAssets->merge(std::move(*runtimeAssets), false); + ShCutDbg("Assets of action \"" + theActionID + "\" has been created using runtime data."); + return actionAssets; + } + } } + return std::shared_ptr(nullptr); } -QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang) const +std::shared_ptr SUIT_ShortcutMgr::createRuntimeActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const { - const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID); - if (actionID.isEmpty()) { - ShCutDbg() && ShCutDbg("Can't get action name: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid."); - return actionID; + const auto actions = SUIT_ShortcutMgr::getActions(theModuleID, theInModuleActionID); + if (actions.empty()) + return std::shared_ptr(nullptr); + + SUIT_ShortcutAssets::LangDependentAssets runtimeLDA; + QIcon runtimeIcon; + for (const auto action : actions) { + if (!action->text().isEmpty()) + runtimeLDA.myName = action->text(); + + if (!action->statusTip().isEmpty()) + runtimeLDA.myToolTip = action->statusTip(); + + if (!action->icon().isNull()) + runtimeIcon = action->icon(); } - const auto itModuleActionAssets = myActionAssets.find(theModuleID); - if (itModuleActionAssets == myActionAssets.end()) - return actionID; + if (runtimeLDA.myName.isEmpty() && runtimeLDA.myToolTip.isEmpty() && runtimeIcon.isNull()) + return std::shared_ptr(nullptr); - const auto moduleActionAssets = itModuleActionAssets->second; - const auto itActionAssets = moduleActionAssets.find(theInModuleActionID); - if (itActionAssets != moduleActionAssets.end() && !itActionAssets->second->myLangDependentAssets.empty()) { - const auto& ldaMap = itActionAssets->second->myLangDependentAssets; - if (ldaMap.empty()) - return theInModuleActionID; + if (runtimeLDA.myName.isEmpty()) + runtimeLDA.myName = runtimeLDA.myToolTip; - auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang); - if (itLang == ldaMap.end()) - itLang = ldaMap.begin(); // Get name in any language. + const auto moduleAssets = SUIT_ShortcutModuleAssets::create(theModuleID); + if (!moduleAssets) + return std::shared_ptr(nullptr); - const auto& name = itLang->second.myName; - return name.isEmpty() ? theInModuleActionID : name; - } - else /* if action assets have not been loaded. */ { - // Try to get action->text() and use it as a name. + const auto actionAssets = moduleAssets->descendantItem(theInModuleActionID, true /*theIsAction*/); + if (!actionAssets) + return std::shared_ptr(nullptr); - // Pitfall of the approach: at the time this code block is called, the action may not exist. - // Moreover, an action with such an ID may not even have been created at the time of calling this method. - // Thus, even buffering of assets of every action ever created at runtime does not guarantee, - // that the assets will be available at any point in the life of the application, - // unless the assets are added to dedicated section in an asset file. + if (runtimeLDA.myName.isEmpty()) + runtimeLDA.myName = actionAssets->myInModuleID; - const auto actions = getActions(theModuleID, theInModuleActionID); - for (const auto& action : actions) { - if (!action->text().isEmpty()) - return action->text(); - } + actionAssets->myLangDependentAssets[SUIT_ShortcutMgr::currentLang()] = std::move(runtimeLDA); + actionAssets->myIcon = std::move(runtimeIcon); + + return actionAssets; +} + +QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang) const +{ + std::shared_ptr actionAssets = getActionAssets(theModuleID, theInModuleActionID, true /*theTryToCreateRuntimeAssetsIfAbsent*/); + if (!actionAssets) return theInModuleActionID; + + auto LDA = actionAssets->bestLangDependentAssets(theLang); + if (LDA) + return LDA->myName; + + return theInModuleActionID; +} + +QtxAction* SUIT_ShortcutMgr::createAction(QObject* theParent, QObject* theReceiver, const char* theReceiverMemberMethod, const QString& theActionID, const bool theIsToggle) const +{ + const auto moduleIDAndInModuleID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID); + const QString& moduleID = moduleIDAndInModuleID.first; + const QString& inModuleID = moduleIDAndInModuleID.second; + + if (inModuleID.isEmpty()) { + Warning("Attempt to create action with invalid ID \"" + theActionID + "\"."); + return nullptr; } + + QtxAction* action = nullptr; + const auto actionAssets = getActionAssets(theActionID, false /*theTryToCreateRuntimeAssetsIfAbsent*/); + if (!actionAssets) { + Warning("SUIT_ShortcutMgr::createAction(\"" + theActionID + "\"): assets of the action are absent."); + const QString lastTokenOfID = SUIT_ShortcutMgr::splitIntoTokens(inModuleID).back(); + action = new QtxAction(theParent, theIsToggle, theActionID, inModuleID, lastTokenOfID, QIcon()); + } + else { + action = new QtxAction(theParent, theIsToggle, theActionID, actionAssets->bestToolTip(), actionAssets->bestName(), actionAssets->myIcon); + } + + if (theReceiver && theReceiverMemberMethod) + connect(action, SIGNAL(triggered(bool)), theReceiver, theReceiverMemberMethod); + + return action; } void SUIT_ShortcutMgr::onActionDestroyed(QObject* theObject) @@ -1892,19 +2580,34 @@ std::set> SUIT_ShortcutMgr::setShortcutNoIDChecks(co void SUIT_ShortcutMgr::setShortcutsFromPreferences() { - ShCutDbg() && ShCutDbg("ShortcutMgr is initializing..."); + ShCutDbg() && ShCutDbg("Retrieving shortcuts from preferences has started."); SUIT_ShortcutContainer container; SUIT_ShortcutMgr::fillContainerFromPreferences(container, false /*theDefaultOnly*/); - mergeShortcutContainer(container, true /*theOverride*/, false /*theTreatAbsentIncomingAsDisabled*/); - setAssetsFromResources(); + mergeShortcutContainer(container, true /*theOverride*/, false /*theTreatAbsentIncomingAsDisabled*/, false /*theSaveToPreferences*/); + + if (myAssetsLoaded) { + // Warn, if some action assets are not provided. + const auto moduleIDs = myShortcutContainer.getIDsOfAllModules(); + for (const QString& moduleID : moduleIDs) { + const auto& moduleShortcutsInversed = getModuleShortcutsInversed(moduleID); + for (const auto& shortcutInversed : moduleShortcutsInversed) { + const QString& inModuleActionID = shortcutInversed.first; + const auto& actionAssets = getActionAssets(moduleID, inModuleActionID, false /*theTryToCreateRuntimeAssetsIfAbsent*/); + if (!actionAssets) + Warning("SUIT_ShortcutMgr::setShortcutsFromPreferences(): Assets of action \"" + moduleID + TOKEN_SEPARATOR + inModuleActionID + "\" are not provided in asset files."); + } + } + } + else + Warning("SUIT_ShortcutMgr: loading of shortcuts from preferences happened earlier, than loading of asset files."); - ShCutDbg() && ShCutDbg("ShortcutMgr has been initialized."); + ShCutDbg() && ShCutDbg("Retrieving shortcuts from preferences has finished."); } /*static*/ void SUIT_ShortcutMgr::saveShortcutsToPreferences(const std::map>& theShortcutsInversed) { - ShCutDbg() && ShCutDbg("Saving preferences to resources."); + ShCutDbg() && ShCutDbg("SUIT_ShortcutMgr::saveShortcutsToPreferences(New_Shortcuts_Inversed)"); SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { @@ -1931,7 +2634,7 @@ void SUIT_ShortcutMgr::setShortcutsFromPreferences() /*static*/ void SUIT_ShortcutMgr::saveShortcutsToPreferences(const std::map>>& theShortcutsInversed) { - ShCutDbg() && ShCutDbg("Saving preferences to resources."); + ShCutDbg() && ShCutDbg("SUIT_ShortcutMgr::saveShortcutsToPreferences(Old_&_New_Shortcuts_Inversed)"); SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { @@ -1958,7 +2661,8 @@ void SUIT_ShortcutMgr::setShortcutsFromPreferences() void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage) { - ShCutDbg() && ShCutDbg("Retrieving action assets."); + ShCutDbg() && ShCutDbg("Retrieving shortcut assets."); + static const std::function loadIcon = [] (SUIT_ShortcutAssets& theAssets) { theAssets.loadIcon(); }; SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { @@ -1967,18 +2671,19 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage) } if (theLanguage.isEmpty()) - theLanguage = resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG); + theLanguage = SUIT_ShortcutMgr::currentLang(); - QStringList langPriorityList = LANG_PRIORITY_LIST; - langPriorityList.push_front(theLanguage); - langPriorityList.removeDuplicates(); + ShCutDbg("Requested asset language is \"" + theLanguage + "\"."); + + const auto langs = std::set({ DEFAULT_LANG, theLanguage }); QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS); #ifdef SHORTCUT_MGR_DBG ShCutDbg("Asset files: " + actionAssetFilePaths.join(", ") + "."); #endif for (const QString& actionAssetFilePath : actionAssetFilePaths) { - const QString path = substituteVars(actionAssetFilePath); + const QString path = ::SUIT_tools::substituteVars(actionAssetFilePath); + ShCutDbg("Parsing asset file \"" + path + "\"."); QFile actionAssetFile(path); if (!actionAssetFile.open(QIODevice::ReadOnly)) { Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!"); @@ -1988,120 +2693,101 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage) QJsonParseError jsonError; QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError); actionAssetFile.close(); - if(jsonError.error != QJsonParseError::NoError) { - Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!"); - continue; + if (jsonError.error != QJsonParseError::NoError) { + Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!"); + continue; } - if(document.isObject()) { - QJsonObject object = document.object(); - SUIT_ActionAssets actionAssets; - for (const QString& actionID : object.keys()) { - const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(actionID); - const QString& moduleID = moduleIDAndActionID.first; - const QString& inModuleActionID = moduleIDAndActionID.second; - - if (inModuleActionID.isEmpty()) { - ShCutDbg("Action asset file \"" + path + "\" contains invalid action ID \"" + actionID + "\"."); + if (document.isObject()) { + QJsonObject documentJSONObject = document.object(); + for (const QString& moduleID : documentJSONObject.keys()) { + if (!SUIT_ShortcutMgr::isModuleIDValid(moduleID)) { + ShCutDbg("Action asset file \"" + path + "\" contains invalid module ID \"" + moduleID + "\"."); continue; } - if (!actionAssets.fromJSON(object[actionID].toObject())) { - ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + actionID + "\"."); + const auto& moduleJSONValue = documentJSONObject[moduleID]; + if (!moduleJSONValue.isObject()) { + ShCutDbg("Action asset file \"" + path + "\" contains invalid value with module ID \"" + moduleID + "\"."); continue; } - const bool nameInCurLangExists = actionAssets.myLangDependentAssets.find(theLanguage) != actionAssets.myLangDependentAssets.end(); - if (nameInCurLangExists) { - actionAssets.clearAllLangsExcept(theLanguage); + const auto parsedModuleAssets = SUIT_ShortcutModuleAssets::create(moduleID); + if (!parsedModuleAssets->fromJSON(moduleJSONValue.toObject(), true /*theParseDescendants*/, langs)) { + ShCutDbg("Action asset file \"" + path + "\" contains invalid/empty assets of module with ID \"" + moduleID + "\"."); + continue; } - else { - bool nameInLinguaFrancaExists = false; - QString usedLanguage = QString(); - for (int i = 1; i < langPriorityList.length(); i++) { - nameInLinguaFrancaExists = actionAssets.myLangDependentAssets.find(langPriorityList[i]) != actionAssets.myLangDependentAssets.end(); - if (nameInLinguaFrancaExists) { - usedLanguage = langPriorityList[i]; - actionAssets.clearAllLangsExcept(usedLanguage); - break; - } - } - #ifdef SHORTCUT_MGR_DBG - if (nameInLinguaFrancaExists) - ShCutDbg("Can't find assets for action with ID \"" + actionID + "\" at current (" + theLanguage + ") language. Assets in " + usedLanguage + " is used for the action." ); - else { - ShCutDbg("Can't find assets for action with ID \"" + actionID + "\". Tried " + langPriorityList.join(", ") + " languages." ); - continue; + auto itModuleAssets = myModuleAssets.find(moduleID); + if (itModuleAssets == myModuleAssets.end()) { + itModuleAssets = myModuleAssets.emplace(moduleID, std::move(parsedModuleAssets)).first; + SUIT_ShortcutModuleAssets& moduleAssets = *(itModuleAssets->second); + + moduleAssets.loadIcon(); + moduleAssets.forEachDescendant(loadIcon); + + { // Fill default assets of module itself, not its actions/folders. The default assets are overriden by assets in files. + if (moduleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) { + if (moduleAssets.myLangDependentAssets.find(DEFAULT_LANG) == moduleAssets.myLangDependentAssets.end()) { + auto& lda = moduleAssets.myLangDependentAssets[DEFAULT_LANG]; + lda.myName = tr("General"); + } + + if (moduleAssets.myIconPath.isEmpty()) { + QString dirPath; + if (resMgr->value("resources", "LightApp", dirPath)) + moduleAssets.myIconPath = dirPath + (!dirPath.isEmpty() && dirPath.back() == "/" ? "" : "/") + "icon_default.png"; + } + + if (moduleAssets.myIcon.isNull()) + moduleAssets.myIcon = QIcon(::SUIT_tools::substituteVars(moduleAssets.myIconPath)); + } + else /* if module is not root module */ { + if (moduleAssets.myLangDependentAssets.find(DEFAULT_LANG) == moduleAssets.myLangDependentAssets.end()) { + auto& lda = moduleAssets.myLangDependentAssets[DEFAULT_LANG]; + + QString moduleName = moduleID; + resMgr->value(moduleID, "name", moduleName); + lda.myName = moduleName; + + resMgr->value(moduleID, "description", lda.myToolTip); + } + + if (moduleAssets.myIconPath.isEmpty()) { + QString dirPath; + QString fileName; + if (resMgr->value("resources", moduleID, dirPath) && resMgr->value(moduleID, "icon", fileName)) + moduleAssets.myIconPath = dirPath + (!dirPath.isEmpty() && dirPath.back() == "/" ? "" : "/") + fileName; + } + + if (moduleAssets.myIcon.isNull()) + moduleAssets.myIcon = QIcon(::SUIT_tools::substituteVars(moduleAssets.myIconPath)); + } } - #endif } + else /* if assets of the module have already been added */ { + SUIT_ShortcutModuleAssets& moduleAssets = *(itModuleAssets->second); - auto& moduleActionAssets = myActionAssets[moduleID]; - auto itAssets = moduleActionAssets.find(inModuleActionID); - if (itAssets == moduleActionAssets.end()) { - auto pAssets = std::shared_ptr(new SUIT_ActionAssets(actionAssets)); - itAssets = moduleActionAssets.emplace(inModuleActionID, pAssets).first; + moduleAssets.merge(std::move(*parsedModuleAssets), true); + moduleAssets.loadIcon(); + moduleAssets.forEachDescendant(loadIcon); } - else - itAssets->second->merge(actionAssets, true); - - const auto& assets = itAssets->second; - if (!assets->myIconPath.isEmpty() && assets->myIcon.isNull()) - assets->myIcon = QIcon(substituteVars(assets->myIconPath)); } } } - #ifdef SHORTCUT_MGR_DBG - ShCutDbg("Parsed assets: "); - QJsonObject object; - for (const auto& moduleIDAndAssets : myActionAssets) { - for (const auto& actionIDAndAssets : moduleIDAndAssets.second) { - actionIDAndAssets.second->toJSON(object); - QJsonDocument doc(object); - QString strJson = doc.toJson(QJsonDocument::Indented); - const QString actionID = SUIT_ShortcutMgr::makeActionID(moduleIDAndAssets.first, actionIDAndAssets.first); - ShCutDbg(actionID + " : " + strJson); - } - } - #endif - - // Fill myModuleAssets. - for (const auto& moduleID : myShortcutContainer.getIDsOfAllModules()) { - const auto assets = std::shared_ptr(new SUIT_ActionAssets()); - auto& lda = assets->myLangDependentAssets[DEFAULT_LANG]; - - if (moduleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) { - lda.myName = tr("General"); - - { // Load icon. - QString dirPath; - if (resMgr->value("resources", "LightApp", dirPath)) { - assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + "icon_default.png"; - assets->myIcon = QIcon(substituteVars(assets->myIconPath)); - } - } - } - else { - QString moduleName = moduleID; - resMgr->value(moduleID, "name", moduleName); - lda.myName = moduleName; - - resMgr->value(moduleID, "description", lda.myToolTip); - - { // Load icon. - QString dirPath; - QString fileName; - if (resMgr->value("resources", moduleID, dirPath) && resMgr->value(moduleID, "icon", fileName)) { - assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + fileName; - assets->myIcon = QIcon(substituteVars(assets->myIconPath)); - } - } - } + myAssetsLoaded = true; - myModuleAssets.emplace(moduleID, std::move(assets)); +#ifdef SHORTCUT_MGR_DBG + ShCutDbg("All assets: "); + QJsonObject moduleJSONObject; + for (const auto& moduleIDAndAssets : myModuleAssets) { + moduleIDAndAssets.second->toJSON(moduleJSONObject); + QJsonDocument doc(moduleJSONObject); + QString strJson = doc.toJson(QJsonDocument::Indented); + ShCutDbg("\"" + moduleIDAndAssets.first + "\": " + strJson); } +#endif } void SUIT_ShortcutMgr::registerAnonymousShortcut(QAction* const theAction) @@ -2210,251 +2896,7 @@ QString SUIT_ShortcutMgr::anonymousShortcutsToString() const } - -SUIT_SentenceMatcher::SUIT_SentenceMatcher() -{ - myUseExactWordOrder = false; - myUseFuzzyWords = true; - myIsCaseSensitive = false; -} - -void SUIT_SentenceMatcher::setUseExactWordOrder(bool theOn) -{ - if (myUseExactWordOrder == theOn) - return; - - myUseExactWordOrder = theOn; - if (theOn) { - myPermutatedSentences.clear(); - myFuzzyPermutatedSentences.clear(); - return; - } - - if (myPermutatedSentences.isEmpty()) - SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); - - if (myUseFuzzyWords && myFuzzyPermutatedSentences.isEmpty()) - SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); -} - -void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn) -{ - if (myUseFuzzyWords == theOn) - return; - - myUseFuzzyWords = theOn; - if (myWords.isEmpty() || !theOn) { - myFuzzyWords.clear(); - myFuzzyPermutatedSentences.clear(); - return; - } - - myFuzzyWords.clear(); - SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); - - if (!myUseExactWordOrder) { - myFuzzyPermutatedSentences.clear(); - SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); - } -} - -void SUIT_SentenceMatcher::setCaseSensitive(bool theOn) -{ - myIsCaseSensitive = theOn; -} - -void SUIT_SentenceMatcher::setQuery(QString theQuery) -{ - theQuery = theQuery.simplified(); - if (theQuery == myQuery) - return; - - myQuery = theQuery; - myWords = theQuery.split(" ", QString::SkipEmptyParts); - - { // Set permutated sentences. - myPermutatedSentences.clear(); - if (!myUseExactWordOrder) - SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); - } - - // Set fuzzy words and sentences. - myFuzzyWords.clear(); - myFuzzyPermutatedSentences.clear(); - - if (myUseFuzzyWords) { - SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); - if (!myUseExactWordOrder) - SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); - } -} - -double SUIT_SentenceMatcher::match(const QString& theInputString) const -{ - int n = 0; - if (myUseExactWordOrder) { - n = SUIT_SentenceMatcher::match(theInputString, myWords, myIsCaseSensitive); - if (n != theInputString.length() && myUseFuzzyWords) { - const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyWords, myIsCaseSensitive); - if (nFuzzy > n) - n = nFuzzy; - } - } - else /* if match with permutated query sentences */ { - n = SUIT_SentenceMatcher::match(theInputString, myPermutatedSentences, myIsCaseSensitive); - if (n != theInputString.length() && myUseFuzzyWords) { - const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyPermutatedSentences, myIsCaseSensitive); - if (nFuzzy > n) - n = nFuzzy; - } - } - - if (n <= 0) - return std::numeric_limits::infinity(); - - const auto strLength = theInputString.length() > myQuery.length() ? theInputString.length() : myQuery.length(); - - if (n > strLength) - return 0; // Exact match or almost exact. - - return double(strLength - n); -} - -QString SUIT_SentenceMatcher::toString() const -{ - QString res = QString("myUseExactWordOrder: ") + (myUseExactWordOrder ? "true" : "false") + ";\n"; - res += QString("myUseFuzzyWords: ") + (myUseFuzzyWords ? "true" : "false") + ";\n"; - res += QString("myIsCaseSensitive: ") + (myIsCaseSensitive ? "true" : "false") + ";\n"; - res += QString("myQuery: ") + myQuery + ";\n"; - res += QString("myWords: ") + myWords.join(", ") + ";\n"; - res += QString("myFuzzyWords: ") + myFuzzyWords.join(", ") + ";\n"; - - res += "myPermutatedSentences:\n"; - for (const auto& sentence : myPermutatedSentences) { - res += "\t" + sentence.join(", ") + ";\n"; - } - - res += "myFuzzyPermutatedSentences:\n"; - for (const auto& sentence : myFuzzyPermutatedSentences) { - res += "\t" + sentence.join(", ") + ";\n"; - } - - res += "."; - return res; -} - -/*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList& theSentences) -{ - theSentences.clear(); - theSentences.push_back(theWords); - QStringList nextPerm = theWords; - QStringList prevPerm = theWords; - - bool hasNextPerm = true; - bool hasPrevPerm = true; - - while (hasNextPerm || hasPrevPerm) { - if (hasNextPerm) - hasNextPerm = std::next_permutation(nextPerm.begin(), nextPerm.end()); - - if (hasNextPerm && !theSentences.contains(nextPerm)) - theSentences.push_back(nextPerm); - - if (hasPrevPerm) - hasPrevPerm = std::prev_permutation(prevPerm.begin(), prevPerm.end()); - - if (hasPrevPerm && !theSentences.contains(prevPerm)) - theSentences.push_back(prevPerm); - } - - return theSentences.size() > 1; -} - -/*static*/ void SUIT_SentenceMatcher::makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords) -{ - theFuzzyWords.clear(); - for (const QString& word : theWords) { - QString fuzzyWord; - for (int i = 0; i < word.size(); i++) { - fuzzyWord += word[i]; - fuzzyWord += "\\w*"; - } - theFuzzyWords.push_back(fuzzyWord); - } -} - -/*static*/ int SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive) -{ - const QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); - regExp.indexIn(theInputString); - const int matchMetrics = regExp.matchedLength(); - return matchMetrics > 0 ? matchMetrics : 0; -} - -/*static*/ int SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive) -{ - int res = 0; - for (const QStringList& sentence : theSentences) { - const int matchMetrics = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive); - if (matchMetrics > res) { - res = matchMetrics; - if (res == theInputString.length()) - return res; - } - } - return res; -} - -/*static*/ int SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive) -{ - int res = 0; - for (const QString& word : theWords) { - const auto regExp = QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); - regExp.indexIn(theInputString); - const int matchMetrics = regExp.matchedLength(); - // The same input word can be counted multiple times. Nobody cares. - if (matchMetrics > 0) - res += matchMetrics; - } - return res; -} - -/*static*/ int SUIT_SentenceMatcher::match( - const QString& theInputString, - const QStringList& theSentence, - bool theCaseSensitive -) { - int res = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive); - if (res == theInputString.length()) - return res; - - const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive); - if (matchMetrics > res) - res = matchMetrics; - - return res; -} - -/*static*/ int SUIT_SentenceMatcher::match( - const QString& theInputString, - const QList& theSentences, - bool theCaseSensitive -) { - int res = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive); - if (res == theInputString.length()) - return res; - - if (theSentences.size() > 0) { - const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive); - if (matchMetrics > res) - res = matchMetrics; - } - - return res; -} - - -SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr theAssets, double theMatchMetrics) +SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr theAssets, double theMatchMetrics) : myAssets(theAssets), myMatchMetrics(theMatchMetrics) { if (theMatchMetrics < 0) { @@ -2493,17 +2935,45 @@ QString SUIT_ActionSearcher::AssetsAndSearchData::toString() const return QString(doc.toJson(QJsonDocument::Indented)); } +/*static*/ double SUIT_ActionSearcher::matchKeySequenceString(const QString& theQuery, const QString& theKeySequence) +{ + static const QRegExp spaceRegEx("\\s"); + + if (theKeySequence.isEmpty()) + return std::numeric_limits::infinity(); + + QString queryCleaned = theQuery.simplified().remove(spaceRegEx);; + if (queryCleaned.isEmpty()) + return std::numeric_limits::infinity(); + + if (queryCleaned.contains(theKeySequence, Qt::CaseInsensitive)) + return queryCleaned.length() - theKeySequence.length(); + else if (theKeySequence.contains(queryCleaned, Qt::CaseInsensitive)) + return theKeySequence.length() - queryCleaned.length(); + + return std::numeric_limits::infinity(); +} + SUIT_ActionSearcher::SUIT_ActionSearcher() { myIncludedModuleIDs = { SUIT_ShortcutMgr::ROOT_MODULE_ID }; myIncludeDisabledActions = false; myFieldsToMatch = { SUIT_ActionSearcher::MatchField::Name, SUIT_ActionSearcher::MatchField::ToolTip }; - myMatcher.setCaseSensitive(false); - myMatcher.setUseExactWordOrder(false); - myMatcher.setUseFuzzyWords(true); + myMatcher = new ::SUIT_tools::SUIT_SentenceMatcher(); + myMatcher->setCaseSensitive(false); + myMatcher->setUseExactWordOrder(false); + myMatcher->setUseFuzzyWords(true); + myKeySequenceGetter = [] (const QString& theModuleID, const QString& theInModuleActionID) { + return SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString(); + }; } -bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set theIncludedModuleIDs) +SUIT_ActionSearcher::~SUIT_ActionSearcher() +{ + delete myMatcher; +} + +bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set theIncludedModuleIDs, bool doNotUpdateResults) { ShCutDbg("SUIT_ActionSearcher::setIncludedModuleIDs"); @@ -2512,6 +2982,9 @@ bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set theIncludedModu myIncludedModuleIDs = theIncludedModuleIDs; + if (doNotUpdateResults) + return false; + bool res = false; // Erase search results from excluded modules. Erase IDs of modules, which are already in search results, from theIncludedModuleIDs. for (auto itFound = mySearchResults.begin(); itFound != mySearchResults.end(); ) { @@ -2527,29 +3000,32 @@ bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set theIncludedModu } // Filter assets of added modules. - const auto& allAssets = SUIT_ShortcutMgr::get()->getActionAssets(); + const std::function)> filterItem = [this, &res] (std::shared_ptr theItemAssets) { + if (!theItemAssets->isAction()) + return; + + const double matchMetrics = matchAction(*theItemAssets); + if (matchMetrics < std::numeric_limits::infinity()) { + mySearchResults[theItemAssets->myModuleID][theItemAssets->myInModuleID] = SUIT_ActionSearcher::AssetsAndSearchData(theItemAssets, matchMetrics); + res = true; + } + }; + + const auto& allAssets = SUIT_ShortcutMgr::get()->getModuleAssets(); for (const auto& moduleIDAndAssets : allAssets) { const QString& moduleID = moduleIDAndAssets.first; - const auto& actionIDsAndAssets = moduleIDAndAssets.second; if (theIncludedModuleIDs.find(moduleID) == theIncludedModuleIDs.end()) continue; - for (const auto& actionIDAndAssets : actionIDsAndAssets) { - const QString& inModuleActionID = actionIDAndAssets.first; - const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); - if (matchMetrics < std::numeric_limits::infinity()) { - mySearchResults[moduleID][inModuleActionID] = SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics); - res = true; - } - } + const auto& moduleAssets = moduleIDAndAssets.second; + moduleAssets->forEachDescendant(filterItem); } ShCutDbg() && ShCutDbg(toString()); - return res; } -bool SUIT_ActionSearcher::includeDisabledActions(bool theOn) +bool SUIT_ActionSearcher::includeDisabledActions(bool theOn, bool doNotUpdateResults) { ShCutDbg("SUIT_ActionSearcher::includeDisabledActions"); @@ -2558,6 +3034,9 @@ bool SUIT_ActionSearcher::includeDisabledActions(bool theOn) myIncludeDisabledActions = theOn; + if (doNotUpdateResults) + return false; + bool res; if (myIncludeDisabledActions) res = extendResults(); @@ -2568,7 +3047,7 @@ bool SUIT_ActionSearcher::includeDisabledActions(bool theOn) return res; } -bool SUIT_ActionSearcher::setFieldsToMatch(const std::set& theFields) +bool SUIT_ActionSearcher::setFieldsToMatch(const std::set& theFields, bool doNotUpdateResults) { if (myFieldsToMatch == theFields) return false; @@ -2579,24 +3058,22 @@ bool SUIT_ActionSearcher::setFieldsToMatch(const std::set& theSetA, const std::set& theSetB) { + for (const auto& valA : theSetA) { + if (theSetB.find(valA) == theSetB.end()) + return false; } - } + return true; + }; - bool extends = true; - for (const SUIT_ActionSearcher::MatchField field : myFieldsToMatch) { - if (theFields.find(field) == theFields.end()) { - extends = false; - break; - } - } + const bool narrows = isASubsetOfB(theFields, myFieldsToMatch); + const bool extends = isASubsetOfB(myFieldsToMatch, theFields); myFieldsToMatch = theFields; + if (doNotUpdateResults) + return false; + bool res; if (narrows) res = filterResults().first; @@ -2609,12 +3086,31 @@ bool SUIT_ActionSearcher::setFieldsToMatch(const std::set SUIT_ActionSearcher::setKeySequenceGetter(const std::function& theKeySequenceGetter, bool doNotUpdateResults) +{ + if (theKeySequenceGetter) + myKeySequenceGetter = theKeySequenceGetter; + else { + myKeySequenceGetter = [] (const QString& theModuleID, const QString& theInModuleActionID) { + return SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString(); + }; + } + + if (doNotUpdateResults) + return std::pair(false, false); + + return filter(); +} + +bool SUIT_ActionSearcher::setCaseSensitive(bool theOn, bool doNotUpdateResults) { - if (myMatcher.isCaseSensitive() == theOn) + if (myMatcher->isCaseSensitive() == theOn) return false; - myMatcher.setCaseSensitive(theOn); + myMatcher->setCaseSensitive(theOn); + + if (doNotUpdateResults) + return false; bool res; if (theOn) @@ -2630,15 +3126,21 @@ bool SUIT_ActionSearcher::setQuery(const QString& theQuery) { ShCutDbg("SUIT_ActionSearcher::setQuery"); - if (theQuery.simplified() == myMatcher.getQuery().simplified()) + if (theQuery.simplified() == myMatcher->getQuery().simplified()) return false; - myMatcher.setQuery(theQuery); + myMatcher->setQuery(theQuery); bool res = filter().first; ShCutDbg() && ShCutDbg(toString()); return res; } +const QString& SUIT_ActionSearcher::getQuery() const +{ + return myMatcher->getQuery(); +} + + const std::map>& SUIT_ActionSearcher::getSearchResults() const { return mySearchResults; @@ -2647,19 +3149,20 @@ const std::map SUIT_ActionSearcher::filter() { ShCutDbg("SUIT_ActionSearcher::filter()"); - auto res = std::pair(false, false); - for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) { + for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getModuleAssets()) { const auto& moduleID = moduleIDAndAssets.first; if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end()) continue; - const auto& actionIDsAndAssets = moduleIDAndAssets.second; - auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID); - for (const auto& actionIDAndAssets : actionIDsAndAssets) { - const QString& inModuleActionID = actionIDAndAssets.first; + const std::function)> filterItem = [this, &res, &itFoundModuleIDAndAssets] (std::shared_ptr theItemAssets) { + if (!theItemAssets->isAction()) + return; + + const QString& moduleID = theItemAssets->myModuleID; + const QString& inModuleActionID = theItemAssets->myInModuleID; if (itFoundModuleIDAndAssets != mySearchResults.end()) { auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second; @@ -2667,7 +3170,7 @@ std::pair SUIT_ActionSearcher::filter() if (itFoundActionIDAndAssets != foundActionIDsAndAssets.end()) { // Action is already in search results. SUIT_ActionSearcher::AssetsAndSearchData& aAndD = itFoundActionIDAndAssets->second; - const double matchMetrics = matchAction(moduleID, inModuleActionID, aAndD.myAssets); + const double matchMetrics = matchAction(*(aAndD.myAssets)); if (matchMetrics < std::numeric_limits::infinity()) { if (matchMetrics != aAndD.matchMetrics()) { aAndD.setMatchMetrics(matchMetrics); @@ -2678,19 +3181,22 @@ std::pair SUIT_ActionSearcher::filter() foundActionIDsAndAssets.erase(itFoundActionIDAndAssets); res.first = true; } - continue; + return; } } - const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); + const double matchMetrics = matchAction(*theItemAssets); if (matchMetrics < std::numeric_limits::infinity()) { if (itFoundModuleIDAndAssets == mySearchResults.end()) itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map()).first; - itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics)); + itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(theItemAssets, matchMetrics)); res.first = true; } - } + }; + + const auto& moduleAssets = moduleIDAndAssets.second; + moduleAssets->forEachDescendant(filterItem); } return res; @@ -2706,7 +3212,7 @@ std::pair SUIT_ActionSearcher::filterResults() for (auto itActionIDAndAssets = actionIDsAndAssets.begin(); itActionIDAndAssets != actionIDsAndAssets.end(); ) { const QString& inModuleActionID = itActionIDAndAssets->first; SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = itActionIDAndAssets->second; - const double matchMetrics = matchAction(moduleID, inModuleActionID, assetsAndSearchData.myAssets); + const double matchMetrics = matchAction(*(assetsAndSearchData.myAssets)); if (matchMetrics == std::numeric_limits::infinity()) { itActionIDAndAssets = actionIDsAndAssets.erase(itActionIDAndAssets); res.first = true; @@ -2732,49 +3238,53 @@ std::pair SUIT_ActionSearcher::filterResults() bool SUIT_ActionSearcher::extendResults() { ShCutDbg("SUIT_ActionSearcher::extendResults()"); - bool res = false; - for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) { + + for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getModuleAssets()) { const auto& moduleID = moduleIDAndAssets.first; if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end()) continue; - const auto& actionIDsAndAssets = moduleIDAndAssets.second; - auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID); - for (const auto& actionIDAndAssets : actionIDsAndAssets) { - const QString& inModuleActionID = actionIDAndAssets.first; + const std::function)> filterItem = [this, &res, &itFoundModuleIDAndAssets] (std::shared_ptr theItemAssets) { + if (!theItemAssets->isAction()) + return; + + const QString& moduleID = theItemAssets->myModuleID; + const QString& inModuleActionID = theItemAssets->myInModuleID; if (itFoundModuleIDAndAssets != mySearchResults.end()) { const auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second; if (foundActionIDsAndAssets.find(inModuleActionID) != foundActionIDsAndAssets.end()) - continue; // Action is already in search results. + return; // Action is already in search results. } - ShCutDbg() && ShCutDbg("SUIT_ActionSearcher::extendResults(): " + moduleID + "/" + inModuleActionID + "." ); - const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); + ShCutDbg() && ShCutDbg("SUIT_ActionSearcher::extendResults(): " + moduleID + TOKEN_SEPARATOR + inModuleActionID + "." ); + const double matchMetrics = matchAction(*theItemAssets); if (matchMetrics < std::numeric_limits::infinity()) { ShCutDbg("SUIT_ActionSearcher::extendResults(): match, metrics = " + QString::fromStdString(std::to_string(matchMetrics))); if (itFoundModuleIDAndAssets == mySearchResults.end()) itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map()).first; - itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics)); + itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(theItemAssets, matchMetrics)); res = true; } - } + }; + + const auto& moduleAssets = moduleIDAndAssets.second; + moduleAssets->forEachDescendant(filterItem); } + return res; } -double SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr theAssets) +double SUIT_ActionSearcher::matchAction(const SUIT_ShortcutItemAssets& theAssets) { - if (!theAssets) { - ShCutDbg("WARNING: SUIT_ActionSearcher::matchAction: theAssets is nullptr."); + if (!theAssets.isAction()) return std::numeric_limits::infinity(); - } if (!myIncludeDisabledActions) { - const auto& actions = SUIT_ShortcutMgr::get()->getActions(theModuleID, theInModuleActionID); + const auto& actions = SUIT_ShortcutMgr::get()->getActions(theAssets.myModuleID, theAssets.myInModuleID); const bool actionEnabled = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); } ) != actions.end(); if (!actionEnabled) return std::numeric_limits::infinity(); @@ -2782,29 +3292,47 @@ double SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QStrin double res = std::numeric_limits::infinity(); - for (const auto& langAndLDA : theAssets->myLangDependentAssets) { + std::set langs = std::set({DEFAULT_LANG, SUIT_ShortcutMgr::currentLang()}); + for (const auto& langAndLDA : theAssets.myLangDependentAssets) { if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ToolTip) != myFieldsToMatch.end()) { - const double matchMetrics = myMatcher.match(langAndLDA.second.myToolTip); + const double matchMetrics = myMatcher->match(langAndLDA.second.myToolTip); if (matchMetrics < res) res = matchMetrics; } if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) { - const double matchMetrics = myMatcher.match(langAndLDA.second.myName); + const double matchMetrics = myMatcher->match(langAndLDA.second.myName); + if (matchMetrics < res) + res = matchMetrics; + + // The block is not engaged at all, if theAssets does not contain LDA. But in fact, query may match with path prefix. + // Path is matched in current, default and exisiting LDA languages in the next block. + // + // const double matchMetricsPath = myMatcher->match(theAssets.bestPath(langAndLDA.first)); + // if (matchMetricsPath < res) + // res = matchMetricsPath; + } + langs.emplace(langAndLDA.first); + } + + // Match path. + if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) { + for (const QString& lang : langs) { + const double matchMetrics = myMatcher->match(theAssets.bestPath(lang)); if (matchMetrics < res) res = matchMetrics; } } if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ID) != myFieldsToMatch.end()) { - const double matchMetrics = myMatcher.match(SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID)); + const double matchMetrics = myMatcher->match(SUIT_ShortcutMgr::makeActionID(theAssets.myModuleID, theAssets.myInModuleID)); if (matchMetrics < res) res = matchMetrics; } if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::KeySequence) != myFieldsToMatch.end()) { - const QString keySequence = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString(); - const double matchMetrics = myMatcher.match(keySequence); + const QString keySequence = myKeySequenceGetter(theAssets.myModuleID, theAssets.myInModuleID); + const double matchMetrics = SUIT_ActionSearcher::matchKeySequenceString(myMatcher->getQuery(), keySequence); if (matchMetrics < res) res = matchMetrics; } @@ -2817,7 +3345,7 @@ QString SUIT_ActionSearcher::toString() const QString res; res += "myMatcher: {\n"; - res += myMatcher.toString(); + res += myMatcher->toString(); res += "};\n"; res += "myIncludedModuleIDs: "; diff --git a/src/SUIT/SUIT_ShortcutMgr.h b/src/SUIT/SUIT_ShortcutMgr.h index b0d813931..228de4067 100644 --- a/src/SUIT/SUIT_ShortcutMgr.h +++ b/src/SUIT/SUIT_ShortcutMgr.h @@ -149,9 +149,20 @@ private: }; -/*! \brief GUI-related assets. */ -struct SUIT_EXPORT SUIT_ActionAssets +class SUIT_ShortcutItemAssets; + + +/*! \brief Base class for GUI-related assets of module, action or folder. Used by Shortcut Manager, Find Action Dialog, etc. */ +class SUIT_EXPORT SUIT_ShortcutAssets : public std::enable_shared_from_this { +public: + enum class Type + { + Module, + Item // Folder, action or folder-action. + }; + + struct LangDependentAssets { static const QString PROP_ID_NAME; @@ -164,25 +175,221 @@ struct SUIT_EXPORT SUIT_ActionAssets QString myToolTip; }; - static const QString STRUCT_ID; + static const QString PROP_ID_LANG_DEPENDENT_ASSETS; static const QString PROP_ID_ICON_PATH; + static const QString PROP_ID_CHILDREN; + +protected: + SUIT_ShortcutAssets(const QString& theModuleID); + +public: + virtual ~SUIT_ShortcutAssets() = 0; + + const std::map>& children() const; + + /*! + \param theRelativeID If empty, nullptr is returned. + \returns Descdendant item (if exist) with inModuleID = myInModuleID/theRelativeID. + Module assets effectively have empty myInModuleID. */ + std::shared_ptr findDescendantItem(const QString& theRelativeID) const; + + /*! + \param theRelativeID If empty, nullptr is returned. + \param theIsAction If true and the descendant is missing, makes the created item action. + \returns Descdendant item with inModuleID = myInModuleID/theRelativeID. + If the descendant item does not exist, creates the item and its missing ancestors. All missing ancestors are created as not actions. + Module assets effectively have empty myInModuleID. */ + std::shared_ptr descendantItem(const QString& theRelativeID, bool theIsAction = true); + + virtual int depth() const = 0; + virtual SUIT_ShortcutAssets::Type type() const = 0; + +private: + /*! \brief Parses everything, except children. + \param theLangs If empty, LangDependentAssets in all available languages are parsed. */ + bool fromJSONOwnProps(const QJsonObject& theJsonObject, const std::set& theLangs = {}); + +public: + /*! + \param theLangs If empty, LangDependentAssets in all available languages are parsed. + \returns true, if any property is parsed. */ + bool fromJSON(const QJsonObject& theJsonObject, bool theParseDescendants = true, const std::set& theLangs = {}); + + /*! \brief Parses only the branch of descdendants, which leads to the item with theRelativeID. + \param theRelativeID If empty, no descendants are added/updated. + \param theLangs If empty, LangDependentAssets in all available languages are parsed. + \returns true, if any property is parsed. */ + bool fromJSON(const QJsonObject& theJsonObject, const QString& theRelativeID, const std::set& theLangs = {}); - bool fromJSON(const QJsonObject& theJsonObject); void toJSON(QJsonObject& oJsonObject) const; + + /*! \param theOverride If true, values of theOther override conflicting values of this. */ + virtual void merge(const SUIT_ShortcutAssets& theOther, bool theOverride); + + /*! \param theOverride If true, values of theOther override conflicting values of this. */ + virtual void merge(SUIT_ShortcutAssets&& theOther, bool theOverride); + +private: + /*! \brief Parses properties of SUIT_ShortcutAssets subclasses. + \returns true, if any property is parsed. */ + virtual bool fromJSONOtherProps(const QJsonObject& theJsonObject) { return false; }; + + virtual void toJSONVirtual(QJsonObject& oJsonObject) const {}; + +public: + void loadIcon(bool theReload = false); + + /*! \brief Iterates all descendants. *this is not iterated. */ + void forEachDescendant(const std::function& theFunc) const; + + /*! \brief Iterates all descendants. *this is not iterated. */ + void forEachDescendant(const std::function& theFunc) const; + + /*! \brief Iterates all descendants. *this is not iterated. */ + void forEachDescendant(const std::function)>& theFunc) const; + + /*! \brief Iterates all descendants. *this is not iterated. */ + void forEachDescendant(const std::function)>& theFunc) const; + QString toString() const; + virtual QString description() const = 0; QStringList getLangs() const; void clearAllLangsExcept(const QString& theLang); - /*! \param theOverride If true, values of theOther override conflicting values of this. */ - void merge(const SUIT_ActionAssets& theOther, bool theOverride); + /*! + \param theLang If empty, current language is requested. + \returns Requested assets or assets in EN, if requested language is absent. */ + const LangDependentAssets* bestLangDependentAssets(QString theLang = QString()) const; + + virtual const QString& bestName(const QString& theLang = QString()) const = 0; + + /*! + \param theLang If empty, current language is requested. + \returns Requested tool tip or tool tip in EN, if requested language is absent. If EN is absent - empty string. */ + virtual const QString& bestToolTip(const QString& theLang = QString()) const; + +public: + const QString myModuleID; std::map myLangDependentAssets; QString myIconPath; - /*! Is not serialized. */ + /*! Not serialized. */ QIcon myIcon; + +private: + // { IDLastToken, assets }. + std::map> myChildren; +}; + + +/*! \brief GUI-related module assets.*/ +class SUIT_EXPORT SUIT_ShortcutModuleAssets : public SUIT_ShortcutAssets +{ +private: + SUIT_ShortcutModuleAssets(const QString& theModuleID); + SUIT_ShortcutModuleAssets(const SUIT_ShortcutModuleAssets&) = delete; + SUIT_ShortcutModuleAssets& operator= (const SUIT_ShortcutModuleAssets&) = delete; + +public: + static std::shared_ptr create(const QString& theModuleID); + ~SUIT_ShortcutModuleAssets() = default; + + int depth() const { return 0; }; + SUIT_ShortcutAssets::Type type() const { return SUIT_ShortcutAssets::Type::Module; }; + + /*! + \param theLang If empty, current language is requested. + \returns Requested name or name in EN, if requested language is absent. If EN is absent - moduleID. */ + const QString& bestName(const QString& theLang = QString()) const; + + QString description() const; +}; + + +/*! \brief May represent not just an action, but also a folder within item tree of a module. +May also represent action-folder: an action with nested actions. +Each action inModuleID is unique within a tree of assets. */ +class SUIT_EXPORT SUIT_ShortcutItemAssets : public SUIT_ShortcutAssets +{ + friend class SUIT_ShortcutAssets; + +public: + static void loadDefaultIcons(); + static const QString PROP_ID_IS_ACTION; // Absense of the key in JSON means myIsAction == true. + +private: + static QIcon DEFAUT_ICON_ACTION; + static QIcon DEFAUT_ICON_FOLDER; + static QIcon DEFAUT_ICON_FOLDER_ACTION; + + /*! \brief Creates root item of module. + \param theModule must not be nullptr. + \param theIDLastToken is also a inModuleID. + */ + SUIT_ShortcutItemAssets(std::shared_ptr theModule, const QString& theIDLastToken, bool theIsAction = true); + + /*! \brief Creates nested item within item tree of module. + \param theParent must not be nullptr. */ + SUIT_ShortcutItemAssets(std::shared_ptr theParentItem, const QString& theIDLastToken, bool theIsAction = true); + SUIT_ShortcutItemAssets(const SUIT_ShortcutItemAssets&) = delete; + SUIT_ShortcutItemAssets& operator= (const SUIT_ShortcutItemAssets&) = delete; + static std::shared_ptr create(std::shared_ptr theParentItemOrModule, const QString& theIDLastToken, bool theIsAction = true); + + public: + ~SUIT_ShortcutItemAssets() = default; + + int depth() const; + SUIT_ShortcutAssets::Type type() const { return SUIT_ShortcutAssets::Type::Item; }; + + /*! \param theOverride If true, values of theOther override conflicting values of this. */ + void merge(const SUIT_ShortcutItemAssets& theOther, bool theOverride); + + /*! \param theOverride If true, values of theOther override conflicting values of this. */ + virtual void merge(SUIT_ShortcutItemAssets&& theOther, bool theOverride); + +private: + bool fromJSONOtherProps(const QJsonObject& theJsonObject); + void toJSONVirtual(QJsonObject& oJsonObject) const; + +public: + /*! + \param theLang If empty, current language is requested. + \returns Requested name or name in EN, if requested language is absent. If EN is absent - inModuleID. */ + const QString& bestName(const QString& theLang = QString()) const; + + /*! \brief Composed as /....../bestName>. Module name is not included. */ + const QString& bestPath(const QString theLang = QString()) const; + + QString description() const; + + bool isAction() const; + bool isFolder() const; + std::shared_ptr parent() const; + std::shared_ptr module() const; + + /*! \brief Even if SUIT_ShortcutItemAssets is not action, its ID is composed in the same way. */ + QString actionID() const; + + /*! \returns myIcon, if !myIcon.isNull(). Otherwise returns appropriate default icon. */ + const QIcon& icon() const; + +private: + std::weak_ptr myParent; + + bool myIsAction; + +public: + const QString myIDLastToken; + const QString myInModuleID; // Composed as /....../myIDLastToken>. Synonym for inModuleActionID. + +private: + const int myDepth; + + /** {lang, bestPath}[] */ + mutable std::map myBestPaths; }; @@ -190,58 +397,82 @@ struct SUIT_EXPORT SUIT_ActionAssets \class SUIT_ShortcutMgr \brief Handles action shortcut customization. - IDENTIFIED ACTIONS/SHORTCUTS - - Register actions under action IDs. Set shortcuts, which are [action ID]<->[key sequence] mappings. - Every time an action is registered or a shorcut is set, if there are an action and a shortcut, - which are mapped to the same action ID, the action is bound to the key sequence of the shortcut. - Action IDs are also used to (de)serialize shortcut settings. - Several QActions may be registered under the same ID. - - Most of actions are intercepted on creation in SUIT_ShortcutMgr:eventFilter(QObject* theObject, QEvent* theEvent). - If an intercepted action is instance of QtxAction, it is registered automatically. - Since non-QtxActions have no member ID(), SUIT_ShortcutMgr is unable to register them automatically - in SUIT_ShortcutMgr::eventFilter(). Thus, every non-QtxAction should be - passed to SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAction). - - Action ID is application-unique must be composed as /. - If an action belongs to a desktop or is available even if no module is active (e.g. 'Save As'), - use empty string as . Let's call such actions as root actions. - - There is a need to keep multiple actions, which do the same from user' perspective, - bound to the same key sequence. E.g. 'Front view'. Let's call such set of actions as meta-action. - Actions of a meta-action may belong to different modules, and/or there may be several actions - of the same meta-action in the same module. of all members of a meta-action - must be the same and start with "#". - Meta-action is root action when it comes to checking for conflicts. - Shortcuts of meta-actions are (de)serialized to the same section of preference files as root shortcuts. - - can contain several "/". Go to \ref isInModuleActionIDValid(const QString&) for details. - You can refer to multiple actions, whose starts with the prefix. - - Only one module can be active at instance. So a key sequence must be unique within a joined temporary table of - root and active module shortcuts. An action is allowed to be bound with only key sequence. - - ANONYMOUS ACTIONS/SHORTCUTS - - Actions without action IDs or with invalid ones are called anonymous actions. - All anonymous actions with non-empty shortcut key sequences are registered by SUIT_ShortcutMgr. - If a shortcut for an anonymous action clashes with a shortcut for an action with defined ID (identified action/shortcut), - the shortcut for the anonymous action is disabled, but [the anonymous action, the hard-coded key sequence] pair - remains within the SUIT_ShortcutMgr. If user redefines key sequences for identified actions, - and the clash is gone, SUIT_ShortcutMgr enables back the shortcut for the anonymous action. - - WARNING! - Avoid assigning shortcuts to instances of QAction and all its descendants directly. - (1) Key sequence being bound directly to any registered/intercepted action with valid ID, - if the key sequence does not conflict with shortcuts kept by SUIT_ShortcutMgr, - is added to the manager and appears in user preference file. If it does conflict, - it is reassigned with a key sequence from preference files or - disabled and added to user preference files (if the files have no shortcut for the action). - (2) It is not possible to reassign key sequences for anonymous actions using the Shortcut Editor GUI. - It is not possible to always warn user, if a key sequence, he assigns to an identified action, - disables an anonymous shortcut, because SUIT_ShortcutMgr has no data about anonymous actions until they appear in runtime. - To prevent the user from relying on such warnings, they are completely disabled. + The manager is designed to detect shortcut conflicts even for actions, which are not constructed yet. + To do this, data about action shortcuts should be available for the manager prior to construction of actions. + + SHORTCUT PREFERENCES + The data about shortcuts is stored in preference files (see SUIT_ShortcutMgr::setShortcutsFromPreferences()) as maps [action ID]->[key sequence]. + It means, actions should have valid IDs (see SUIT_ShortcutMgr::isActionIDValid(const QString&)). + Let's call such action identified, and actions with invalid IDs anonymous. + Action IDs are language-independent. + + After an action is constructed (even if it is anonymous), it must be registered by the manager. + If the action is instance of QtxAction, the manager registers it automatically (see SUIT_ShortcutMgr:eventFilter(QObject* theObject, QEvent* theEvent)). + Otherwise SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAction) must be called. + + Upon registration of an action, the manager checks, if a key sequence, assigned to the action, clashes with other shortcuts. + If it does, and the action is anonymous - empty keysequnce is set to the action. + If the action is identified - a keysequence from preferences is set to the action, + even if incoming key sequence does not clash with other shortcuts. Absence of an action ID in preference files means + default key sequence is empty. + + If shortcuts are changed (via SUIT_ShortcutMgr::mergeShortcutContainer(const SUIT_ShortcutContainer&, bool, bool, bool)), + the manager serizalizes the changes into user preference file and sets new key sequences to according registered actions. + + ACTION ASSETS + Users are not aware about action IDs, since GUI normally shows user-friendly data: action names, tooltips and icons. Let's call the data assets. + Assets of an action should be available for GUI for shortcut presenting/editing (let's call the piece of GUI Shortcut Editor) even before + the action is constructed. The assets should be provided is asset files (see SUIT_ShortcutMgr::setAssetsFromResources(QString)). + + CONFLICT DETECTION + IDENTIFIED ACTIONS/SHORTCUTS + Let's call GUI module root module. + Only one module, besides the root module, can be active at instance. So a key sequence must be unique within a joined temporary table of + root and active module shortcuts. An action is allowed to be bound with only key sequence. Multiple actions may be registered under the same ID. + + Action ID is application-unique, language-independent and must be composed as /. + If an action belongs to root module (e.g. 'Save As'), use empty string as . + Let's call actions with empty module ID root actions. + + can be composed of several tokens, delimited by "/". See SUIT_ShortcutMgr::isInModuleActionIDValid(const QString&) for details. + Shortcut Editor considers as "path": + without the last token is inModuleID of parent folder or action-folder (see SUIT_ShortcutMgr::setAssetsFromResources(QString)). + + META-ACTIONS + There is a need to keep actions from different modules, which do the same from user' perspective, + bound to the same key sequence. E.g. 'Front view' or 'Undo'. Let's call such set of actions meta-action. + of all members of a meta-action must be the same and the last token of the ID must start with "#". + Meta-action is root action, when it comes to checking for conflicts. + Shortcuts of meta-actions are (de)serialized to the same section of preference files as root shortcuts. + Assets of meta-actions should be placed in asset files of root (GUI) module. + + ANONYMOUS ACTIONS/SHORTCUTS + Actions without action IDs or with invalid ones are called anonymous actions. + All anonymous actions with non-empty shortcut key sequences are registered by SUIT_ShortcutMgr. + If a shortcut for an anonymous action clashes with a shortcut for an action with defined ID (identified action/shortcut), + the shortcut for the anonymous action is disabled, but [the anonymous action, the hard-coded key sequence] pair + remains within the SUIT_ShortcutMgr. If user redefines key sequences for identified actions, + and the clash is gone, SUIT_ShortcutMgr enables back the shortcut for the anonymous action. + + It is not possible to reassign key sequences for anonymous actions using the Shortcut Editor GUI. + It is not possible to always warn users, if a key sequence, they assigns to an identified action, + disables an anonymous shortcut, because SUIT_ShortcutMgr has no data about anonymous actions until they appear in runtime. + To prevent the user from relying on such warnings, they are completely disabled. + + HOW TO USE + 1) Come up with valid action ID for an action and: + 1A) Pass the ID as an agrument to constructor of QtxAction; or + 1B) Call SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAction); or + 1C) Construct the action using SUIT_ShortcutMgr::createAction(..). The latest option allows to avoid duplication of action assets in *.ts files. + 2) Add action assets in asset files. + 3) Add action default keysequence to default preference file. + + DEVELOPMENT + There are two macros: SHORTCUT_MGR_DBG and SHORTCUT_MGR_DEVTOOLS. + SHORTCUT_MGR_DBG enables shortcut-related debug output. + SHORTCUT_MGR_DEVTOOLS enables DevTools class. It assists in making anonymous actions identified and composing asset and default preference files. + + More details can be found in the "SUIT_ShortcutMgr. ReadMe.md". */ class SUIT_EXPORT SUIT_ShortcutMgr: public QObject { @@ -273,10 +504,14 @@ public: Empty module ID is valid - it is root module ID. */ static bool isModuleIDValid(const QString& theModuleID); + static bool isInModuleIDTokenValid(const QString& theInModuleIDToken); + + static bool isInModuleIDTokenMeta(const QString& theInModuleIDToken); + /*! \brief Valid in-module action ID may consist of several tokens, separated by "/": /.../. Each must be non-empty and be equal to QString().simplified(). - Empty or "#" in-module action ID is not valid. */ + Token "#" is also not valid, since the character in the beginning of the last token means action is meta-action. */ static bool isInModuleActionIDValid(const QString& theInModuleActionID); /*! \returns true, is theInModuleActionID starts with "#". */ @@ -287,11 +522,17 @@ public: \returns { _ , "" }, if theActionID is invalid. */ static std::pair splitIntoModuleIDAndInModuleID(const QString& theActionID); + /*! \brief Does not check validity. */ + static QStringList splitIntoTokens(const QString& theRelativeID); + + /*! \brief Does not check validity. */ + static QString joinIntoRelativeID(const QStringList& theTokens); + /*! See \ref splitIntoModuleIDAndInModuleID(const QString&). */ static bool isActionIDValid(const QString& theActionID); /*! \brief Creates application-unique action ID. Reverse to splitIntoModuleIDAndInModuleID. - \returns Emppty string, if either theModuleID or theInModuleActionID is invalid*/ + \returns Empty string, if either theModuleID or theInModuleActionID is invalid*/ static QString makeActionID(const QString& theModuleID, const QString& theInModuleActionID); /*! \brief Sets all shortcuts from preferences to theContainer. Incoming shortcuts override existing ones. @@ -301,12 +542,14 @@ public: \param theDefaultOnly If true, user preferences are ignored and only default preferences are used. */ static void fillContainerFromPreferences(SUIT_ShortcutContainer& theContainer, bool theDefaultOnly); - /*! \brief Checks the resource manager directly. - \returns {assetsExist, assets}. */ - static std::pair getActionAssetsFromResources(const QString& theActionID); + /*! \brief Returns item assets as they are in asset files. + Returned module assets is necessary to keep memory ownership of theAction ancestors. The module assets contain only ancestors of theActionID. + \param theLangs If empty, all languages is parsed. */ + static std::pair, std::shared_ptr> + getActionAssetsFromResources(const QString& theActionID, const std::set& theLangs = {}); /*! \returns Language, which is set in resource manager. */ - static QString getLang(); + static QString currentLang(); /*! \brief Add theAction to map of managed actions. */ @@ -362,7 +605,7 @@ public: const SUIT_ShortcutContainer& getShortcutContainer() const; /*! \brief Does not perform validity checks on theModuleID and theInModuleActionID. */ - void mergeShortcutContainer(const SUIT_ShortcutContainer& theContainer, bool theOverride = true, bool theTreatAbsentIncomingAsDisabled = false); + void mergeShortcutContainer(const SUIT_ShortcutContainer& theContainer, bool theOverride = true, bool theTreatAbsentIncomingAsDisabled = false, bool theSaveToPreferences = true); /*! \brief Get a key sequence mapped to the action. */ const QKeySequence& getKeySequence(const QString& theModuleID, const QString& theInModuleActionID) const; @@ -377,24 +620,25 @@ public: if the module is root (theModuleID is empty) - returns all module IDs, otherwise returns ["", theModuleID]. */ std::set getIDsOfInterferingModules(const QString& theModuleID) const; - /*! \returns assets, which describe module's header, not its content. */ - std::shared_ptr getModuleAssets(const QString& theModuleID) const; + std::shared_ptr getModuleAssets(const QString& theModuleID) const; + const std::map>& getModuleAssets() const { return myModuleAssets; } - /*! \returns assets, which describe modules' headers, not their content. */ - std::map> getModuleAssets() const { return myModuleAssets; } - - /*! \brief Retrieves module name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */ + /*! \brief Retrieves module name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is current language. */ QString getModuleName(const QString& theModuleID, const QString& theLang = "") const; - std::shared_ptr getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const; + std::shared_ptr getActionAssets(const QString& theModuleID, const QString& theInModuleActionID, bool theTryToCreateRuntimeAssetsIfAbsent = true) const; - std::shared_ptr getActionAssets(const QString& theActionID) const; + std::shared_ptr getActionAssets(const QString& theActionID, bool theTryToCreateRuntimeAssetsIfAbsent = true) const; - std::map>> getActionAssets() const { return myActionAssets; } + /*! \brief Creates assets using action instance fields, if corresponding action is registered. */ + std::shared_ptr createRuntimeActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const; - /*! \brief Retrieves action name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */ + /*! \brief Retrieves action name, if the asset is myModuleAssets. Name of module is not included. If theLang is empty, it is current language. */ QString getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang = "") const; + /*! \brief Creates an action and sets asset data to the action. */ + QtxAction* createAction(QObject* theParent, QObject* theReceiver, const char* theReceiverSlot, const QString& theActionID, const bool theIsToggle = false) const; + private slots: /*! \brief Called when the corresponding action is destroyed. @@ -415,7 +659,7 @@ private: /*! \brief Set shortcuts from preference files. The method is intended to be called before any calls to setShortcut() or mergeShortcutContainer(). Definition of this method assumes, that shortcut settings are serialized as prerefence entries {name=, val=} - in dedicated section for each module, with names of sections being composed as "shortcuts:". + in dedicated section for each module, with names of sections being composed as "shortcuts_vA1.0:". E.g. in case of XML file it may look like this: -
- - - - - - +
+ + + + +
-
- - - - +
+ + + + + +
Empty inModuleActionIDs are ignored. - - nb! For any theQtxAction you wish user be able to assign it to a shortcut, - add theQtxAction.ID() to default resource files (you can map it to no key sequence).*/ + */ void setShortcutsFromPreferences(); /*! \brief Writes shortcuts to preference files. @@ -455,26 +698,58 @@ private: OldKeySequences are ignored. */ static void saveShortcutsToPreferences(const std::map>>& theShortcutsInversed); - /*! Fills myActionAssets from asset files in theLanguage. - \param theLanguage If default, fills assets in current language. - If an asset in requested language is not found, seeks for the asset EN in and then in FR. + /*! Fills myModuleAssets from asset files in theLanguage and EN. + \param theLanguage If empty, fills assets in current language and EN. Asset files must be structured like this: { - ... - actionID : { + moduleID: { + "iconPath": iconPath, "langDependentAssets": { ... lang: { - "name": name, - "tooltip": tooltip + "name": moduleName, + "tooltip": moduleTooltip }, ... }, - "iconPath": iconPath - }, - ... + "children": { + ... + actionA_IDLastToken : { + "iconPath": iconPath, + "langDependentAssets": { + ... + lang: { + "name": actionName, + "tooltip": actionTooltip + }, + ... + }, + "children": { // The action has nested actions. + actionB_IDLastToken: { + ... + } + } + }, + ... + folderC_IDLastToken: { + "isAction": false, // The folder is pure folder. + "iconPath": iconPath, + "langDependentAssets": { + ... + }, + children: { + ... + } + } + ... + } + } } + + The JSON above describes an action-folder with ID "moduleID/actionA_IDLastToken" and a pure folder with ID "moduleID/folderC_IDLastToken". + The action-folder has a nested action with "moduleID/actionA_IDLastToken//actionB_IDLastToken". + Requirements for action' and folder' IDs are the same. */ void setAssetsFromResources(QString theLanguage = QString()); @@ -500,11 +775,11 @@ private: Sets of moduleIDs and inModuleActionIDs may NOT be equal for myActions and myShortcutContainer. */ - /** { moduleID, {inModuleActionID, assets}[] }[] */ - std::map>> myActionAssets; + /** {moduleID, moduleAssets}[]. Structured assets: module assets refer to folders and actions, folders and actions refer to nested ones, etc. */ + mutable std::map> myModuleAssets; - /** {moduleID, assets}[] */ - mutable std::map> myModuleAssets; + /** True, if SUIT_ShortcutMgr::setAssetsFromResources(QString) was called. */ + bool myAssetsLoaded; mutable std::set myActiveModuleIDs; @@ -518,75 +793,9 @@ private: }; -/*! - \class SUIT_SentenceMatcher - \brief Approximate string matcher, treats strings as sentences composed of words. -*/ -class SUIT_EXPORT SUIT_SentenceMatcher -{ -public: - /*! Default config: - Exact word order = false; - Fuzzy words = true; - Case sensitive = false; - Query = ""; // matches nothing. - */ - SUIT_SentenceMatcher(); - - void setUseExactWordOrder(bool theOn); - void setUseFuzzyWords(bool theOn); - void setCaseSensitive(bool theOn); - inline bool isCaseSensitive() const { return myIsCaseSensitive; }; - - /*! \param theQuery should not be regex. */ - void setQuery(QString theQuery); - - inline const QString& getQuery() const { return myQuery; }; - - /*! \returns match metrics. The metrics >= 0. INF means mismatch. - The class is unable to differentiate exact match with some approximate matches! */ - double match(const QString& theInputString) const; - - /** \brief For debug. */ - QString toString() const; - -private: - static bool makePermutatedSentences(const QStringList& theWords, QList& theSentences); - static void makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords); - - /*! \returns number of characters in matched words. The number >= 0. */ - static int matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive); - /*! \returns number of characters in matched words. The number >= 0. */ - static int matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive); - - /*! \returns number of characters in matched words. The number >= 0. */ - static int matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive); - - /*! \returns number of characters in matched words. The number >= 0. */ - static int match( - const QString& theInputString, - const QStringList& theSentence, - bool theCaseSensitive - ); - - /*! \returns number of characters in matched words. The number >= 0. */ - static int match( - const QString& theInputString, - const QList& theSentences, - bool theCaseSensitive - ); - - bool myUseExactWordOrder; // If false, try to match with sentences, composed of query's words in different orders. - bool myUseFuzzyWords; // Try to match with sentences, composed of query's truncated words. - bool myIsCaseSensitive; - QString myQuery; - - QStringList myWords; // It is also original search sentence. - QList myPermutatedSentences; - - QStringList myFuzzyWords; // Regexes. - QList myFuzzyPermutatedSentences; -}; +namespace SUIT_tools { + class SUIT_SentenceMatcher; +} /*! @@ -598,7 +807,7 @@ class SUIT_EXPORT SUIT_ActionSearcher public: enum MatchField { ID, - Name, + Name, // Also matches with path, composed as /....../. ToolTip, KeySequence }; @@ -606,12 +815,12 @@ public: class AssetsAndSearchData { public: - AssetsAndSearchData(std::shared_ptr theAssets = nullptr, double theMatchMetrics = std::numeric_limits::infinity()); + AssetsAndSearchData(std::shared_ptr theAssets = nullptr, double theMatchMetrics = std::numeric_limits::infinity()); void setMatchMetrics(double theMatchMetrics); double matchMetrics() const { return myMatchMetrics; }; - std::shared_ptr myAssets; + std::shared_ptr myAssets; void toJSON(QJsonObject& oJsonObject) const; QString toString() const; @@ -621,35 +830,50 @@ public: double myMatchMetrics; }; + + static double matchKeySequenceString(const QString& theQuery, const QString& theKeySequence); + /*! Default config: Included modules' IDs = { ROOT_MODULE_ID }; Include disabled actions = false; Fields to match = { Name, Tooltip }; Case sensitive = false; Fuzzy matching = true; - Query = ""; // matches everything. + Query = ""; // matches nothing. */ SUIT_ActionSearcher(); SUIT_ActionSearcher(const SUIT_ActionSearcher&) = delete; SUIT_ActionSearcher& operator=(const SUIT_ActionSearcher&) = delete; - virtual ~SUIT_ActionSearcher() = default; + virtual ~SUIT_ActionSearcher(); - /*! \returns true, if set of results is changed. */ - bool setIncludedModuleIDs(std::set theIncludedModuleIDs); + /*! \returns true, if set of results is changed. + \param doNotUpdateResults Set to true to initialize the instance without unnececessary computations. */ + bool setIncludedModuleIDs(std::set theIncludedModuleIDs, bool doNotUpdateResults = false); - /*! \returns true, if set of results is changed. */ - bool includeDisabledActions(bool theOn); + /*! \returns true, if set of results is changed. + \param doNotUpdateResults Set to true to initialize the instance without unnececessary computations. */ + bool includeDisabledActions(bool theOn, bool doNotUpdateResults = false); inline bool areDisabledActionsIncluded() const {return myIncludeDisabledActions;}; - /*! \returns true, if set of results is changed. */ - bool setFieldsToMatch(const std::set& theFields); + /*! \returns true, if set of results is changed. + \param doNotUpdateResults Set to true to initialize the instance without unnececessary computations. */ + bool setFieldsToMatch(const std::set& theFields, bool doNotUpdateResults = false); - /*! \returns true, if set of results is changed. */ - bool setCaseSensitive(bool theOn); + /*! \returns { true, _ } if set of results is changed; { _ , true } if matching metrics is changed for at least one result. + \param theKeySequenceGetter getKeySequence(theModuleID, theInModuleActionID). If empty, a default getter, retrieving key sequence from ShortcutMgr, is set. + \param doNotUpdateResults Set to true to initialize the instance without unnececessary computations. */ + std::pair setKeySequenceGetter( + const std::function& theKeySequenceGetter = std::function(), + bool doNotUpdateResults = false + ); + + /*! \returns true, if set of results is changed. + \param doNotUpdateResults Set to true to initialize the instance without unnececessary computations. */ + bool setCaseSensitive(bool theOn, bool doNotUpdateResults = false); /*! \returns true, if set of results is changed. */ bool setQuery(const QString& theQuery); - inline const QString& getQuery() const {return myMatcher.getQuery();}; + inline const QString& getQuery() const; const std::map>& getSearchResults() const; @@ -667,7 +891,7 @@ private: \returns True, if set of results is extended. */ bool extendResults(); - double matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr theAssets); + double matchAction(const SUIT_ShortcutItemAssets& theAssets); QString toString() const; @@ -676,7 +900,10 @@ private: bool myIncludeDisabledActions; std::set myFieldsToMatch; - SUIT_SentenceMatcher myMatcher; + ::SUIT_tools::SUIT_SentenceMatcher* myMatcher; + + /* getKeySequence(theModuleID, theInModuleActionID) */ + std::function myKeySequenceGetter; /* { moduleID, {inModuleActionID, assetsAndSearchData}[] }[]. */ std::map> mySearchResults; @@ -687,4 +914,4 @@ private: #pragma warning( default: 4251 ) #endif -#endif +#endif //SUIT_SHORTCUTMGR_H diff --git a/src/SUIT/SUIT_ShortcutTree.cxx b/src/SUIT/SUIT_ShortcutTree.cxx deleted file mode 100644 index 5e7f321f9..000000000 --- a/src/SUIT/SUIT_ShortcutTree.cxx +++ /dev/null @@ -1,880 +0,0 @@ -// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 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 -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -// -// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com -// - -#include "SUIT_ShortcutTree.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - - -#define COLUMN_SIZE 500 - - -SUIT_KeySequenceEdit::SUIT_KeySequenceEdit(QWidget* parent) -: QFrame(parent) -{ - initialize(); - myKeySequenceLineEdit->installEventFilter(this); -} - -/*! \brief Set a key sequence to edit. */ -void SUIT_KeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence) -{ - myConfirmedKeySequenceString = theKeySequence.toString(); - myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); - myPrevKeySequenceString = myConfirmedKeySequenceString; -} - -void SUIT_KeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence) -{ - const QString keySequenceString = theKeySequence.toString(); - myKeySequenceLineEdit->setText(keySequenceString); - myPrevKeySequenceString = keySequenceString; -} - -QKeySequence SUIT_KeySequenceEdit::editedKeySequence() const -{ - return QKeySequence::fromString(myKeySequenceLineEdit->text()); -} - -/*! \returns true, if the edited key sequence differs from confirmed one. */ -bool SUIT_KeySequenceEdit::isKeySequenceModified() const -{ - return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence(); -} - -/*! \brief Set confirmed key sequence to line editor. */ -void SUIT_KeySequenceEdit::restoreKeySequence() -{ - myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); - myPrevKeySequenceString = myConfirmedKeySequenceString; -} - -/*! - \brief Gets the key sequence from keys that were pressed - \param e a key event - \returns a string representation of the key sequence -*/ -/*static*/ QString SUIT_KeySequenceEdit::parseEvent(QKeyEvent* e) -{ - bool isShiftPressed = e->modifiers() & Qt::ShiftModifier; - bool isControlPressed = e->modifiers() & Qt::ControlModifier; - bool isAltPressed = e->modifiers() & Qt::AltModifier; - bool isMetaPressed = e->modifiers() & Qt::MetaModifier; - bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier! - int result=0; - if(isControlPressed) - result += Qt::CTRL; - if(isAltPressed) - result += Qt::ALT; - if(isShiftPressed) - result += Qt::SHIFT; - if(isMetaPressed) - result += Qt::META; - - int aKey = e->key(); - if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12))) - result += aKey; - - return QKeySequence(result).toString(); -} - -/*! - \brief Check if the key event contains a 'valid' key - \param theKey the code of the key - \returns \c true if the key is 'valid' -*/ -/*static*/ bool SUIT_KeySequenceEdit::isValidKey(int theKey) -{ - if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape || - ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) || - ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) || - ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 ) || - ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) || - ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_AsciiTilde ) ) - return true; - return false; -} - -/*! \brief Called when "Clear" button is clicked. */ -void SUIT_KeySequenceEdit::onClear() -{ - myKeySequenceLineEdit->setText(""); - myPrevKeySequenceString = ""; - emit editingFinished(); -} - -/*! \brief Called when myKeySequenceLineEdit loses focus. */ -void SUIT_KeySequenceEdit::onEditingFinished() -{ - if (myKeySequenceLineEdit->text().endsWith("+")) - myKeySequenceLineEdit->setText(myPrevKeySequenceString); - else - myPrevKeySequenceString = myKeySequenceLineEdit->text(); - emit editingFinished(); -} - -/*! - \brief Custom event filter. - \param obj event receiver object - \param event event - \returns \c true if further event processing should be stopped -*/ -bool SUIT_KeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent) -{ - if (theObject == myKeySequenceLineEdit) { - if (theEvent->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(theEvent); - QString text = parseEvent(keyEvent); - if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace) - myKeySequenceLineEdit->setText(""); - if (!text.isEmpty()) - myKeySequenceLineEdit->setText(text); - - emit editingStarted(); - return true; - } - if (theEvent->type() == QEvent::KeyRelease) { - onEditingFinished(); - return true; - } - } - return false; -} - -/* - \brief Perform internal intialization. -*/ -void SUIT_KeySequenceEdit::initialize() -{ - static const int PIXMAP_SIZE = 30; - - QHBoxLayout* base = new QHBoxLayout( this ); - base->setMargin(0); - base->setSpacing(5); - - base->addWidget(myKeySequenceLineEdit = new QLineEdit(this)); - setFocusProxy(myKeySequenceLineEdit); - - QToolButton* clearBtn = new QToolButton(); - auto clearPixmap = QPixmap(":/images/shortcut_disable.svg"); - clearPixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); - clearBtn->setIcon(clearPixmap); - clearBtn->setToolTip(tr("Disable shortcut.")); - base->addWidget(clearBtn); - - QToolButton* restoreBtn = new QToolButton(); - auto restorePixmap = QPixmap(":/images/shortcut_restore.svg"); - restorePixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); - restoreBtn->setIcon(restorePixmap); - restoreBtn->setToolTip(tr("Restore the currently applied key sequence.")); - base->addWidget(restoreBtn); - - myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear())); - connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked())); - connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); -} - - -/*! \param theParent must not be nullptr. */ -SUIT_EditKeySequenceDialog::SUIT_EditKeySequenceDialog(SUIT_ShortcutTree* theParent) -: QDialog(theParent) -{ - setMinimumWidth(500); - setWindowTitle(tr("Change key sequence")); - QVBoxLayout* layout = new QVBoxLayout(this); - myActionName = new QLabel(this); - myActionName->setTextFormat(Qt::RichText); - myKeySequenceEdit = new SUIT_KeySequenceEdit(this); - myTextEdit = new QTextEdit(this); - layout->addWidget(myActionName); - layout->addWidget(myKeySequenceEdit); - layout->addWidget(myTextEdit); - myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - myTextEdit->setReadOnly(true); - myTextEdit->setAcceptRichText(true); - myTextEdit->setPlaceholderText(tr("No conflicts.")); - setFocusProxy(myKeySequenceEdit); - - QHBoxLayout* buttonLayout = new QHBoxLayout(this); - layout->addLayout(buttonLayout); - QPushButton* confirmButton = new QPushButton(tr("Confirm"), this); - confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - QPushButton* cancelButton = new QPushButton(tr("Cancel"), this); - cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - buttonLayout->addStretch(); - buttonLayout->addWidget(confirmButton); - buttonLayout->addWidget(cancelButton); - - connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted())); - connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); - connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr())); - connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm())); - connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); -} - -void SUIT_EditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID) -{ - myModuleID = theModuleID; - myInModuleActionID = theInModuleActionID; -} - -const QString& SUIT_EditKeySequenceDialog::moduleID() const { return myModuleID; } -const QString& SUIT_EditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; } - -void SUIT_EditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip) -{ - myActionName->setText("" + theModuleName + "  " + theActionName); - myActionName->setToolTip(theActionToolTip); -} - -void SUIT_EditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence) -{ - myKeySequenceEdit->setConfirmedKeySequence(theSequence); -} - -QKeySequence SUIT_EditKeySequenceDialog::editedKeySequence() const -{ - return myKeySequenceEdit->editedKeySequence(); -} - -int SUIT_EditKeySequenceDialog::exec() -{ - myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason); - return QDialog::exec(); -} - -void SUIT_EditKeySequenceDialog::onEditingStarted() -{ - myTextEdit->setEnabled(false); -} - -void SUIT_EditKeySequenceDialog::onEditingFinished() -{ - updateConflictsMessage(); -} - -void SUIT_EditKeySequenceDialog::onRestoreFromShortcutMgr() -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - myKeySequenceEdit->setEditedKeySequence(shortcutMgr->getKeySequence(myModuleID, myInModuleActionID)); - updateConflictsMessage(); -} - -/*! Updates message with list of actions, whose shortcuts will be disabled on Confirm. */ -void SUIT_EditKeySequenceDialog::updateConflictsMessage() -{ - myTextEdit->setEnabled(true); - QTextDocument* doc = myTextEdit->document(); - if (!doc) { - doc = new QTextDocument(myTextEdit); - myTextEdit->setDocument(doc); - } - - if (!myKeySequenceEdit->isKeySequenceModified()) { - doc->clear(); - return; - } - - const QKeySequence newKeySequence = editedKeySequence(); - - const auto shortcutTree = static_cast(parentWidget()); - /** {moduleID, inModuleActionID}[] */ - std::set> conflicts = shortcutTree->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence); - if (!conflicts.empty()) { - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - - QString report = "" + tr("These shortcuts will be disabled on confirm:") + ""; - { - report += "
    "; - for (const auto& conflict : conflicts) { - const QString conflictingModuleName = shortcutMgr->getModuleName(conflict.first); - const QString conflictingActionName = shortcutMgr->getActionName(conflict.first, conflict.second); - report += "
  • " + conflictingModuleName + "  " + conflictingActionName + "
  • "; - } - report += "
"; - } - doc->setHtml(report); - } - else /* if no conflicts */ { - doc->clear(); - } -} - -void SUIT_EditKeySequenceDialog::onConfirm() -{ - if (myKeySequenceEdit->isKeySequenceModified()) - accept(); - else - reject(); -} - - -/*! \brief Compensates lack of std::distance(), which is introduced in C++17. -\returns -1, if theIt does not belong to the */ -template -size_t indexOf( - const Container& theContainer, - const typename Container::iterator& theIt -) { - auto it = theContainer.begin(); - size_t distance = 0; - while (it != theContainer.end()) { - if (it == theIt) - return distance; - - it++; - distance++; - } - return -1; -} - - -/*! \param theContainer Share the same container between several trees, -to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr. -Pass nullptr to create non-synchronized tree. */ -SUIT_ShortcutTree::SUIT_ShortcutTree( - std::shared_ptr theContainer, - QWidget* theParent -) : QTreeWidget(theParent), -myShortcutContainer(theContainer ? theContainer : std::shared_ptr(new SUIT_ShortcutContainer())), -mySortKey(SUIT_ShortcutTree::SortKey::Name), mySortOrder(SUIT_ShortcutTree::SortOrder::Ascending) -{ - setColumnCount(2); - setSelectionMode(QAbstractItemView::SingleSelection); - setColumnWidth(0, COLUMN_SIZE); - setSortingEnabled(false); // Items are sorted in the same way, as in ShortcutContainer. - header()->setSectionResizeMode(QHeaderView::Interactive); - { - QMap labelMap; - labelMap[SUIT_ShortcutTree::ElementIdx::Name] = tr("Action"); - labelMap[SUIT_ShortcutTree::ElementIdx::KeySequence] = tr("Key sequence"); - setHeaderLabels(labelMap.values()); - } - setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead. - setSortingEnabled(false); - setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - myEditDialog = new SUIT_EditKeySequenceDialog(this); - - this->installEventFilter(this); - connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); - - SUIT_ShortcutTree::instances[myShortcutContainer.get()].emplace(this); -} - -SUIT_ShortcutTree::~SUIT_ShortcutTree() -{ - SUIT_ShortcutTree::instances[myShortcutContainer.get()].erase(this); - if (SUIT_ShortcutTree::instances[myShortcutContainer.get()].empty()) - SUIT_ShortcutTree::instances.erase(myShortcutContainer.get()); -} - -/*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */ -void SUIT_ShortcutTree::setShortcutsFromManager() -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - *myShortcutContainer = shortcutMgr->getShortcutContainer(); - // nb! ShortcutMgr never removes shortcuts from its container, only disables. - - updateItems(false /*theHighlightModified*/, true /*theUpdateSyncTrees*/); -} - -/*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */ -void SUIT_ShortcutTree::setDefaultShortcuts() -{ - SUIT_ShortcutContainer defaultShortcuts; - SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/); - - myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/); - // nb! SUIT_ShortcutContainer never erases shortcuts, only disables. - - updateItems(true /*theHighlightModified*/, true /*theUpdateSyncTrees*/); -} - -/*! \brief Applies pending changes to ShortcutMgr. Updates other instances of SUIT_ShortcutTree. */ -void SUIT_ShortcutTree::applyChangesToShortcutMgr() -{ - const auto mgr = SUIT_ShortcutMgr::get(); - mgr->mergeShortcutContainer(*myShortcutContainer); - - // Update non-synchronized with this instances. - for (const auto& containerAndSyncTrees : SUIT_ShortcutTree::instances) { - if (containerAndSyncTrees.first == myShortcutContainer.get()) - continue; - - const std::set& syncTrees = containerAndSyncTrees.second; - const auto itFirstSyncTree = syncTrees.begin(); - if (itFirstSyncTree == syncTrees.end()) - continue; - - (*itFirstSyncTree)->setShortcutsFromManager(); - const auto editDialog = (*itFirstSyncTree)->myEditDialog; - editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); - editDialog->updateConflictsMessage(); - } -} - -std::shared_ptr SUIT_ShortcutTree::shortcutContainer() const -{ - return myShortcutContainer; -} - -/*! \brief Does not sort modules. */ -void SUIT_ShortcutTree::sort(SUIT_ShortcutTree::SortKey theKey, SUIT_ShortcutTree::SortOrder theOrder) -{ - if (theKey == mySortKey && theOrder == mySortOrder) - return; - - mySortKey = theKey; - mySortOrder = theOrder; - - for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { - const auto moduleItem = static_cast(topLevelItem(moduleIdx)); - const auto sortedChildren = getSortedChildren(moduleItem); - moduleItem->takeChildren(); - - for (const auto childItem : sortedChildren) { - moduleItem->addChild(childItem); - } - } -} - -/*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */ -void SUIT_ShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees) -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - const QString lang = SUIT_ShortcutMgr::getLang(); - - for (const QString& moduleID : myModuleIDs) { - const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID); - if (moduleShortcuts.empty()) { - // Do not display empty module. - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - if (moduleItemAndIdx.second >= 0) - delete takeTopLevelItem(moduleItemAndIdx.second); - - continue; - } - - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - SUIT_ShortcutTreeFolder* moduleItem = moduleItemAndIdx.first; - if (!moduleItem) { - moduleItem = new SUIT_ShortcutTreeFolder(moduleID); - moduleItem->setAssets(shortcutMgr->getModuleAssets(moduleID), lang); - addTopLevelItem(moduleItem); - moduleItem->setFlags(Qt::ItemIsEnabled); - - auto sortedChildren = getSortedChildren(moduleItem); - for (const auto& shortcut : moduleShortcuts) { - const QString& inModuleActionID = shortcut.first; - const QKeySequence& keySequence = shortcut.second; - const QString keySequenceString = keySequence.toString(); - - auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID); - if (!actionItem) { - ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); - continue; - } - - actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); - actionItem->setKeySequence(keySequenceString); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); - actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); - } - - insertChild(moduleItem, sortedChildren, actionItem); - } - - moduleItem->setExpanded(true); // Make tree expanded on first show. - } - else /* if the tree has the module-item */ { - for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { - // Update exisiting items of a module. - SUIT_ShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); - const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID); - if (itShortcut == moduleShortcuts.end()) { - // Shortcut of the item has been removed from myShortcutContainer - impossible. - continue; - } - const QKeySequence& newKeySequence = itShortcut->second; - const QString newKeySequenceString = newKeySequence.toString(); - if (childItem->keySequence() != newKeySequenceString) - childItem->setKeySequence(newKeySequenceString); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); - childItem->highlightKeySequenceAsModified(newKeySequence != appliedKeySequence); - } - else - childItem->highlightKeySequenceAsModified(false); - } - - // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot - // to add shortcuts for registered actions to resource files. - if (moduleItem->childCount() < moduleShortcuts.size()) { - auto sortedChildren = getSortedChildren(moduleItem); - for (const auto& shortcut : moduleShortcuts) { - const QString& inModuleActionID = shortcut.first; - const auto predicate = [&inModuleActionID](const SUIT_ShortcutTreeItem* const theItem) -> bool { - return static_cast(theItem)->myInModuleActionID == inModuleActionID; - }; - - if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) { - const auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID); - if (!actionItem) { - ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); - continue; - } - - const QKeySequence& keySequence = shortcut.second; - actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); - actionItem->setKeySequence(keySequence.toString()); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); - actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); - } - - insertChild(moduleItem, sortedChildren, actionItem); - } - } - } - } - } - - if (theUpdateSyncTrees) { - const std::set& syncTrees = SUIT_ShortcutTree::instances[myShortcutContainer.get()]; - for (const auto syncTree: syncTrees) { - if (syncTree == this) - continue; - - syncTree->updateItems(theHighlightModified, false /*theUpdateSyncTrees*/); - const auto editDialog = syncTree->myEditDialog; - editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); - editDialog->updateConflictsMessage(); - } - } -} - -/*! \returns Pointer and index of top-level item. -If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */ -std::pair SUIT_ShortcutTree::findModuleFolderItem(const QString& theModuleID) const -{ - for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { - SUIT_ShortcutTreeFolder* moduleItem = static_cast(topLevelItem(moduleIdx)); - if (moduleItem->myModuleID == theModuleID) - return std::pair(moduleItem, moduleIdx); - } - return std::pair(nullptr, -1); -} - -/*! \returns Children of theParentItem being sorted according to current sort mode and order. */ -std::set> SUIT_ShortcutTree::getSortedChildren(SUIT_ShortcutTreeFolder* theParentItem) -{ - QList> sortSchema = SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA; - { - for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) { - if (itSameKey->first == mySortKey) { - sortSchema.erase(itSameKey); - break; - } - } - sortSchema.push_front(std::pair(mySortKey, mySortOrder)); - } - - const std::function comparator = - [this, sortSchema](const SUIT_ShortcutTreeItem* theItemA, const SUIT_ShortcutTreeItem* theItemB) { - const auto collator = QCollator(); - for (const auto& keyAndOrder : sortSchema) { - const int res = collator.compare(theItemA->getValue(keyAndOrder.first), theItemB->getValue(keyAndOrder.first)); - if (res != 0) - return keyAndOrder.second == SUIT_ShortcutTree::SortOrder::Ascending ? res < 0 : res > 0; - } - return false; - }; - - std::set> sortedChildren(comparator); - for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) { - SUIT_ShortcutTreeAction* const childItem = static_cast(theParentItem->child(childIdx)); - sortedChildren.emplace(childItem); - } - return sortedChildren; -} - -/*! \brief Inserts theChildItem to theParentItem and theSortedChildren. -Does not check whether theSortedChildren are actually child items of theParentItem. -Does not check whether current item sort schema is same as one of theSortedChildren. */ -void SUIT_ShortcutTree::insertChild( - SUIT_ShortcutTreeFolder* theParentItem, - std::set>& theSortedChildren, - SUIT_ShortcutTreeItem* theChildItem -) { - auto emplaceRes = theSortedChildren.emplace(theChildItem); - theParentItem->insertChild(indexOf(theSortedChildren, emplaceRes.first), theChildItem); -} - -void SUIT_ShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx) -{ - { - SUIT_ShortcutTreeItem* const item = static_cast(theItem); - // Do not react if folder-item is clicked. - if (item->type() != SUIT_ShortcutTreeItem::Type::Action) - return; - } - - SUIT_ShortcutTreeAction* const actionItem = static_cast(theItem); - - myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID); - QString actionToolTip = actionItem->toolTip(SUIT_ShortcutTree::ElementIdx::Name); - actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1); - myEditDialog->setModuleAndActionName( - static_cast(actionItem->parent())->name(), - actionItem->name(), - actionToolTip - ); - myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(actionItem->keySequence())); - myEditDialog->updateConflictsMessage(); - const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted; - - if (!somethingChanged) - return; - - const QKeySequence newKeySequence = myEditDialog->editedKeySequence(); - - /** { moduleID, inModuleActionID }[] */ - std::set> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/); - - /** { moduleID, {inModuleActionID, keySequence}[] }[] */ - std::map> changes; - changes[actionItem->myModuleID][actionItem->myInModuleActionID] = newKeySequence.toString(); - for (const auto moduleAndActionID : disabledActionIDs) { - changes[moduleAndActionID.first][moduleAndActionID.second] = QString(); - } - - // Set new key sequences to shortcut items. - for (const auto& moduleIDAndChanges : changes) { - const QString& moduleID = moduleIDAndChanges.first; - - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - const auto moduleItem = moduleItemAndIdx.first; - if (!moduleItem) - continue; - - /** {inModuleActionID, newKeySequence}[] */ - const std::map& moduleChanges = moduleIDAndChanges.second; - - // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences. - for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { - SUIT_ShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); - const auto itChange = moduleChanges.find(childItem->myInModuleActionID); - if (itChange == moduleChanges.end()) { - // The shortcut has not been changed. - continue; - } - - childItem->setKeySequence(itChange->second); - - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); - childItem->highlightKeySequenceAsModified(QKeySequence::fromString(itChange->second) != appliedKeySequence); - } - } -} - -/*static*/ const QList> SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA = -{ - {SUIT_ShortcutTree::SortKey::Name, SUIT_ShortcutTree::SortOrder::Ascending}, - {SUIT_ShortcutTree::SortKey::ToolTip, SUIT_ShortcutTree::SortOrder::Ascending}, - {SUIT_ShortcutTree::SortKey::KeySequence, SUIT_ShortcutTree::SortOrder::Ascending}, - {SUIT_ShortcutTree::SortKey::ID, SUIT_ShortcutTree::SortOrder::Ascending} -}; - -/*static*/ std::map> SUIT_ShortcutTree::instances = -std::map>(); - - - -SUIT_ShortcutTreeItem::SUIT_ShortcutTreeItem(const QString& theModuleID) -: QTreeWidgetItem(), myModuleID(theModuleID) -{ } - -QString SUIT_ShortcutTreeItem::name() const -{ - return text(SUIT_ShortcutTree::ElementIdx::Name); -} - - -SUIT_ShortcutTreeFolder::SUIT_ShortcutTreeFolder(const QString& theModuleID) -: SUIT_ShortcutTreeItem(theModuleID) -{ - QFont f = font(SUIT_ShortcutTree::ElementIdx::Name); - f.setBold(true); - setFont(SUIT_ShortcutTree::ElementIdx::Name, f); - setText(SUIT_ShortcutTree::ElementIdx::Name, theModuleID); -} - -void SUIT_ShortcutTreeFolder::setAssets(std::shared_ptr theAssets, const QString& theLang) -{ - if (!theAssets) - return; - - setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon); - - const auto& ldaMap = theAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(SUIT_ShortcutTree::ElementIdx::Name, myModuleID); - return; - } - - auto itLDA = ldaMap.find(theLang); - if (itLDA == ldaMap.end()) - itLDA = ldaMap.begin(); - - const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second; - const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName; - setText(SUIT_ShortcutTree::ElementIdx::Name, name); -} - -QString SUIT_ShortcutTreeFolder::getValue(SUIT_ShortcutTree::SortKey theKey) const -{ - switch (theKey) { - case SUIT_ShortcutTree::SortKey::ID: - return myModuleID; - case SUIT_ShortcutTree::SortKey::Name: - return name(); - case SUIT_ShortcutTree::SortKey::ToolTip: - return name(); - default: - return QString(); - } -} - - -SUIT_ShortcutTreeAction::SUIT_ShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID) -: SUIT_ShortcutTreeItem(theModuleID), myInModuleActionID(theInModuleActionID) -{ - setText(SUIT_ShortcutTree::ElementIdx::Name, theInModuleActionID); - setToolTip( - SUIT_ShortcutTree::ElementIdx::Name, - theInModuleActionID + (theInModuleActionID.at(theInModuleActionID.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.") - ); - setToolTip(SUIT_ShortcutTree::ElementIdx::KeySequence, SUIT_ShortcutTree::tr("Double click to edit key sequence.")); -} - -/*static*/ SUIT_ShortcutTreeAction* SUIT_ShortcutTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID) -{ - if (theInModuleActionID.isEmpty()) { - ShCutDbg("SUIT_ShortcutTreeItem: attempt to create item with empty action ID."); - return nullptr; - } - - return new SUIT_ShortcutTreeAction(theModuleID, theInModuleActionID); -} - -void SUIT_ShortcutTreeAction::setAssets(std::shared_ptr theAssets, const QString& theLang) -{ - if (!theAssets) - return; - - setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon); - - const auto& ldaMap = theAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(SUIT_ShortcutTree::ElementIdx::Name, myInModuleActionID); - return; - } - - auto itLDA = ldaMap.find(theLang); - if (itLDA == ldaMap.end()) - itLDA = ldaMap.begin(); - - const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second; - const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName; - setText(SUIT_ShortcutTree::ElementIdx::Name, name); - - const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip; - setToolTip( - SUIT_ShortcutTree::ElementIdx::Name, - actionToolTip + (actionToolTip.at(actionToolTip.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.") - ); -} - -QString SUIT_ShortcutTreeAction::getValue(SUIT_ShortcutTree::SortKey theKey) const -{ - switch (theKey) { - case SUIT_ShortcutTree::SortKey::ID: - return myInModuleActionID; - case SUIT_ShortcutTree::SortKey::Name: - return name(); - case SUIT_ShortcutTree::SortKey::ToolTip: - return toolTip(SUIT_ShortcutTree::ElementIdx::Name); - case SUIT_ShortcutTree::SortKey::KeySequence: - return keySequence(); - default: - return QString(); - } -} - -void SUIT_ShortcutTreeAction::setKeySequence(const QString& theKeySequence) -{ - setText(SUIT_ShortcutTree::ElementIdx::KeySequence, theKeySequence); -} - -QString SUIT_ShortcutTreeAction::keySequence() const -{ - return text(SUIT_ShortcutTree::ElementIdx::KeySequence); -} - -/*! \brief Highlights text at ElementIdx::KeySequence. */ -void SUIT_ShortcutTreeAction::highlightKeySequenceAsModified(bool theHighlight) -{ - static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen); - static const QBrush fgHighlitingBrush = QBrush(Qt::white); - static const QBrush noBrush = QBrush(); - - setBackground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush); - setForeground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush); -} \ No newline at end of file diff --git a/src/SUIT/SUIT_ShortcutTree.h b/src/SUIT/SUIT_ShortcutTree.h deleted file mode 100644 index 7d2df2238..000000000 --- a/src/SUIT/SUIT_ShortcutTree.h +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; either -// version 2.1 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 -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -// -// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com -// - -#ifndef SUIT_SHORTCUTTREE_H -#define SUIT_SHORTCUTTREE_H - -#include "SUIT.h" -#include -#include -#include -#include "SUIT_ShortcutMgr.h" -#include -#include -#include -#include - - -class QLineEdit; -class QLabel; -class QPushButton; -class QTreeWidgetItem; - -class SUIT_EXPORT SUIT_KeySequenceEdit : public QFrame -{ - Q_OBJECT - -public: - SUIT_KeySequenceEdit(QWidget* = nullptr); - virtual ~SUIT_KeySequenceEdit() = default; - - void setConfirmedKeySequence(const QKeySequence&); - void setEditedKeySequence(const QKeySequence&); - QKeySequence editedKeySequence() const; - bool isKeySequenceModified() const; - void restoreKeySequence(); - - static QString parseEvent(QKeyEvent*); - static bool isValidKey(int); - -signals: - void editingStarted(); - void editingFinished(); - void restoreFromShortcutMgrClicked(); - -private slots: - void onClear(); - void onEditingFinished(); - -protected: - virtual bool eventFilter(QObject*, QEvent*); - -private: - void initialize(); - -private: - QLineEdit* myKeySequenceLineEdit; - QString myConfirmedKeySequenceString; - - // Last valid key sequence string from myKeySequenceLineEdit. - QString myPrevKeySequenceString; -}; - - -class SUIT_ShortcutTree; -class SUIT_ShortcutTreeItem; -class SUIT_ShortcutTreeFolder; -class SUIT_ShortcutTreeAction; -class QTextEdit; - - -class SUIT_EXPORT SUIT_EditKeySequenceDialog : public QDialog -{ - Q_OBJECT - -public: - SUIT_EditKeySequenceDialog(SUIT_ShortcutTree* theParent); - SUIT_EditKeySequenceDialog(const SUIT_EditKeySequenceDialog&) = delete; - SUIT_EditKeySequenceDialog& operator=(const SUIT_EditKeySequenceDialog&) = delete; - virtual ~SUIT_EditKeySequenceDialog() = default; - - void setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID); - const QString& moduleID() const; - const QString& inModuleActionID() const; - - void setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip = ""); - - void setConfirmedKeySequence(const QKeySequence& theSequence); - QKeySequence editedKeySequence() const; - - void updateConflictsMessage(); - - int exec(); - -private slots: - void onEditingStarted(); - void onEditingFinished(); - void onRestoreFromShortcutMgr(); - void onConfirm(); - -private: - QString myModuleID; - QString myInModuleActionID; - QLabel* myActionName; - SUIT_KeySequenceEdit* myKeySequenceEdit; - QTextEdit* myTextEdit; -}; - - -class SUIT_EXPORT SUIT_ShortcutTree : public QTreeWidget -{ - Q_OBJECT - -public: - enum ElementIdx { - Name = 0, - KeySequence = 1, // Empty, if item is folder item. - }; - - enum class SortKey { - ID, - Name, - ToolTip, - KeySequence, - }; - - enum class SortOrder { - Ascending, - Descending - }; - - SUIT_ShortcutTree( - std::shared_ptr theContainer = std::shared_ptr(), - QWidget* theParent = nullptr - ); - SUIT_ShortcutTree(const SUIT_ShortcutTree&) = delete; - SUIT_ShortcutTree& operator=(const SUIT_ShortcutTree&) = delete; - virtual ~SUIT_ShortcutTree(); - - void setShortcutsFromManager(); - void setDefaultShortcuts(); - void applyChangesToShortcutMgr(); - - std::shared_ptr shortcutContainer() const; - - void sort(SUIT_ShortcutTree::SortKey theKey, SUIT_ShortcutTree::SortOrder theOrder); - -private: - void updateItems(bool theHighlightModified, bool theUpdateSyncTrees); - std::pair findModuleFolderItem(const QString& theModuleID) const; - - std::set> getSortedChildren(SUIT_ShortcutTreeFolder* theParentItem); - - void insertChild( - SUIT_ShortcutTreeFolder* theParentItem, - std::set>& theSortedChildren, - SUIT_ShortcutTreeItem* theChildItem - ); - -private slots: - void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx); - -public: - /** Keeps IDs of modules, which will are shown on setShortcutsFromManager(). */ - std::set myModuleIDs; - - static const QList> DEFAULT_SORT_SCHEMA; - -private: - /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. */ - const std::shared_ptr myShortcutContainer; - - SUIT_EditKeySequenceDialog* myEditDialog; - - SUIT_ShortcutTree::SortKey mySortKey; - SUIT_ShortcutTree::SortOrder mySortOrder; - - /** - * Ensures that, if several SUIT_ShortcutTree instances coexist, - * all of them are updated when one of them applies pending changes to SUIT_ShortcutMgr. - * - * Sharing of SUIT_ShortcutContainer allows to keep some trees synchronized even without - * applying changes to SUIT_ShortcutMgr. Why? See SUIT_PagePrefShortcutTreeItem. - * - * Access is not synchronized in assumption, that all instances live in the same thread. - */ - static std::map> instances; -}; - - -class SUIT_ShortcutTreeItem : public QTreeWidgetItem -{ -public: - enum Type { - Folder = 0, - Action = 1, - }; - -protected: - SUIT_ShortcutTreeItem(const QString& theModuleID); - -public: - virtual ~SUIT_ShortcutTreeItem() = default; - virtual SUIT_ShortcutTreeItem::Type type() const = 0; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang) = 0; - QString name() const; - - virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const = 0; - -public: - const QString myModuleID; -}; - - -class SUIT_ShortcutTreeFolder : public SUIT_ShortcutTreeItem -{ -public: - SUIT_ShortcutTreeFolder(const QString& theModuleID); - virtual ~SUIT_ShortcutTreeFolder() = default; - virtual SUIT_ShortcutTreeItem::Type type() const { return SUIT_ShortcutTreeItem::Type::Folder; }; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); - - virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const; -}; - - -class SUIT_ShortcutTreeAction : public SUIT_ShortcutTreeItem -{ -private: - SUIT_ShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID); - -public: - static SUIT_ShortcutTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID); - virtual ~SUIT_ShortcutTreeAction() = default; - virtual SUIT_ShortcutTreeItem::Type type() const { return SUIT_ShortcutTreeItem::Type::Action; }; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); - - virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const; - - void setKeySequence(const QString& theKeySequence); - QString keySequence() const; - void highlightKeySequenceAsModified(bool theHighlight); - - const QString myInModuleActionID; -}; - -#endif // SUIT_SHORTCUTTREE_H diff --git a/src/SUIT/Tools/SUIT_SentenceMatcher.cxx b/src/SUIT/Tools/SUIT_SentenceMatcher.cxx new file mode 100644 index 000000000..2d7878016 --- /dev/null +++ b/src/SUIT/Tools/SUIT_SentenceMatcher.cxx @@ -0,0 +1,271 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "SUIT_SentenceMatcher.h" +#include + + +namespace SUIT_tools { + +SUIT_SentenceMatcher::SUIT_SentenceMatcher() +{ + myUseExactWordOrder = false; + myUseFuzzyWords = true; + myIsCaseSensitive = false; +} + +void SUIT_SentenceMatcher::setUseExactWordOrder(bool theOn) +{ + if (myUseExactWordOrder == theOn) + return; + + myUseExactWordOrder = theOn; + if (theOn) { + myPermutatedSentences.clear(); + myFuzzyPermutatedSentences.clear(); + return; + } + + if (myPermutatedSentences.isEmpty()) + SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); + + if (myUseFuzzyWords && myFuzzyPermutatedSentences.isEmpty()) + SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); +} + +void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn) +{ + if (myUseFuzzyWords == theOn) + return; + + myUseFuzzyWords = theOn; + if (myWords.isEmpty() || !theOn) { + myFuzzyWords.clear(); + myFuzzyPermutatedSentences.clear(); + return; + } + + myFuzzyWords.clear(); + SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); + + if (!myUseExactWordOrder) { + myFuzzyPermutatedSentences.clear(); + SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); + } +} + +void SUIT_SentenceMatcher::setCaseSensitive(bool theOn) +{ + myIsCaseSensitive = theOn; +} + +void SUIT_SentenceMatcher::setQuery(QString theQuery) +{ + theQuery = theQuery.simplified(); + if (theQuery == myQuery) + return; + + myQuery = theQuery; + myWords = theQuery.split(" ", QString::SkipEmptyParts); + + { // Set permutated sentences. + myPermutatedSentences.clear(); + if (!myUseExactWordOrder) + SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); + } + + // Set fuzzy words and sentences. + myFuzzyWords.clear(); + myFuzzyPermutatedSentences.clear(); + + if (myUseFuzzyWords) { + SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); + if (!myUseExactWordOrder) + SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); + } +} + +double SUIT_SentenceMatcher::match(const QString& theInputString) const +{ + int n = 0; + if (myUseExactWordOrder) { + n = SUIT_SentenceMatcher::match(theInputString, myWords, myIsCaseSensitive); + if (n != theInputString.length() && myUseFuzzyWords) { + const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyWords, myIsCaseSensitive); + if (nFuzzy > n) + n = nFuzzy; + } + } + else /* if match with permutated query sentences */ { + n = SUIT_SentenceMatcher::match(theInputString, myPermutatedSentences, myIsCaseSensitive); + if (n != theInputString.length() && myUseFuzzyWords) { + const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyPermutatedSentences, myIsCaseSensitive); + if (nFuzzy > n) + n = nFuzzy; + } + } + + if (n <= 0) + return std::numeric_limits::infinity(); + + const auto strLength = theInputString.length() > myQuery.length() ? theInputString.length() : myQuery.length(); + + if (n > strLength) + return 0; // Exact match or almost exact. + + return double(strLength - n); +} + +QString SUIT_SentenceMatcher::toString() const +{ + QString res = QString("myUseExactWordOrder: ") + (myUseExactWordOrder ? "true" : "false") + ";\n"; + res += QString("myUseFuzzyWords: ") + (myUseFuzzyWords ? "true" : "false") + ";\n"; + res += QString("myIsCaseSensitive: ") + (myIsCaseSensitive ? "true" : "false") + ";\n"; + res += QString("myQuery: ") + myQuery + ";\n"; + res += QString("myWords: ") + myWords.join(", ") + ";\n"; + res += QString("myFuzzyWords: ") + myFuzzyWords.join(", ") + ";\n"; + + res += "myPermutatedSentences:\n"; + for (const auto& sentence : myPermutatedSentences) { + res += "\t" + sentence.join(", ") + ";\n"; + } + + res += "myFuzzyPermutatedSentences:\n"; + for (const auto& sentence : myFuzzyPermutatedSentences) { + res += "\t" + sentence.join(", ") + ";\n"; + } + + res += "."; + return res; +} + +/*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList& theSentences) +{ + theSentences.clear(); + theSentences.push_back(theWords); + QStringList nextPerm = theWords; + QStringList prevPerm = theWords; + + bool hasNextPerm = true; + bool hasPrevPerm = true; + + while (hasNextPerm || hasPrevPerm) { + if (hasNextPerm) + hasNextPerm = std::next_permutation(nextPerm.begin(), nextPerm.end()); + + if (hasNextPerm && !theSentences.contains(nextPerm)) + theSentences.push_back(nextPerm); + + if (hasPrevPerm) + hasPrevPerm = std::prev_permutation(prevPerm.begin(), prevPerm.end()); + + if (hasPrevPerm && !theSentences.contains(prevPerm)) + theSentences.push_back(prevPerm); + } + + return theSentences.size() > 1; +} + +/*static*/ void SUIT_SentenceMatcher::makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords) +{ + theFuzzyWords.clear(); + for (const QString& word : theWords) { + QString fuzzyWord; + for (int i = 0; i < word.size(); i++) { + fuzzyWord += word[i]; + fuzzyWord += "\\w*"; + } + theFuzzyWords.push_back(fuzzyWord); + } +} + +/*static*/ int SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive) +{ + const QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + regExp.indexIn(theInputString); + const int matchMetrics = regExp.matchedLength(); + return matchMetrics > 0 ? matchMetrics : 0; +} + +/*static*/ int SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive) +{ + int res = 0; + for (const QStringList& sentence : theSentences) { + const int matchMetrics = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive); + if (matchMetrics > res) { + res = matchMetrics; + if (res == theInputString.length()) + return res; + } + } + return res; +} + +/*static*/ int SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive) +{ + int res = 0; + for (const QString& word : theWords) { + const auto regExp = QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + regExp.indexIn(theInputString); + const int matchMetrics = regExp.matchedLength(); + // The same input word can be counted multiple times. Nobody cares. + if (matchMetrics > 0) + res += matchMetrics; + } + return res; +} + +/*static*/ int SUIT_SentenceMatcher::match( + const QString& theInputString, + const QStringList& theSentence, + bool theCaseSensitive +) { + int res = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive); + if (res == theInputString.length()) + return res; + + const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive); + if (matchMetrics > res) + res = matchMetrics; + + return res; +} + +/*static*/ int SUIT_SentenceMatcher::match( + const QString& theInputString, + const QList& theSentences, + bool theCaseSensitive +) { + int res = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive); + if (res == theInputString.length()) + return res; + + if (theSentences.size() > 0) { + const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive); + if (matchMetrics > res) + res = matchMetrics; + } + + return res; +} + +} //namespace SUIT_tools \ No newline at end of file diff --git a/src/SUIT/Tools/SUIT_SentenceMatcher.h b/src/SUIT/Tools/SUIT_SentenceMatcher.h new file mode 100644 index 000000000..15d6ad03a --- /dev/null +++ b/src/SUIT/Tools/SUIT_SentenceMatcher.h @@ -0,0 +1,103 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef SUIT_SENTENCEMATCHER_H +#define SUIT_SENTENCEMATCHER_H + +#include "../SUIT.h" + +#include +#include + + +namespace SUIT_tools { + /*! \class SUIT_SentenceMatcher + \brief Approximate string matcher, treats strings as sentences composed of words. */ + class SUIT_EXPORT SUIT_SentenceMatcher + { + public: + /*! Default config: + Exact word order = false; + Fuzzy words = true; + Case sensitive = false; + Query = ""; // matches nothing. + */ + SUIT_SentenceMatcher(); + + void setUseExactWordOrder(bool theOn); + void setUseFuzzyWords(bool theOn); + void setCaseSensitive(bool theOn); + inline bool isCaseSensitive() const { return myIsCaseSensitive; }; + + /*! \param theQuery should not be regex. */ + void setQuery(QString theQuery); + + inline const QString& getQuery() const { return myQuery; }; + + /*! \returns match metrics. The metrics >= 0. INF means mismatch. + The class is unable to differentiate exact match with some approximate matches! */ + double match(const QString& theInputString) const; + + /** \brief For debug. */ + QString toString() const; + + private: + static bool makePermutatedSentences(const QStringList& theWords, QList& theSentences); + static void makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive); + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int match( + const QString& theInputString, + const QStringList& theSentence, + bool theCaseSensitive + ); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int match( + const QString& theInputString, + const QList& theSentences, + bool theCaseSensitive + ); + + bool myUseExactWordOrder; // If false, try to match with sentences, composed of query's words in different orders. + bool myUseFuzzyWords; // Try to match with sentences, composed of query's truncated words. + bool myIsCaseSensitive; + QString myQuery; + + QStringList myWords; // It is also original search sentence. + QList myPermutatedSentences; + + QStringList myFuzzyWords; // Regexes. + QList myFuzzyPermutatedSentences; + }; + +} //namespace SUIT_tools + +#endif //SUIT_SENTENCEMATCHER_H \ No newline at end of file diff --git a/src/SUIT/Tools/SUIT_extensions.cxx b/src/SUIT/Tools/SUIT_extensions.cxx new file mode 100644 index 000000000..5d089b9bb --- /dev/null +++ b/src/SUIT/Tools/SUIT_extensions.cxx @@ -0,0 +1,75 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "SUIT_extensions.h" +#include +#include +#include +#include +#include + +namespace SUIT_tools { + +QString substituteBashVars(const QString& theString) +{ + QString res = theString; + const auto env = QProcessEnvironment::systemEnvironment(); + int pos = 0; + QRegExp rx("\\$\\{([^\\}]+)\\}"); // Match substrings enclosed with "${" and "}". + rx.setMinimal(true); // Set search to non-greedy. + while((pos = rx.indexIn(res, pos)) != -1) { + QString capture = rx.cap(1); + QString subst = env.value(capture); + res.replace("${" + capture + "}", subst); + pos += rx.matchedLength(); + } + return res; +} + +QString substituteDOSVars(const QString& theString) +{ + QString res = theString; + int pos = 0; + QRegExp rx("%([^%]+)%"); // Match substrings enclosed with "%". + rx.setMinimal(true); // Set search to non-greedy. + while((pos = rx.indexIn(res, pos)) != -1) { + QString capture = rx.cap(1); + QString subst = Qtx::getenv(capture.toUtf8().constData()); + res.replace("%" + capture + "%", subst); + pos += rx.matchedLength(); + } + return res; +} + +QString substituteVars(const QString& theString) +{ + QString str = substituteBashVars(theString); + return substituteDOSVars(str); +} + +std::wstring to_wstring(const std::string& theStdString) +{ + std::wstring wideString = std::wstring_convert>().from_bytes(theStdString); + return wideString; +} + +} //namespace SUIT_tools diff --git a/src/SUIT/Tools/SUIT_extensions.h b/src/SUIT/Tools/SUIT_extensions.h new file mode 100644 index 000000000..c107db2a1 --- /dev/null +++ b/src/SUIT/Tools/SUIT_extensions.h @@ -0,0 +1,87 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef SUIT_TOOLS_EXTENSIONS_H +#define SUIT_TOOLS_EXTENSIONS_H + +#include "../SUIT.h" +#include +#include +#include +#include + + +namespace SUIT_tools { + + /*! \brief Compensates lack of std::distance(..) prior to C++17. + Does not check whether the iterators iterate the same container instance.*/ + template + SUIT_EXPORT std::size_t distance( + const Iterator& theStartIt, + const Iterator& theFinalIt + ) { + auto it = theStartIt; + std::size_t distance = 0; + while (distance <= std::numeric_limits::difference_type>::max()) { + if (it == theFinalIt) + return distance; + + it++; + distance++; + } + return distance; + } + + /*! \returns Distance from the theContainer.begin() to theIt. + \returns Inf, if theIt is not iterator of theContainer. */ + template + SUIT_EXPORT std::size_t distanceFromBegin( + const Container& theContainer, + const typename Container::const_iterator& theIt + ) { + auto it = theContainer.begin(); + std::size_t distance = 0; + while (it != theContainer.end()) { + if (it == theIt) + return distance; + + it++; + distance++; + } + return std::numeric_limits::infinity(); + } + + /*! \brief Replaces all Bash-style variables (enlosed with "${" and "}") with values of corresponding environment variables. */ + SUIT_EXPORT QString substituteBashVars(const QString& theString); + + /*! \brief Replaces all DOS-style variables (enlosed with "%") with values of corresponding environment variables. */ + SUIT_EXPORT QString substituteDOSVars(const QString& theString); + + /*! \brief Replaces all Bash- and DOS- style variables with values of corresponding environment variables. */ + SUIT_EXPORT QString substituteVars(const QString& theString); + + SUIT_EXPORT std::wstring to_wstring(const std::string& theStdString); + +} //namespace SUIT_tools + +#endif //SUIT_TOOLS_EXTENSIONS_H + diff --git a/src/SUIT/resources/SUIT_msg_fr.ts b/src/SUIT/resources/SUIT_msg_fr.ts index 9b8a31906..5698213e9 100644 --- a/src/SUIT/resources/SUIT_msg_fr.ts +++ b/src/SUIT/resources/SUIT_msg_fr.ts @@ -166,6 +166,13 @@ Voulez-vous l'écraser ? Find action Trouver une action + + Double click to start. + Double-cliquez pour démarrer. + + + + SUIT_FindActionWidget Unavailable actions Actions indisponibles @@ -182,6 +189,10 @@ Voulez-vous l'écraser ? Description Description + + Key sequence + Séquence de touches + SUIT_ViewWindow @@ -255,6 +266,17 @@ Voulez-vous l'écraser ? Ces raccourcis seront désactivés lors de la confirmation : + + SUIT_ShortcutTabWidget + + Double click to edit key sequence. + Double-cliquez pour modifier la séquence de touches. + + + Find shortcut + Trouver un raccourci + + SUIT_ShortcutTree @@ -265,6 +287,10 @@ Voulez-vous l'écraser ? Key sequence Séquence de touches + + Description + Description + Double click to edit key sequence. Double-cliquez pour modifier la séquence de touches. diff --git a/src/SUIT/resources/SUIT_msg_ja.ts b/src/SUIT/resources/SUIT_msg_ja.ts index ad3f28ddb..df619aa52 100644 --- a/src/SUIT/resources/SUIT_msg_ja.ts +++ b/src/SUIT/resources/SUIT_msg_ja.ts @@ -156,10 +156,17 @@ SUIT_FindActionDialog - + Find action 検索アクション - + + + Double click to start. + ダブルクリックして開始します。 + + + + SUIT_FindActionWidget Unavailable actions 利用できないアクション @@ -176,6 +183,10 @@ Description 説明 + + Key sequence + キーシーケンス + SUIT_ViewWindow @@ -249,6 +260,17 @@ これらのショートカットは確認時に無効になります。 + + SUIT_ShortcutTabWidget + + Double click to edit key sequence. + キーシーケンスを編集するにはダブルクリックします。 + + + Find shortcut + ショートカットを探す + + SUIT_ShortcutTree @@ -259,6 +281,10 @@ Key sequence キーシーケンス + + Description + 説明 + Double click to edit key sequence. ダブルクリックしてキー シーケンスを編集します。 diff --git a/src/SUIT/resources/action_assets.json b/src/SUIT/resources/action_assets.json index 14bbfeff2..16ccf28a8 100644 --- a/src/SUIT/resources/action_assets.json +++ b/src/SUIT/resources/action_assets.json @@ -1,733 +1,881 @@ { - "/#TOT_DESK_EDIT_COPY": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/copy.png", - "langDependentAssets": { - "en": { - "name": "Copy", - "tooltip": "Copy the selection to the Clipboard" - }, - "fr": { - "name": "Copier", - "tooltip": "Copier la sélection dans le presse-papiers" - }, - "ja": { - "name": "コピー", - "tooltip": "選択範囲をクリップボードにコピー" - } - } - }, - "/#Viewers/View/Reset": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/reset.png", - "langDependentAssets": { - "en": { - "name": "Reset", - "tooltip": "Reset View Point" - }, - "fr": { - "name": "Restaurer", - "tooltip": "Restaurer le point de vue" - }, - "ja": { - "name": "復元", - "tooltip": "ビューのポイントを復元します。" - } - } - }, - "/#Viewers/View/Rotate anticlockwise": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Rotate counterclockwise", - "tooltip": "Rotate view counterclockwise" - }, - "fr": { - "name": "Tourner la vue à gauche", - "tooltip": "Tourner la vue à gauche" - }, - "ja": { - "name": "表示を左に", - "tooltip": "表示を左に" - } - } - }, - "/#Viewers/View/Rotate clockwise": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Rotate clockwise", - "tooltip": "Rotate View Clockwise" - }, - "fr": { - "name": "Tourner la vue à droite", - "tooltip": "Tourner la vue à droite" - }, - "ja": { - "name": "右のビューを回転させる", - "tooltip": "右のビューを回転させる" - } - } - }, - "/#Viewers/View/Set X+": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "+OX", - "tooltip": "+OX View" - }, - "fr": { - "name": "+OX", - "tooltip": "Vue +OX" - }, - "ja": { - "name": "+OX", - "tooltip": "+OX View" - } - } - }, - "/#Viewers/View/Set X-": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "-OX", - "tooltip": "-OX View" - }, - "fr": { - "name": "-OX", - "tooltip": "Vue -OX" - }, - "ja": { - "name": "-OX", - "tooltip": "-OX View" - } - } - }, - "/#Viewers/View/Set Y+": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "+OY", - "tooltip": "+OY View" - }, - "fr": { - "name": "+OY", - "tooltip": "Vue +OY" - }, - "ja": { - "name": "+OY", - "tooltip": "+OY View" - } - } - }, - "/#Viewers/View/Set Y-": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "-OY", - "tooltip": "-OY View" - }, - "fr": { - "name": "-OY", - "tooltip": "Vue -OY" - }, - "ja": { - "name": "-OY", - "tooltip": "-OY View" - } - } - }, - "/#Viewers/View/Set Z+": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "+OZ", - "tooltip": "+OZ View" - }, - "fr": { - "name": "+OZ", - "tooltip": "Vue +OZ" - }, - "ja": { - "name": "+OZ", - "tooltip": "+OZ View" - } - } - }, - "/#Viewers/View/Set Z-": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "-OZ", - "tooltip": "-OZ View" - }, - "fr": { - "name": "-OZ", - "tooltip": "Vue -OZ" - }, - "ja": { - "name": "-OZ", - "tooltip": "-OZ View" - } - } - }, - "/#General/Object(s)/Hide": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Hide", - "tooltip": "Hide" - }, - "fr": { - "name": "Cacher", - "tooltip": "Cacher" - }, - "ja": { - "name": "非表示", - "tooltip": "非表示" - } - } - }, - "/#General/Object(s)/Show": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Show", - "tooltip": "Show" - }, - "fr": { - "name": "Afficher", - "tooltip": "Afficher" - }, - "ja": { - "name": "表示", - "tooltip": "表示" - } - } - }, - "/PRP_CLOSE": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Close", - "tooltip": "Close active window" - }, - "fr": { - "name": "Fermer", - "tooltip": "Fermer la fenêtre active" - }, - "ja": { - "name": "閉じる", - "tooltip": "アクティブ ウィンドウを閉じる" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_0": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "GL 2D view", - "tooltip": "Create new GL 2D view" - }, - "fr": { - "name": "Scène GL ", - "tooltip": "Créer une nouvelle Scène GL " - }, - "ja": { - "name": "GL 2D view", - "tooltip": "新しい GL 2D view を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_1": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Plot 2D view", - "tooltip": "Create new Plot 2D view" - }, - "fr": { - "name": "Scène Plot2d ", - "tooltip": "Créer une nouvelle Scène Plot2d " - }, - "ja": { - "name": "Plot 2D View", - "tooltip": "新しい Plot 2D View を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_2": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "OCC 3D view", - "tooltip": "Create new OCC 3D view" - }, - "fr": { - "name": "Scène OCC", - "tooltip": "Créer une nouvelle Scène OCC" - }, - "ja": { - "name": "OCC 3D View", - "tooltip": "新しい OCC 3D View を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_3": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "VTK 3D view", - "tooltip": "Create new VTK 3D view" - }, - "fr": { - "name": "Scène VTK", - "tooltip": "Créer une nouvelle Scène VTK" - }, - "ja": { - "name": "VTK 3D View", - "tooltip": "新しい VTK 3D View を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_4": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "QxScene 2D view", - "tooltip": "Create new QxScene 2D view" - }, - "fr": { - "name": "Scène QxScene", - "tooltip": "Créer une nouvelle Scène QxScene" - }, - "ja": { - "name": "シーン QxScene", - "tooltip": "新しい シーン QxScene を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_5": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Graphics view", - "tooltip": "Create new Graphics view" - }, - "fr": { - "name": "Scène Graphiques", - "tooltip": "Créer une nouvelle Scène Graphiques" - }, - "ja": { - "name": "グラフィックの表示", - "tooltip": "新しい グラフィックの表示 を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_6": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "ParaView view", - "tooltip": "Create new ParaView view" - }, - "fr": { - "name": "Scène ParaView", - "tooltip": "Créer une nouvelle Scène ParaView" - }, - "ja": { - "name": "ParaView 表示 ", - "tooltip": "新しい ParaView 表示 を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_7": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Python view", - "tooltip": "Create new Python view" - }, - "fr": { - "name": "Vue Python", - "tooltip": "Créer une nouvelle Vue Python" - }, - "ja": { - "name": "Python view", - "tooltip": "新しい Python view を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_8": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "ParaView3D view", - "tooltip": "Create new ParaView3D view" - }, - "fr": { - "name": "ParaView3D view", - "tooltip": "Créer une nouvelle ParaView3D view" - }, - "ja": { - "name": "ParaView3D view", - "tooltip": "新しい ParaView3D view を作成します。" - } - } - }, - "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_9": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "QtView view", - "tooltip": "Create new QtView view" - }, - "fr": { - "name": "QtView view", - "tooltip": "Créer une nouvelle QtView view" - }, - "ja": { - "name": "QtView view", - "tooltip": "新しい QtView view を作成します。" - } - } - }, - "/PRP_DESK_CATALOG_GENERATOR": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Catalog Generator", - "tooltip": "Generates XML catalog of a component's interface" - }, - "fr": { - "name": "Genérateur de catalogue", - "tooltip": "Génére un catalogue XML de l'interface du composant" - }, - "ja": { - "name": "カタログ ジェネレーター", - "tooltip": "コンポーネントインターフェイスのXMLカタログを生成" - } - } - }, - "/PRP_DESK_FIND_ACTION": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Find action", - "tooltip": "Opens action search dialog" - }, - "fr": { - "name": "Trouver une action", - "tooltip": "Ouvre la boîte de dialogue de recherche d'action" - }, - "ja": { - "name": "検索アクション", - "tooltip": "アクション検索ダイアログを開きます" - } - } - }, - "/PRP_DESK_CONNECT": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Connect", - "tooltip": "Connect active study" - }, - "fr": { - "name": "Connecter", - "tooltip": "Connecter l'étude en cours" - }, - "ja": { - "name": "接続", - "tooltip": "アクティブスタディの接続" - } - } - }, - "/PRP_DESK_DISCONNECT": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Disconnect", - "tooltip": "Disconnect the current study" - }, - "fr": { - "name": "Déconnecter", - "tooltip": "Déconnecter l'étude en cours" - }, - "ja": { - "name": "切断", - "tooltip": "カレントスタディの切断" - } - } - }, - "/PRP_DESK_FILE_DUMP_STUDY": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Dump Study...", - "tooltip": "Dumps study to the python script" - }, - "fr": { - "name": "Générer le script de l'étude...", - "tooltip": "Génère le script python de l'étude" - }, - "ja": { - "name": "スクリプトを保存", - "tooltip": "Pythonスクリプトにスタディをダンプする" - } - } - }, - "/PRP_DESK_FILE_LOAD_SCRIPT": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Load Script...", - "tooltip": "Loads python script from file" - }, - "fr": { - "name": "Exécuter un script...", - "tooltip": "Exécute un script Python à partir d'un fichier" - }, - "ja": { - "name": "スクリプトを読込み...", - "tooltip": "ファイルからPythonスクリプトを読込み" - } - } - }, - "/PRP_DESK_HELP_ABOUT": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/about.png", - "langDependentAssets": { - "en": { - "name": "About...", - "tooltip": "Shows 'About' dialog" - }, - "fr": { - "name": "A propos de...", - "tooltip": "Montre la boîte de dialogue 'A propos'" - }, - "ja": { - "name": "バージョン情報...", - "tooltip": "ソフト情報の表示" - } - } - }, - "/PRP_DESK_PREFERENCES": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Preferences...", - "tooltip": "Allow to change the preferences" - }, - "fr": { - "name": "Préférences...", - "tooltip": "Permettre de changer les préférences" - }, - "ja": { - "name": "環境設定...", - "tooltip": "設定を変更することができます。" - } - } - }, - "/PRP_DESK_VIEW_STATUSBAR": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Status Bar", - "tooltip": "Toggles status bar view on/off" - }, - "fr": { - "name": "Barre de status", - "tooltip": "Activer ou désactiver la barre de status" - }, - "ja": { - "name": "ステータス バー", - "tooltip": "ステータスバーの有効/無効" - } - } - }, - "/PRP_DESK_WINDOW_HSPLIT": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/htile.png", - "langDependentAssets": { - "en": { - "name": "Split Horizontally", - "tooltip": "Splits the active window on two horizontal parts" - }, - "fr": { - "name": "Séparation horizontale", - "tooltip": "Diviser la fenêtre actuelle en deux parties horizontales" - }, - "ja": { - "name": "水平分割", - "tooltip": "現在のウィンドウを 2つに水平分割" - } - } - }, - "/PRP_DESK_WINDOW_VSPLIT": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/vtile.png", - "langDependentAssets": { - "en": { - "name": "Split Vertically", - "tooltip": "Splits the active window on two vertical parts" - }, - "fr": { - "name": "Séparation verticale", - "tooltip": "Diviser la fenêtre actuelle en deux parties verticales" - }, - "ja": { - "name": "垂直分割", - "tooltip": "現在のウィンドウを2つに上下分割" - } - } - }, - "/PRP_FULLSCREEN": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Full screen", - "tooltip": "Switch to full screen mode" - }, - "fr": { - "name": "Plein écran", - "tooltip": "Basculer en mode plein écran" - }, - "ja": { - "name": "全画面表示", - "tooltip": "全画面表示モードに切り替え" - } - } - }, - "/PRP_RENAME": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Rename", - "tooltip": "Rename active window" - }, - "fr": { - "name": "Renommer", - "tooltip": "Renommer la fenêtre active" - }, - "ja": { - "name": "名前変更", - "tooltip": "アクティブなウィンドウの名前を変更" - } - } - }, - "/TOT_DESK_EDIT_PASTE": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/paste.png", - "langDependentAssets": { - "en": { - "name": "Paste", - "tooltip": "Inserts the Clipboard content at the insertion point" - }, - "fr": { - "name": "Coller", - "tooltip": "Insérer le contenu du presse-papiers au point d'insertion" - }, - "ja": { - "name": "貼り付け", - "tooltip": "クリップボードの内容を挿入" - } - } - }, - "/TOT_DESK_FILE_CLOSE": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/close.png", - "langDependentAssets": { - "en": { - "name": "Close", - "tooltip": "Closes the active document" - }, - "fr": { - "name": "Fermer", - "tooltip": "Ferme le document actuel" - }, - "ja": { - "name": "閉じる", - "tooltip": "現在のドキュメントを閉じる" - } - } - }, - "/TOT_DESK_FILE_EXIT": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Exit", - "tooltip": "Exits the application" - }, - "fr": { - "name": "Quitter", - "tooltip": "Quitte l'application" - }, - "ja": { - "name": "終了", - "tooltip": "アプリケーションを終了" - } - } - }, - "/TOT_DESK_FILE_NEW": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/new.png", - "langDependentAssets": { - "en": { - "name": "New", - "tooltip": "Create a new document" - }, - "fr": { - "name": "Nouveau", - "tooltip": "Créer une nouvelle étude" - }, - "ja": { - "name": "新規作成", - "tooltip": "新しいドキュメントを作成" - } - } - }, - "/TOT_DESK_FILE_OPEN": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/open.png", - "langDependentAssets": { - "en": { - "name": "Open...", - "tooltip": "Open an existing document" - }, - "fr": { - "name": "Ouvrir...", - "tooltip": "Ouvre une étude existant" - }, - "ja": { - "name": "開く...", - "tooltip": "既存のドキュメントを開く" - } - } - }, - "/TOT_DESK_FILE_SAVE": { - "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/save.png", - "langDependentAssets": { - "en": { - "name": "Save", - "tooltip": "Save the active document" - }, - "fr": { - "name": "Enregistrer", - "tooltip": "Sauvegarder l'étude actuelle" - }, - "ja": { - "name": "保存", - "tooltip": "現在のドキュメントを保存" - } - } - }, - "/TOT_DESK_FILE_SAVEAS": { - "iconPath": "", - "langDependentAssets": { - "en": { - "name": "Save As...", - "tooltip": "Saves the active document with a new name" - }, - "fr": { - "name": "Enregistrer sous...", - "tooltip": "Sauvegarder le document actuel sous un nouveau nom" - }, - "ja": { - "name": "別名保存...", - "tooltip": "現在のドキュメントを新しい名前で保存" + "": { + "children": { + "File": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "File" + }, + "fr": { + "name": "Fichier" + }, + "ja": { + "name": "ファイル" + } + }, + "children": { + "New": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/new.png", + "langDependentAssets": { + "en": { + "name": "New", + "tooltip": "Create a new document" + }, + "fr": { + "name": "Nouveau", + "tooltip": "Créer une nouvelle étude" + }, + "ja": { + "name": "新規作成", + "tooltip": "新しいドキュメントを作成" + } + } + }, + "Open": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/open.png", + "langDependentAssets": { + "en": { + "name": "Open...", + "tooltip": "Open an existing document" + }, + "fr": { + "name": "Ouvrir...", + "tooltip": "Ouvre une étude existant" + }, + "ja": { + "name": "開く...", + "tooltip": "既存のドキュメントを開く" + } + } + }, + "Close": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/close.png", + "langDependentAssets": { + "en": { + "name": "Close", + "tooltip": "Closes the active document" + }, + "fr": { + "name": "Fermer", + "tooltip": "Ferme le document actuel" + }, + "ja": { + "name": "閉じる", + "tooltip": "現在のドキュメントを閉じる" + } + } + }, + "Save": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/save.png", + "langDependentAssets": { + "en": { + "name": "Save", + "tooltip": "Save the active document" + }, + "fr": { + "name": "Enregistrer", + "tooltip": "Sauvegarder l'étude actuelle" + }, + "ja": { + "name": "保存", + "tooltip": "現在のドキュメントを保存" + } + } + }, + "SaveAs": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/save.png", + "langDependentAssets": { + "en": { + "name": "Save As...", + "tooltip": "Saves the active document with a new name" + }, + "fr": { + "name": "Enregistrer sous...", + "tooltip": "Sauvegarder le document actuel sous un nouveau nom" + }, + "ja": { + "name": "別名保存...", + "tooltip": "現在のドキュメントを新しい名前で保存" + } + } + }, + "Study_ScriptPy_DumpTo": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Dump Study...", + "tooltip": "Dumps study to the python script" + }, + "fr": { + "name": "Générer le script de l'étude...", + "tooltip": "Génère le script python de l'étude" + }, + "ja": { + "name": "スクリプトを保存", + "tooltip": "Pythonスクリプトにスタディをダンプする" + } + } + }, + "Study_ScriptPy_LoadFrom": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Load Script...", + "tooltip": "Loads python script from file" + }, + "fr": { + "name": "Exécuter un script...", + "tooltip": "Exécute un script Python à partir d'un fichier" + }, + "ja": { + "name": "スクリプトを読込み...", + "tooltip": "ファイルからPythonスクリプトを読込み" + } + } + }, + "Study_Connection_Connect": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Connect", + "tooltip": "Connect active study" + }, + "fr": { + "name": "Connecter", + "tooltip": "Connecter l'étude en cours" + }, + "ja": { + "name": "接続", + "tooltip": "アクティブスタディの接続" + } + } + }, + "Study_Connection_Disconnect": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Disconnect", + "tooltip": "Disconnect the current study" + }, + "fr": { + "name": "Déconnecter", + "tooltip": "Déconnecter l'étude en cours" + }, + "ja": { + "name": "切断", + "tooltip": "カレントスタディの切断" + } + } + }, + "Preferences": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Preferences...", + "tooltip": "Allow to change the preferences" + }, + "fr": { + "name": "Préférences...", + "tooltip": "Permettre de changer les préférences" + }, + "ja": { + "name": "環境設定...", + "tooltip": "設定を変更することができます。" + } + } + }, + "Exit": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Exit", + "tooltip": "Exits the application" + }, + "fr": { + "name": "Quitter", + "tooltip": "Quitte l'application" + }, + "ja": { + "name": "終了", + "tooltip": "アプリケーションを終了" + } + } + } + } + }, + + "Edit": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "Edit" + }, + "fr": { + "name": "Edition" + }, + "ja": { + "name": "編集" + } + }, + "children": { + "#Clipboard_Copy": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/copy.png", + "langDependentAssets": { + "en": { + "name": "Copy", + "tooltip": "Copy the selection to the Clipboard" + }, + "fr": { + "name": "Copier", + "tooltip": "Copier la sélection dans le presse-papiers" + }, + "ja": { + "name": "コピー", + "tooltip": "選択範囲をクリップボードにコピー" + } + } + }, + "#Clipboard_Paste": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/paste.png", + "langDependentAssets": { + "en": { + "name": "Paste", + "tooltip": "Inserts the Clipboard content at the insertion point" + }, + "fr": { + "name": "Coller", + "tooltip": "Insérer le contenu du presse-papiers au point d'insertion" + }, + "ja": { + "name": "貼り付け", + "tooltip": "クリップボードの内容を挿入" + } + } + }, + "#Object_Hide": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Hide", + "tooltip": "Hide" + }, + "fr": { + "name": "Cacher", + "tooltip": "Cacher" + }, + "ja": { + "name": "非表示", + "tooltip": "非表示" + } + } + }, + "#Object_Show": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Show", + "tooltip": "Show" + }, + "fr": { + "name": "Afficher", + "tooltip": "Afficher" + }, + "ja": { + "name": "表示", + "tooltip": "表示" + } + } + }, + "#Undo": { + "iconPath": "%SHAPER_ROOT_DIR%/share/salome/resources/shaper/icons/XGUI/undo.png", + "langDependentAssets": { + "en": { + "name": "Undo", + "tooltip": "Undo last command" + }, + "fr": { + "name": "Annuler", + "tooltip": "Annuler la dernière commande" + } + } + }, + "#Redo": { + "iconPath": "%SHAPER_ROOT_DIR%/share/salome/resources/shaper/icons/XGUI/redo.png", + "langDependentAssets": { + "en": { + "name": "Redo", + "tooltip": "Redo last command" + }, + "fr": { + "name": "Refaire", + "tooltip": "Refaire la dernière commande" + } + } + } + } + }, + + "View": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "View" + }, + "fr": { + "name": "Affichage" + }, + "ja": { + "name": "ビュー" + } + }, + "children": { + "ViewPoint": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "View Point" + }, + "fr": { + "name": "Point de Vue" + }, + "ja": { + "name": "ビューポイント" + } + }, + "children": { + "#Reset": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/reset.png", + "langDependentAssets": { + "en": { + "name": "Reset", + "tooltip": "Reset View Point" + }, + "fr": { + "name": "Restaurer", + "tooltip": "Restaurer le point de vue" + }, + "ja": { + "name": "復元", + "tooltip": "ビューのポイントを復元します。" + } + } + }, + "#RotateAnticlockwise": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Rotate counterclockwise", + "tooltip": "Rotate view counterclockwise" + }, + "fr": { + "name": "Tourner la vue à gauche", + "tooltip": "Tourner la vue à gauche" + }, + "ja": { + "name": "表示を左に", + "tooltip": "表示を左に" + } + } + }, + "#RotateClockwise": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Rotate clockwise", + "tooltip": "Rotate View Clockwise" + }, + "fr": { + "name": "Tourner la vue à droite", + "tooltip": "Tourner la vue à droite" + }, + "ja": { + "name": "右のビューを回転させる", + "tooltip": "右のビューを回転させる" + } + } + }, + "#SetDirOX+": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "+OX", + "tooltip": "+OX View" + }, + "fr": { + "name": "+OX", + "tooltip": "Vue +OX" + }, + "ja": { + "name": "+OX", + "tooltip": "+OX View" + } + } + }, + "#SetDirOX-": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "-OX", + "tooltip": "-OX View" + }, + "fr": { + "name": "-OX", + "tooltip": "Vue -OX" + }, + "ja": { + "name": "-OX", + "tooltip": "-OX View" + } + } + }, + "#SetDirOY+": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "+OY", + "tooltip": "+OY View" + }, + "fr": { + "name": "+OY", + "tooltip": "Vue +OY" + }, + "ja": { + "name": "+OY", + "tooltip": "+OY View" + } + } + }, + "#SetDirOY-": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "-OY", + "tooltip": "-OY View" + }, + "fr": { + "name": "-OY", + "tooltip": "Vue -OY" + }, + "ja": { + "name": "-OY", + "tooltip": "-OY View" + } + } + }, + "#SetDirOZ+": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "+OZ", + "tooltip": "+OZ View" + }, + "fr": { + "name": "+OZ", + "tooltip": "Vue +OZ" + }, + "ja": { + "name": "+OZ", + "tooltip": "+OZ View" + } + } + }, + "#SetDirOZ-": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "-OZ", + "tooltip": "-OZ View" + }, + "fr": { + "name": "-OZ", + "tooltip": "Vue -OZ" + }, + "ja": { + "name": "-OZ", + "tooltip": "-OZ View" + } + } + } + } + }, + "StatusBar_Toggle": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Status Bar", + "tooltip": "Toggles status bar view on/off" + }, + "fr": { + "name": "Barre de status", + "tooltip": "Activer ou désactiver la barre de status" + }, + "ja": { + "name": "ステータス バー", + "tooltip": "ステータスバーの有効/無効" + } + } + }, + "FullScreen": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Full screen", + "tooltip": "Switch to full screen mode" + }, + "fr": { + "name": "Plein écran", + "tooltip": "Basculer en mode plein écran" + }, + "ja": { + "name": "全画面表示", + "tooltip": "全画面表示モードに切り替え" + } + } + } + } + }, + + "Tools": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "Tools" + }, + "fr": { + "name": "Outils" + }, + "ja": { + "name": "ツール" + } + }, + "children": { + "CatalogGenerator": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Catalog Generator", + "tooltip": "Generates XML catalog of a component's interface" + }, + "fr": { + "name": "Genérateur de catalogue", + "tooltip": "Génére un catalogue XML de l'interface du composant" + }, + "ja": { + "name": "カタログ ジェネレーター", + "tooltip": "コンポーネントインターフェイスのXMLカタログを生成" + } + } + }, + "FindAction": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Find action", + "tooltip": "Opens action search dialog" + }, + "fr": { + "name": "Trouver une action", + "tooltip": "Ouvre la boîte de dialogue de recherche d'action" + }, + "ja": { + "name": "検索アクション", + "tooltip": "アクション検索ダイアログを開きます" + } + } + } + } + }, + + "Window": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "Window" + }, + "fr": { + "name": "Fenêtre" + }, + "ja": { + "name": "ウィンドウ" + } + }, + "children": { + "New": { + "isAction": false, + "langDependentAssets": { + "en": { + "name": "New Window" + }, + "fr": { + "name": "Nouvelle Fenêtre" + }, + "ja": { + "name": "新しいウィンドウ" + } + }, + "children": { + "ForViewer0": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "GL 2D view", + "tooltip": "Create new GL 2D view" + }, + "fr": { + "name": "Scène GL ", + "tooltip": "Créer une nouvelle Scène GL " + }, + "ja": { + "name": "GL 2D view", + "tooltip": "新しい GL 2D view を作成します。" + } + } + }, + "ForViewer1": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Plot 2D view", + "tooltip": "Create new Plot 2D view" + }, + "fr": { + "name": "Scène Plot2d ", + "tooltip": "Créer une nouvelle Scène Plot2d " + }, + "ja": { + "name": "Plot 2D View", + "tooltip": "新しい Plot 2D View を作成します。" + } + } + }, + "ForViewer2": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "OCC 3D view", + "tooltip": "Create new OCC 3D view" + }, + "fr": { + "name": "Scène OCC", + "tooltip": "Créer une nouvelle Scène OCC" + }, + "ja": { + "name": "OCC 3D View", + "tooltip": "新しい OCC 3D View を作成します。" + } + } + }, + "ForViewer3": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "VTK 3D view", + "tooltip": "Create new VTK 3D view" + }, + "fr": { + "name": "Scène VTK", + "tooltip": "Créer une nouvelle Scène VTK" + }, + "ja": { + "name": "VTK 3D View", + "tooltip": "新しい VTK 3D View を作成します。" + } + } + }, + "ForViewer4": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "QxScene 2D view", + "tooltip": "Create new QxScene 2D view" + }, + "fr": { + "name": "Scène QxScene", + "tooltip": "Créer une nouvelle Scène QxScene" + }, + "ja": { + "name": "シーン QxScene", + "tooltip": "新しい シーン QxScene を作成します。" + } + } + }, + "ForViewer5": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Graphics view", + "tooltip": "Create new Graphics view" + }, + "fr": { + "name": "Scène Graphiques", + "tooltip": "Créer une nouvelle Scène Graphiques" + }, + "ja": { + "name": "グラフィックの表示", + "tooltip": "新しい グラフィックの表示 を作成します。" + } + } + }, + "ForViewer6": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "ParaView view", + "tooltip": "Create new ParaView view" + }, + "fr": { + "name": "Scène ParaView", + "tooltip": "Créer une nouvelle Scène ParaView" + }, + "ja": { + "name": "ParaView 表示 ", + "tooltip": "新しい ParaView 表示 を作成します。" + } + } + }, + "ForViewer7": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Python view", + "tooltip": "Create new Python view" + }, + "fr": { + "name": "Vue Python", + "tooltip": "Créer une nouvelle Vue Python" + }, + "ja": { + "name": "Python view", + "tooltip": "新しい Python view を作成します。" + } + } + }, + "ForViewer8": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "ParaView3D view", + "tooltip": "Create new ParaView3D view" + }, + "fr": { + "name": "ParaView3D view", + "tooltip": "Créer une nouvelle ParaView3D view" + }, + "ja": { + "name": "ParaView3D view", + "tooltip": "新しい ParaView3D view を作成します。" + } + } + }, + "ForViewer9": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "QtView view", + "tooltip": "Create new QtView view" + }, + "fr": { + "name": "QtView view", + "tooltip": "Créer une nouvelle QtView view" + }, + "ja": { + "name": "QtView view", + "tooltip": "新しい QtView view を作成します。" + } + } + } + } + }, + + "Close": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Close", + "tooltip": "Close active window" + }, + "fr": { + "name": "Fermer", + "tooltip": "Fermer la fenêtre active" + }, + "ja": { + "name": "閉じる", + "tooltip": "アクティブ ウィンドウを閉じる" + } + } + }, + "SplitH": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/htile.png", + "langDependentAssets": { + "en": { + "name": "Split Horizontally", + "tooltip": "Splits the active window on two horizontal parts" + }, + "fr": { + "name": "Séparation horizontale", + "tooltip": "Diviser la fenêtre actuelle en deux parties horizontales" + }, + "ja": { + "name": "水平分割", + "tooltip": "現在のウィンドウを 2つに水平分割" + } + } + }, + "SplitV": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/vtile.png", + "langDependentAssets": { + "en": { + "name": "Split Vertically", + "tooltip": "Splits the active window on two vertical parts" + }, + "fr": { + "name": "Séparation verticale", + "tooltip": "Diviser la fenêtre actuelle en deux parties verticales" + }, + "ja": { + "name": "垂直分割", + "tooltip": "現在のウィンドウを2つに上下分割" + } + } + }, + "Rename": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Rename", + "tooltip": "Rename active window" + }, + "fr": { + "name": "Renommer", + "tooltip": "Renommer la fenêtre active" + }, + "ja": { + "name": "名前変更", + "tooltip": "アクティブなウィンドウの名前を変更" + } + } + } + } + }, + + "AboutDialog": { + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/about.png", + "langDependentAssets": { + "en": { + "name": "About...", + "tooltip": "Shows 'About' dialog" + }, + "fr": { + "name": "A propos de...", + "tooltip": "Montre la boîte de dialogue 'A propos'" + }, + "ja": { + "name": "バージョン情報...", + "tooltip": "ソフト情報の表示" + } + } } } } -} +} \ No newline at end of file diff --git a/src/SUIT/resources/default_action_icon.svg b/src/SUIT/resources/default_action_icon.svg new file mode 100755 index 000000000..c3b3e7183 --- /dev/null +++ b/src/SUIT/resources/default_action_icon.svg @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/src/SUIT/resources/default_folder_action_icon.svg b/src/SUIT/resources/default_folder_action_icon.svg new file mode 100755 index 000000000..f2e2da434 --- /dev/null +++ b/src/SUIT/resources/default_folder_action_icon.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + diff --git a/src/SUIT/resources/default_folder_icon.svg b/src/SUIT/resources/default_folder_icon.svg new file mode 100755 index 000000000..beea9874f --- /dev/null +++ b/src/SUIT/resources/default_folder_icon.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/src/SUIT/resources/find.svg b/src/SUIT/resources/find.svg new file mode 100644 index 000000000..67a05d405 --- /dev/null +++ b/src/SUIT/resources/find.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/SUIT/resources/shortcut_disable.svg b/src/SUIT/resources/shortcut_disable.svg new file mode 100644 index 000000000..cf661ff9f --- /dev/null +++ b/src/SUIT/resources/shortcut_disable.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/SUIT/resources/shortcut_restore.svg b/src/SUIT/resources/shortcut_restore.svg new file mode 100644 index 000000000..90dfc47d5 --- /dev/null +++ b/src/SUIT/resources/shortcut_restore.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/SUIT/resources/sort_ascending.svg b/src/SUIT/resources/sort_ascending.svg new file mode 100644 index 000000000..cb3916ee3 --- /dev/null +++ b/src/SUIT/resources/sort_ascending.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/src/SUIT/resources/sort_ascending_leading_key.svg b/src/SUIT/resources/sort_ascending_leading_key.svg new file mode 100644 index 000000000..4429329fa --- /dev/null +++ b/src/SUIT/resources/sort_ascending_leading_key.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/src/SUIT/resources/sort_descending.svg b/src/SUIT/resources/sort_descending.svg new file mode 100644 index 000000000..e698306bb --- /dev/null +++ b/src/SUIT/resources/sort_descending.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/src/SUIT/resources/sort_descending_leading_key.svg b/src/SUIT/resources/sort_descending_leading_key.svg new file mode 100644 index 000000000..bba9fb85e --- /dev/null +++ b/src/SUIT/resources/sort_descending_leading_key.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/src/SVTK/SVTK_ViewWindow.cxx b/src/SVTK/SVTK_ViewWindow.cxx index 74412fa3b..38f80aee7 100644 --- a/src/SVTK/SVTK_ViewWindow.cxx +++ b/src/SVTK/SVTK_ViewWindow.cxx @@ -2263,7 +2263,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) // Projections anAction = new QtxAction(tr("MNU_FRONT_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_FRONT" ) ), - tr( "MNU_FRONT_VIEW" ), 0, this, false, "/#Viewers/View/Set X-"); + tr( "MNU_FRONT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOX-"); anAction->setStatusTip(tr("DSC_FRONT_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onFrontView())); this->addAction(anAction); @@ -2271,7 +2271,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) anAction = new QtxAction(tr("MNU_BACK_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_BACK" ) ), - tr( "MNU_BACK_VIEW" ), 0, this, false, "/#Viewers/View/Set X+"); + tr( "MNU_BACK_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOX+"); anAction->setStatusTip(tr("DSC_BACK_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onBackView())); this->addAction(anAction); @@ -2279,7 +2279,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) anAction = new QtxAction(tr("MNU_TOP_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_TOP" ) ), - tr( "MNU_TOP_VIEW" ), 0, this, false, "/#Viewers/View/Set Z-"); + tr( "MNU_TOP_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOZ-"); anAction->setStatusTip(tr("DSC_TOP_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onTopView())); this->addAction(anAction); @@ -2287,7 +2287,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) anAction = new QtxAction(tr("MNU_BOTTOM_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_BOTTOM" ) ), - tr( "MNU_BOTTOM_VIEW" ), 0, this, false, "/#Viewers/View/Set Z+"); + tr( "MNU_BOTTOM_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOZ+"); anAction->setStatusTip(tr("DSC_BOTTOM_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onBottomView())); this->addAction(anAction); @@ -2295,7 +2295,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) anAction = new QtxAction(tr("MNU_LEFT_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_LEFT" ) ), - tr( "MNU_LEFT_VIEW" ), 0, this, false, "/#Viewers/View/Set Y+"); + tr( "MNU_LEFT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOY+"); anAction->setStatusTip(tr("DSC_LEFT_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onLeftView())); this->addAction(anAction); @@ -2303,7 +2303,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) anAction = new QtxAction(tr("MNU_RIGHT_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_RIGHT" ) ), - tr( "MNU_RIGHT_VIEW" ), 0, this, false, "/#Viewers/View/Set Y-"); + tr( "MNU_RIGHT_VIEW" ), 0, this, false, "/View/ViewPoint/#SetDirOY-"); anAction->setStatusTip(tr("DSC_RIGHT_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onRightView())); this->addAction(anAction); @@ -2312,7 +2312,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) // rotate anticlockwise anAction = new QtxAction(tr("MNU_ANTICLOCKWISE_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_ANTICLOCKWISE" ) ), - tr( "MNU_ANTICLOCKWISE_VIEW" ), 0, this, false, "/#Viewers/View/Rotate anticlockwise"); + tr( "MNU_ANTICLOCKWISE_VIEW" ), 0, this, false, "/View/ViewPoint/#RotateAnticlockwise"); anAction->setStatusTip(tr("DSC_ANTICLOCKWISE_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onAntiClockWiseView())); this->addAction(anAction); @@ -2321,7 +2321,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) // rotate clockwise anAction = new QtxAction(tr("MNU_CLOCKWISE_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_CLOCKWISE" ) ), - tr( "MNU_CLOCKWISE_VIEW" ), 0, this, false, "/#Viewers/View/Rotate clockwise"); + tr( "MNU_CLOCKWISE_VIEW" ), 0, this, false, "/View/ViewPoint/#RotateClockwise"); anAction->setStatusTip(tr("DSC_CLOCKWISE_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onClockWiseView())); this->addAction(anAction); @@ -2330,7 +2330,7 @@ void SVTK_ViewWindow::createActions(SUIT_ResourceMgr* theResourceMgr) // Reset anAction = new QtxAction(tr("MNU_RESET_VIEW"), theResourceMgr->loadPixmap( "VTKViewer", tr( "ICON_VTKVIEWER_VIEW_RESET" ) ), - tr( "MNU_RESET_VIEW" ), 0, this, false, "/#Viewers/View/Reset"); + tr( "MNU_RESET_VIEW" ), 0, this, false, "/View/ViewPoint/#Reset"); anAction->setStatusTip(tr("DSC_RESET_VIEW")); connect(anAction, SIGNAL(triggered()), this, SLOT(onResetView())); this->addAction(anAction); diff --git a/src/SalomeApp/SalomeApp_Application.cxx b/src/SalomeApp/SalomeApp_Application.cxx index 6a5847fd4..ff57f77e0 100644 --- a/src/SalomeApp/SalomeApp_Application.cxx +++ b/src/SalomeApp/SalomeApp_Application.cxx @@ -344,12 +344,12 @@ void SalomeApp_Application::createActions() //! Dump study createAction( DumpStudyId, tr( "TOT_DESK_FILE_DUMP_STUDY" ), QIcon(), tr( "MEN_DESK_FILE_DUMP_STUDY" ), tr( "PRP_DESK_FILE_DUMP_STUDY" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onDumpStudy() ), "/PRP_DESK_FILE_DUMP_STUDY" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onDumpStudy() ), "/File/Study_ScriptPy_DumpTo" ); //! Load script createAction( LoadScriptId, tr( "TOT_DESK_FILE_LOAD_SCRIPT" ), QIcon(), tr( "MEN_DESK_FILE_LOAD_SCRIPT" ), tr( "PRP_DESK_FILE_LOAD_SCRIPT" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onLoadScript() ), "/PRP_DESK_FILE_LOAD_SCRIPT" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onLoadScript() ), "/File/Study_ScriptPy_LoadFrom" ); //! Properties createAction( PropertiesId, tr( "TOT_DESK_PROPERTIES" ), QIcon(), @@ -359,7 +359,7 @@ void SalomeApp_Application::createActions() //! Catalog Generator createAction( CatalogGenId, tr( "TOT_DESK_CATALOG_GENERATOR" ), QIcon(), tr( "MEN_DESK_CATALOG_GENERATOR" ), tr( "PRP_DESK_CATALOG_GENERATOR" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onCatalogGen() ), "/PRP_DESK_CATALOG_GENERATOR" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onCatalogGen() ), "/Tools/CatalogGenerator" ); //! Registry Display createAction( RegDisplayId, tr( "TOT_DESK_REGISTRY_DISPLAY" ), QIcon(), @@ -369,17 +369,17 @@ void SalomeApp_Application::createActions() //! Find action dialog createAction( FindActionId, tr( "TOT_DESK_FIND_ACTION" ), QIcon(), tr( "MEN_DESK_FIND_ACTION" ), tr( "PRP_DESK_FIND_ACTION" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onFindAction() ), "/PRP_DESK_FIND_ACTION" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onFindAction() ), "/Tools/FindAction" ); createAction( ConnectId, tr( "TOT_DESK_CONNECT_STUDY" ), QIcon(), - tr( "MEN_DESK_CONNECT" ), tr( "PRP_DESK_CONNECT" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onLoadDoc() ), "/PRP_DESK_CONNECT" ); + tr( "MEN_DESK_CONNECT" ), tr( "File/Study_Connection_Connect" ), + QKeySequence::UnknownKey, desk, false, this, SLOT( onLoadDoc() ), "/File/Study_Connection_Connect" ); //no need at this action for mono-study application because study is always exists action( ConnectId )->setVisible( false ); createAction( DisconnectId, tr( "TOT_DESK_DISCONNECT_STUDY" ), QIcon(), tr( "MEN_DESK_DISCONNECT" ), tr( "PRP_DESK_DISCONNECT" ), - QKeySequence::UnknownKey, desk, false, this, SLOT( onUnloadDoc() ), "/PRP_DESK_DISCONNECT" ); + QKeySequence::UnknownKey, desk, false, this, SLOT( onUnloadDoc() ), "/File/Study_Connection_Disconnect" ); //no need at this action for mono-study application because study is always exists action( DisconnectId )->setVisible( false ); diff --git a/tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx b/tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx deleted file mode 100644 index bfc86f94a..000000000 Binary files a/tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx and /dev/null differ