1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "qmljsoutline.h"
27 #include "qmloutlinemodel.h"
28 #include "qmljseditor.h"
29 #include "qmljsoutlinetreeview.h"
30 
31 #include <coreplugin/find/itemviewfind.h>
32 #include <coreplugin/icore.h>
33 #include <coreplugin/idocument.h>
34 #include <coreplugin/editormanager/editormanager.h>
35 
36 #include <QSettings>
37 #include <QAction>
38 #include <QVBoxLayout>
39 #include <QTextBlock>
40 
41 using namespace QmlJS;
42 
43 enum {
44     debug = false
45 };
46 
47 namespace QmlJSEditor {
48 namespace Internal {
49 
QmlJSOutlineFilterModel(QObject * parent)50 QmlJSOutlineFilterModel::QmlJSOutlineFilterModel(QObject *parent) :
51     QSortFilterProxyModel(parent)
52 {
53     setDynamicSortFilter(true);
54 }
55 
flags(const QModelIndex & index) const56 Qt::ItemFlags QmlJSOutlineFilterModel::flags(const QModelIndex &index) const
57 {
58     Qt::ItemFlags f = sourceModel()->flags(index);
59     if (m_sorted)
60         f.setFlag(Qt::ItemIsDropEnabled, false);
61     return f;
62 }
63 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const64 bool QmlJSOutlineFilterModel::filterAcceptsRow(int sourceRow,
65                                                const QModelIndex &sourceParent) const
66 {
67     if (m_filterBindings) {
68         QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent);
69         QVariant itemType = sourceIndex.data(QmlOutlineModel::ItemTypeRole);
70         if (itemType == QmlOutlineModel::NonElementBindingType)
71             return false;
72     }
73     return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
74 }
75 
lessThan(const QModelIndex & sourceLeft,const QModelIndex & sourceRight) const76 bool QmlJSOutlineFilterModel::lessThan(const QModelIndex &sourceLeft,
77                                        const QModelIndex &sourceRight) const
78 {
79     if (!m_sorted)
80         return sourceLeft.row() > sourceRight.row();
81 
82     return sourceLeft.data().toString() > sourceRight.data().toString();
83 }
84 
data(const QModelIndex & index,int role) const85 QVariant QmlJSOutlineFilterModel::data(const QModelIndex &index, int role) const
86 {
87     if (role == QmlOutlineModel::AnnotationRole) {
88         // Don't show element id etc behind element if the property is also visible
89         if (!filterBindings()
90                 && index.data(QmlOutlineModel::ItemTypeRole) == QmlOutlineModel::ElementType) {
91             return QVariant();
92         }
93     }
94     return QSortFilterProxyModel::data(index, role);
95 }
96 
supportedDragActions() const97 Qt::DropActions QmlJSOutlineFilterModel::supportedDragActions() const
98 {
99     return sourceModel()->supportedDragActions();
100 }
101 
filterBindings() const102 bool QmlJSOutlineFilterModel::filterBindings() const
103 {
104     return m_filterBindings;
105 }
106 
setFilterBindings(bool filterBindings)107 void QmlJSOutlineFilterModel::setFilterBindings(bool filterBindings)
108 {
109     m_filterBindings = filterBindings;
110     invalidateFilter();
111 }
112 
setSorted(bool sorted)113 void QmlJSOutlineFilterModel::setSorted(bool sorted)
114 {
115     m_sorted = sorted;
116     invalidate();
117 }
118 
QmlJSOutlineWidget(QWidget * parent)119 QmlJSOutlineWidget::QmlJSOutlineWidget(QWidget *parent)
120     : TextEditor::IOutlineWidget(parent)
121     , m_treeView(new QmlJSOutlineTreeView(this))
122     , m_filterModel(new QmlJSOutlineFilterModel(this))
123 {
124     m_filterModel->setFilterBindings(false);
125 
126     m_treeView->setModel(m_filterModel);
127     m_treeView->setSortingEnabled(true);
128 
129     setFocusProxy(m_treeView);
130 
131     auto layout = new QVBoxLayout;
132 
133     layout->setContentsMargins(0, 0, 0, 0);
134     layout->setSpacing(0);
135     layout->addWidget(Core::ItemViewFind::createSearchableWrapper(m_treeView));
136 
137     m_showBindingsAction = new QAction(this);
138     m_showBindingsAction->setText(tr("Show All Bindings"));
139     m_showBindingsAction->setCheckable(true);
140     m_showBindingsAction->setChecked(true);
141     connect(m_showBindingsAction, &QAction::toggled, this, &QmlJSOutlineWidget::setShowBindings);
142 
143     setLayout(layout);
144 }
145 
setEditor(QmlJSEditorWidget * editor)146 void QmlJSOutlineWidget::setEditor(QmlJSEditorWidget *editor)
147 {
148     m_editor = editor;
149 
150     m_filterModel->setSourceModel(m_editor->qmlJsEditorDocument()->outlineModel());
151     m_treeView->expandAll();
152     connect(m_editor->qmlJsEditorDocument()->outlineModel(), &QAbstractItemModel::modelAboutToBeReset, m_treeView, [this]() {
153         if (m_treeView->selectionModel())
154             m_treeView->selectionModel()->blockSignals(true);
155     });
156     connect(m_editor->qmlJsEditorDocument()->outlineModel(), &QAbstractItemModel::modelReset, m_treeView, [this]() {
157         if (m_treeView->selectionModel())
158             m_treeView->selectionModel()->blockSignals(false);
159     });
160 
161     connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged,
162             this, &QmlJSOutlineWidget::updateSelectionInText);
163 
164     connect(m_treeView, &QAbstractItemView::activated,
165             this, &QmlJSOutlineWidget::focusEditor);
166 
167     connect(m_editor, &QmlJSEditorWidget::outlineModelIndexChanged,
168             this, &QmlJSOutlineWidget::updateSelectionInTree);
169     connect(m_editor->qmlJsEditorDocument()->outlineModel(), &QmlOutlineModel::updated, this, [this] () {
170         m_treeView->expandAll();
171         m_editor->updateOutlineIndexNow();
172     });
173 }
174 
filterMenuActions() const175 QList<QAction*> QmlJSOutlineWidget::filterMenuActions() const
176 {
177     return {m_showBindingsAction};
178 }
179 
setCursorSynchronization(bool syncWithCursor)180 void QmlJSOutlineWidget::setCursorSynchronization(bool syncWithCursor)
181 {
182     m_enableCursorSync = syncWithCursor;
183     m_editor->updateOutlineIndexNow();
184 }
185 
setSorted(bool sorted)186 void QmlJSOutlineWidget::setSorted(bool sorted)
187 {
188     m_sorted = sorted;
189     m_filterModel->setSorted(m_sorted);
190 }
191 
restoreSettings(const QVariantMap & map)192 void QmlJSOutlineWidget::restoreSettings(const QVariantMap &map)
193 {
194     bool showBindings = map.value(QString::fromLatin1("QmlJSOutline.ShowBindings"), true).toBool();
195     m_showBindingsAction->setChecked(showBindings);
196     setSorted(map.value(QString("QmlJSOutline.Sort"), false).toBool());
197 }
198 
settings() const199 QVariantMap QmlJSOutlineWidget::settings() const
200 {
201     return {
202         {QString("QmlJSOutline.ShowBindings"), m_showBindingsAction->isChecked()},
203         {QString("QmlJSOutline.Sort"), m_sorted}
204     };
205 }
206 
updateSelectionInTree(const QModelIndex & index)207 void QmlJSOutlineWidget::updateSelectionInTree(const QModelIndex &index)
208 {
209     if (!syncCursor())
210         return;
211 
212     m_blockCursorSync = true;
213 
214     QModelIndex baseIndex = index;
215     QModelIndex filterIndex = m_filterModel->mapFromSource(baseIndex);
216     while (baseIndex.isValid() && !filterIndex.isValid()) { // Search for ancestor index actually shown
217         baseIndex = baseIndex.parent();
218         filterIndex = m_filterModel->mapFromSource(baseIndex);
219     }
220 
221     m_treeView->setCurrentIndex(filterIndex);
222     m_treeView->scrollTo(filterIndex);
223     m_blockCursorSync = false;
224 }
225 
updateSelectionInText(const QItemSelection & selection)226 void QmlJSOutlineWidget::updateSelectionInText(const QItemSelection &selection)
227 {
228     if (!syncCursor())
229         return;
230 
231     if (!selection.indexes().isEmpty()) {
232         QModelIndex index = selection.indexes().first();
233 
234         updateTextCursor(index);
235     }
236 }
237 
updateTextCursor(const QModelIndex & index)238 void QmlJSOutlineWidget::updateTextCursor(const QModelIndex &index)
239 {
240     const auto update = [this](const QModelIndex &index) {
241         if (!m_editor->isOutlineCursorChangesBlocked()) {
242             QModelIndex sourceIndex = m_filterModel->mapToSource(index);
243 
244             SourceLocation location
245                     = m_editor->qmlJsEditorDocument()->outlineModel()->sourceLocation(sourceIndex);
246 
247             if (!location.isValid())
248                 return;
249 
250             const QTextBlock lastBlock = m_editor->document()->lastBlock();
251             const uint textLength = lastBlock.position() + lastBlock.length();
252             if (location.offset >= textLength)
253                 return;
254 
255             Core::EditorManager::cutForwardNavigationHistory();
256             Core::EditorManager::addCurrentPositionToNavigationHistory();
257 
258             QTextCursor textCursor = m_editor->textCursor();
259 
260             textCursor.setPosition(location.offset);
261             m_editor->setTextCursor(textCursor);
262             m_editor->centerCursor();
263         }
264     };
265     m_blockCursorSync = true;
266     update(index);
267     m_blockCursorSync = false;
268 }
269 
focusEditor()270 void QmlJSOutlineWidget::focusEditor()
271 {
272     m_editor->setFocus();
273 }
274 
setShowBindings(bool showBindings)275 void QmlJSOutlineWidget::setShowBindings(bool showBindings)
276 {
277     m_filterModel->setFilterBindings(!showBindings);
278     m_treeView->expandAll();
279     m_editor->updateOutlineIndexNow();
280 }
281 
syncCursor()282 bool QmlJSOutlineWidget::syncCursor()
283 {
284     return m_enableCursorSync && !m_blockCursorSync;
285 }
286 
supportsEditor(Core::IEditor * editor) const287 bool QmlJSOutlineWidgetFactory::supportsEditor(Core::IEditor *editor) const
288 {
289     if (qobject_cast<QmlJSEditor*>(editor))
290         return true;
291     return false;
292 }
293 
createWidget(Core::IEditor * editor)294 TextEditor::IOutlineWidget *QmlJSOutlineWidgetFactory::createWidget(Core::IEditor *editor)
295 {
296     auto widget = new QmlJSOutlineWidget;
297 
298     auto qmlJSEditable = qobject_cast<const QmlJSEditor*>(editor);
299     auto qmlJSEditor = qobject_cast<QmlJSEditorWidget*>(qmlJSEditable->widget());
300     Q_ASSERT(qmlJSEditor);
301 
302     widget->setEditor(qmlJSEditor);
303 
304     return widget;
305 }
306 
307 } // namespace Internal
308 } // namespace QmlJSEditor
309