forked from qt-creator/qt-creator
TextEditor: Add type hierarchy infrastructure
We want to support more than one back-end in the future. Task-number: QTCREATORBUG-28116 Change-Id: I72020c94b36072a297e13f44130e5e2482922cd4 Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -22,9 +22,6 @@ const char M_REFACTORING_MENU_INSERTION_POINT[] = "CppEditor.RefactorGroup";
|
||||
const char UPDATE_CODEMODEL[] = "CppEditor.UpdateCodeModel";
|
||||
const char INSPECT_CPP_CODEMODEL[] = "CppEditor.InspectCppCodeModel";
|
||||
|
||||
const char TYPE_HIERARCHY_ID[] = "CppEditor.TypeHierarchy";
|
||||
const char OPEN_TYPE_HIERARCHY[] = "CppEditor.OpenTypeHierarchy";
|
||||
|
||||
const char INCLUDE_HIERARCHY_ID[] = "CppEditor.IncludeHierarchy";
|
||||
const char OPEN_INCLUDE_HIERARCHY[] = "CppEditor.OpenIncludeHierarchy";
|
||||
|
||||
|
@@ -144,6 +144,7 @@ public:
|
||||
| TextEditorActionHandler::FollowSymbolUnderCursor
|
||||
| TextEditorActionHandler::FollowTypeUnderCursor
|
||||
| TextEditorActionHandler::RenameSymbol
|
||||
| TextEditorActionHandler::TypeHierarchy
|
||||
| TextEditorActionHandler::FindUsage);
|
||||
}
|
||||
};
|
||||
@@ -358,6 +359,7 @@ void CppEditorPlugin::addPerSymbolActions()
|
||||
|
||||
setupCppTypeHierarchy();
|
||||
|
||||
addSymbolActionToMenus(TextEditor::Constants::OPEN_TYPE_HIERARCHY);
|
||||
addSymbolActionToMenus(TextEditor::Constants::OPEN_CALL_HIERARCHY);
|
||||
|
||||
// Refactoring sub-menu
|
||||
|
@@ -4,6 +4,7 @@
|
||||
#include "cpptypehierarchy.h"
|
||||
|
||||
#include "cppeditorconstants.h"
|
||||
#include "cppeditordocument.h"
|
||||
#include "cppeditortr.h"
|
||||
#include "cppeditorwidget.h"
|
||||
#include "cppelementevaluator.h"
|
||||
@@ -11,11 +12,10 @@
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/find/itemviewfind.h>
|
||||
#include <coreplugin/inavigationwidgetfactory.h>
|
||||
#include <coreplugin/navigationwidget.h>
|
||||
#include <coreplugin/progressmanager/progressmanager.h>
|
||||
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/typehierarchy.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/delegates.h>
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
};
|
||||
|
||||
class CppTypeHierarchyWidget : public QWidget
|
||||
class CppTypeHierarchyWidget : public TextEditor::TypeHierarchyWidget
|
||||
{
|
||||
public:
|
||||
CppTypeHierarchyWidget();
|
||||
@@ -61,6 +61,8 @@ public:
|
||||
void perform();
|
||||
|
||||
private:
|
||||
void reload() override { perform(); }
|
||||
|
||||
void displayHierarchy();
|
||||
typedef QList<CppClass> CppClass::*HierarchyMember;
|
||||
void performFromExpression(const QString &expression, const FilePath &filePath);
|
||||
@@ -87,6 +89,7 @@ private:
|
||||
ProgressIndicator *m_progressIndicator = nullptr;
|
||||
QString m_oldClass;
|
||||
bool m_showOldClass = false;
|
||||
int m_runningIndexers = 0;
|
||||
};
|
||||
|
||||
enum ItemRole {
|
||||
@@ -197,10 +200,22 @@ CppTypeHierarchyWidget::CppTypeHierarchyWidget()
|
||||
|
||||
connect(&m_futureWatcher, &QFutureWatcher<void>::finished,
|
||||
this, &CppTypeHierarchyWidget::displayHierarchy);
|
||||
|
||||
connect(ProgressManager::instance(), &ProgressManager::taskStarted, [this](Id type) {
|
||||
if (type == Constants::TASK_INDEX)
|
||||
++m_runningIndexers;
|
||||
});
|
||||
connect(ProgressManager::instance(), &ProgressManager::allTasksFinished, [this](Id type) {
|
||||
if (type == Constants::TASK_INDEX)
|
||||
--m_runningIndexers;
|
||||
});
|
||||
}
|
||||
|
||||
void CppTypeHierarchyWidget::perform()
|
||||
{
|
||||
if (m_runningIndexers > 0)
|
||||
return;
|
||||
|
||||
if (m_future.isRunning())
|
||||
m_future.cancel();
|
||||
|
||||
@@ -397,47 +412,19 @@ QMimeData *CppTypeHierarchyModel::mimeData(const QModelIndexList &indexes) const
|
||||
|
||||
// CppTypeHierarchyFactory
|
||||
|
||||
class CppTypeHierarchyFactory final : public INavigationWidgetFactory
|
||||
class CppTypeHierarchyFactory final : public TextEditor::TypeHierarchyWidgetFactory
|
||||
{
|
||||
public:
|
||||
CppTypeHierarchyFactory()
|
||||
TextEditor::TypeHierarchyWidget *createWidget(Core::IEditor *editor) final
|
||||
{
|
||||
setDisplayName(Tr::tr("Type Hierarchy"));
|
||||
setPriority(700);
|
||||
setId(Constants::TYPE_HIERARCHY_ID);
|
||||
const auto textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
||||
if (!textEditor)
|
||||
return nullptr;
|
||||
const auto cppDoc = qobject_cast<CppEditorDocument *>(textEditor->textDocument());
|
||||
if (!cppDoc /* || cppDoc->usesClangd() */)
|
||||
return nullptr;
|
||||
|
||||
ActionBuilder openTypeHierarchy(this, Constants::OPEN_TYPE_HIERARCHY);
|
||||
openTypeHierarchy.setText(Tr::tr("Open Type Hierarchy"));
|
||||
openTypeHierarchy.setContext(Context(Constants::CPPEDITOR_ID));
|
||||
openTypeHierarchy.bindContextAction(&m_openTypeHierarchyAction);
|
||||
openTypeHierarchy.setDefaultKeySequence(Tr::tr("Meta+Shift+T"), Tr::tr("Ctrl+Shift+T"));
|
||||
openTypeHierarchy.addToContainers({Constants::M_TOOLS_CPP, Constants::M_CONTEXT},
|
||||
Constants::G_SYMBOL);
|
||||
|
||||
connect(m_openTypeHierarchyAction, &QAction::triggered, this, [] {
|
||||
NavigationWidget::activateSubWidget(Constants::TYPE_HIERARCHY_ID, Side::Left);
|
||||
});
|
||||
|
||||
connect(ProgressManager::instance(), &ProgressManager::taskStarted, [this](Id type) {
|
||||
if (type == Constants::TASK_INDEX)
|
||||
m_openTypeHierarchyAction->setEnabled(false);
|
||||
});
|
||||
connect(ProgressManager::instance(), &ProgressManager::allTasksFinished, [this](Id type) {
|
||||
if (type == Constants::TASK_INDEX)
|
||||
m_openTypeHierarchyAction->setEnabled(true);
|
||||
});
|
||||
return new CppTypeHierarchyWidget;
|
||||
}
|
||||
|
||||
NavigationView createWidget() final
|
||||
{
|
||||
auto w = new CppTypeHierarchyWidget;
|
||||
connect(m_openTypeHierarchyAction, &QAction::triggered, w, &CppTypeHierarchyWidget::perform);
|
||||
w->perform();
|
||||
|
||||
return {w, {}};
|
||||
}
|
||||
|
||||
QAction *m_openTypeHierarchyAction = nullptr;
|
||||
};
|
||||
|
||||
static CppTypeHierarchyFactory &cppTypeHierarchyFactory()
|
||||
@@ -446,11 +433,6 @@ static CppTypeHierarchyFactory &cppTypeHierarchyFactory()
|
||||
return theCppTypeHierarchyFactory;
|
||||
}
|
||||
|
||||
void openCppTypeHierarchy()
|
||||
{
|
||||
cppTypeHierarchyFactory().m_openTypeHierarchyAction->trigger();
|
||||
}
|
||||
|
||||
void setupCppTypeHierarchy()
|
||||
{
|
||||
(void) cppTypeHierarchyFactory(); // Trigger instantiation
|
||||
|
@@ -4,8 +4,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace CppEditor::Internal {
|
||||
|
||||
void openCppTypeHierarchy();
|
||||
void setupCppTypeHierarchy();
|
||||
|
||||
} // CppEditor::Internal
|
||||
|
@@ -9,7 +9,6 @@
|
||||
#include "cppinsertvirtualmethods.h"
|
||||
#include "cppmodelmanager.h"
|
||||
#include "cpptoolstestcase.h"
|
||||
#include "cpptypehierarchy.h"
|
||||
#include "cppworkingcopy.h"
|
||||
#include "projectinfo.h"
|
||||
|
||||
@@ -18,6 +17,7 @@
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/typehierarchy.h>
|
||||
|
||||
#include <cplusplus/CppDocument.h>
|
||||
#include <cplusplus/TranslationUnit.h>
|
||||
@@ -383,7 +383,7 @@ public:
|
||||
|
||||
void OpenTypeHierarchyTokenAction::run(CppEditorWidget *)
|
||||
{
|
||||
openCppTypeHierarchy();
|
||||
TextEditor::openTypeHierarchy();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
|
@@ -113,6 +113,7 @@ add_qtc_plugin(TextEditor
|
||||
textindenter.cpp textindenter.h
|
||||
textmark.cpp textmark.h
|
||||
textstyles.h
|
||||
typehierarchy.cpp typehierarchy.h
|
||||
typingsettings.cpp typingsettings.h
|
||||
)
|
||||
|
||||
|
@@ -8759,6 +8759,11 @@ void TextEditorWidget::appendStandardContextMenuActions(QMenu *menu)
|
||||
if (!menu->actions().contains(action))
|
||||
menu->addAction(action);
|
||||
}
|
||||
if (optionalActions() & TextEditorActionHandler::TypeHierarchy) {
|
||||
const auto action = ActionManager::command(Constants::OPEN_TYPE_HIERARCHY)->action();
|
||||
if (!menu->actions().contains(action))
|
||||
menu->addAction(action);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
appendMenuActionsFromContext(menu, Constants::M_STANDARDCONTEXTMENU);
|
||||
|
@@ -137,7 +137,8 @@ QtcPlugin {
|
||||
"texteditor.cpp",
|
||||
"texteditor.h",
|
||||
"texteditor.qrc",
|
||||
"texteditor_global.h", "texteditortr.h",
|
||||
"texteditor_global.h",
|
||||
"texteditortr.h",
|
||||
"texteditoractionhandler.cpp",
|
||||
"texteditoractionhandler.h",
|
||||
"texteditorconstants.cpp",
|
||||
@@ -152,6 +153,8 @@ QtcPlugin {
|
||||
"textmark.cpp",
|
||||
"textmark.h",
|
||||
"textstyles.h",
|
||||
"typehierarchy.cpp",
|
||||
"typehierarchy.h",
|
||||
"typingsettings.cpp",
|
||||
"typingsettings.h",
|
||||
]
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#include "linenumberfilter.h"
|
||||
#include "texteditortr.h"
|
||||
#include "texteditorsettings.h"
|
||||
#include "typehierarchy.h"
|
||||
|
||||
#include <aggregation/aggregate.h>
|
||||
|
||||
@@ -16,6 +17,7 @@
|
||||
#include <coreplugin/icore.h>
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/coreconstants.h>
|
||||
#include <coreplugin/navigationwidget.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/command.h>
|
||||
@@ -137,6 +139,7 @@ public:
|
||||
QAction *m_followToTypeInNextSplitAction = nullptr;
|
||||
QAction *m_findUsageAction = nullptr;
|
||||
QAction *m_openCallHierarchyAction = nullptr;
|
||||
QAction *m_openTypeHierarchyAction = nullptr;
|
||||
QAction *m_renameSymbolAction = nullptr;
|
||||
QAction *m_jumpToFileAction = nullptr;
|
||||
QAction *m_jumpToFileInNextSplitAction = nullptr;
|
||||
@@ -269,7 +272,17 @@ void TextEditorActionHandlerPrivate::createActions()
|
||||
QKeySequence(Utils::HostOsInfo::isMacHost() ? Tr::tr("Meta+E, F2") : Tr::tr("Ctrl+E, F2")).toString());
|
||||
m_openCallHierarchyAction = registerAction(OPEN_CALL_HIERARCHY,
|
||||
[] (TextEditorWidget *w) { w->openCallHierarchy(); }, true, Tr::tr("Open Call Hierarchy"));
|
||||
|
||||
m_openTypeHierarchyAction = registerAction(
|
||||
OPEN_TYPE_HIERARCHY,
|
||||
[](TextEditorWidget *) {
|
||||
updateTypeHierarchy(
|
||||
NavigationWidget::activateSubWidget(Constants::TYPE_HIERARCHY_FACTORY_ID,
|
||||
Side::Left));
|
||||
},
|
||||
true,
|
||||
Tr::tr("Open Type Hierarchy"),
|
||||
QKeySequence(Utils::HostOsInfo::isMacHost() ? Tr::tr("Meta+Shift+T")
|
||||
: Tr::tr("Ctrl+Shift+T")));
|
||||
registerAction(VIEW_PAGE_UP,
|
||||
[] (TextEditorWidget *w) { w->viewPageUp(); }, true, Tr::tr("Move the View a Page Up and Keep the Cursor Position"),
|
||||
QKeySequence(Tr::tr("Ctrl+PgUp")));
|
||||
@@ -550,6 +563,8 @@ void TextEditorActionHandlerPrivate::updateOptionalActions()
|
||||
optionalActions & TextEditorActionHandler::RenameSymbol);
|
||||
m_openCallHierarchyAction->setEnabled(
|
||||
optionalActions & TextEditorActionHandler::CallHierarchy);
|
||||
m_openTypeHierarchyAction->setEnabled(
|
||||
optionalActions & TextEditorActionHandler::TypeHierarchy);
|
||||
|
||||
bool formatEnabled = (optionalActions & TextEditorActionHandler::Format)
|
||||
&& m_currentEditorWidget && !m_currentEditorWidget->isReadOnly();
|
||||
|
@@ -38,7 +38,8 @@ public:
|
||||
JumpToFileUnderCursor = 32,
|
||||
RenameSymbol = 64,
|
||||
FindUsage = 128,
|
||||
CallHierarchy = 256
|
||||
CallHierarchy = 256,
|
||||
TypeHierarchy = 512,
|
||||
};
|
||||
using TextEditorWidgetResolver = std::function<TextEditorWidget *(Core::IEditor *)>;
|
||||
|
||||
|
@@ -212,6 +212,8 @@ const char FIND_USAGES[] = "TextEditor.FindUsages";
|
||||
// moved from CppEditor to TextEditor avoid breaking the setting by using the old key
|
||||
const char RENAME_SYMBOL[] = "CppEditor.RenameSymbolUnderCursor";
|
||||
const char OPEN_CALL_HIERARCHY[] = "TextEditor.OpenCallHierarchy";
|
||||
const char OPEN_TYPE_HIERARCHY[] = "TextEditor.OpenTypeHierarchy";
|
||||
const char TYPE_HIERARCHY_FACTORY_ID[] = "TextEditor.TypeHierarchy";
|
||||
const char JUMP_TO_FILE_UNDER_CURSOR[] = "TextEditor.JumpToFileUnderCursor";
|
||||
const char JUMP_TO_FILE_UNDER_CURSOR_IN_NEXT_SPLIT[] = "TextEditor.JumpToFileUnderCursorInNextSplit";
|
||||
|
||||
|
@@ -26,6 +26,7 @@
|
||||
#include "texteditorsettings.h"
|
||||
#include "texteditortr.h"
|
||||
#include "textmark.h"
|
||||
#include "typehierarchy.h"
|
||||
#include "typingsettings.h"
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
@@ -105,6 +106,7 @@ void TextEditorPlugin::initialize()
|
||||
|
||||
setupTextMarkRegistry(this);
|
||||
setupOutlineFactory();
|
||||
setupTypeHierarchyFactory();
|
||||
setupLineNumberFilter(); // Goto line functionality for quick open
|
||||
|
||||
setupPlainTextEditor();
|
||||
|
136
src/plugins/texteditor/typehierarchy.cpp
Normal file
136
src/plugins/texteditor/typehierarchy.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "typehierarchy.h"
|
||||
|
||||
#include "texteditorconstants.h"
|
||||
#include "texteditortr.h"
|
||||
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/inavigationwidgetfactory.h>
|
||||
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
#include <QStackedWidget>
|
||||
#include <QToolButton>
|
||||
|
||||
using namespace Utils;
|
||||
|
||||
namespace TextEditor {
|
||||
namespace Internal {
|
||||
|
||||
static QList<TypeHierarchyWidgetFactory *> g_widgetFactories;
|
||||
|
||||
class TypeHierarchyFactory : public Core::INavigationWidgetFactory
|
||||
{
|
||||
public:
|
||||
TypeHierarchyFactory()
|
||||
{
|
||||
setDisplayName(Tr::tr("Type Hierarchy"));
|
||||
setPriority(649);
|
||||
setId(Constants::TYPE_HIERARCHY_FACTORY_ID);
|
||||
}
|
||||
|
||||
private:
|
||||
Core::NavigationView createWidget() override;
|
||||
};
|
||||
|
||||
class TypeHierarchyWidgetStack : public QStackedWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TypeHierarchyWidgetStack();
|
||||
|
||||
void reload();
|
||||
};
|
||||
|
||||
static TypeHierarchyFactory &typeHierarchyFactory()
|
||||
{
|
||||
static TypeHierarchyFactory theFactory;
|
||||
return theFactory;
|
||||
}
|
||||
|
||||
void setupTypeHierarchyFactory()
|
||||
{
|
||||
(void) typeHierarchyFactory();
|
||||
}
|
||||
|
||||
TypeHierarchyWidgetStack::TypeHierarchyWidgetStack()
|
||||
{
|
||||
QLabel *label = new QLabel(Tr::tr("No type hierarchy available"), this);
|
||||
label->setAlignment(Qt::AlignCenter);
|
||||
|
||||
label->setAutoFillBackground(true);
|
||||
label->setBackgroundRole(QPalette::Base);
|
||||
|
||||
addWidget(label);
|
||||
reload();
|
||||
}
|
||||
|
||||
void TypeHierarchyWidgetStack::reload()
|
||||
{
|
||||
const auto editor = Core::EditorManager::currentEditor();
|
||||
TypeHierarchyWidget *newWidget = nullptr;
|
||||
|
||||
if (editor) {
|
||||
for (TypeHierarchyWidgetFactory * const widgetFactory : std::as_const(g_widgetFactories)) {
|
||||
if ((newWidget = widgetFactory->createWidget(editor)))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QWidget * const current = currentWidget();
|
||||
if (current) {
|
||||
removeWidget(current);
|
||||
current->deleteLater();
|
||||
}
|
||||
if (newWidget) {
|
||||
addWidget(newWidget);
|
||||
setCurrentWidget(newWidget);
|
||||
setFocusProxy(newWidget);
|
||||
newWidget->reload();
|
||||
}
|
||||
}
|
||||
|
||||
Core::NavigationView TypeHierarchyFactory::createWidget()
|
||||
{
|
||||
const auto placeholder = new TypeHierarchyWidgetStack;
|
||||
const auto reloadButton = new QToolButton;
|
||||
reloadButton->setIcon(Icons::RELOAD_TOOLBAR.icon());
|
||||
reloadButton->setToolTip(Tr::tr("Reloads the type hierarchy for the symbol under the cursor."));
|
||||
connect(reloadButton, &QToolButton::clicked, placeholder, &TypeHierarchyWidgetStack::reload);
|
||||
return {placeholder, {reloadButton}};
|
||||
}
|
||||
|
||||
void updateTypeHierarchy(QWidget *widget)
|
||||
{
|
||||
if (const auto w = qobject_cast<TypeHierarchyWidgetStack *>(widget))
|
||||
w->reload();
|
||||
}
|
||||
|
||||
} // namespace Internal
|
||||
|
||||
TypeHierarchyWidgetFactory::TypeHierarchyWidgetFactory()
|
||||
{
|
||||
Internal::g_widgetFactories.append(this);
|
||||
}
|
||||
|
||||
TypeHierarchyWidgetFactory::~TypeHierarchyWidgetFactory()
|
||||
{
|
||||
Internal::g_widgetFactories.removeOne(this);
|
||||
}
|
||||
|
||||
void openTypeHierarchy()
|
||||
{
|
||||
if (const auto action = Core::ActionManager::command(Constants::OPEN_TYPE_HIERARCHY)->action())
|
||||
action->trigger();
|
||||
}
|
||||
|
||||
} // namespace TextEditor
|
||||
|
||||
#include <typehierarchy.moc>
|
39
src/plugins/texteditor/typehierarchy.h
Normal file
39
src/plugins/texteditor/typehierarchy.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <texteditor/texteditor_global.h>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace Core { class IEditor; }
|
||||
|
||||
namespace TextEditor {
|
||||
namespace Internal {
|
||||
void setupTypeHierarchyFactory();
|
||||
void updateTypeHierarchy(QWidget *widget);
|
||||
}
|
||||
|
||||
class TEXTEDITOR_EXPORT TypeHierarchyWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual void reload() = 0;
|
||||
};
|
||||
|
||||
class TEXTEDITOR_EXPORT TypeHierarchyWidgetFactory : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
virtual TypeHierarchyWidget *createWidget(Core::IEditor *editor) = 0;
|
||||
|
||||
protected:
|
||||
TypeHierarchyWidgetFactory();
|
||||
~TypeHierarchyWidgetFactory() override;
|
||||
};
|
||||
|
||||
TEXTEDITOR_EXPORT void openTypeHierarchy();
|
||||
|
||||
} // namespace TextEditor
|
Reference in New Issue
Block a user