]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/TocWidget.cpp
Move Lexer to support/ directory (and lyx::support namespace)
[lyx.git] / src / frontends / qt / TocWidget.cpp
1 /**
2  * \file TocWidget.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Abdelrazak Younes
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "TocWidget.h"
15
16 #include "GuiApplication.h"
17 #include "GuiView.h"
18 #include "qt_helpers.h"
19 #include "TocModel.h"
20 #include "FancyLineEdit.h"
21
22 #include "Buffer.h"
23 #include "BufferView.h"
24 #include "Cursor.h"
25 #include "CutAndPaste.h"
26 #include "FuncRequest.h"
27 #include "FuncStatus.h"
28 #include "LyX.h"
29 #include "Menus.h"
30 #include "TocBackend.h"
31
32 #include "insets/InsetCommand.h"
33 #include "insets/InsetRef.h"
34
35 #include "support/debug.h"
36 #include "support/lassert.h"
37
38 #include <QHeaderView>
39 #include <QMenu>
40 #include <QTimer>
41
42 #include <vector>
43
44 using namespace std;
45
46 namespace lyx {
47 namespace frontend {
48
49 TocWidget::TocWidget(GuiView & gui_view, QWidget * parent)
50         : QWidget(parent), depth_(0), persistent_(false), keep_expanded_(false),
51           gui_view_(gui_view), timer_(new QTimer(this))
52 {
53         setupUi(this);
54
55         moveOutTB->setIcon(QIcon(getPixmap("images/", "outline-out", "svgz,png")));
56         moveInTB->setIcon(QIcon(getPixmap("images/", "outline-in", "svgz,png")));
57         moveUpTB->setIcon(QIcon(getPixmap("images/", "outline-up", "svgz,png")));
58         moveDownTB->setIcon(QIcon(getPixmap("images/", "outline-down", "svgz,png")));
59         updateTB->setIcon(QIcon(getPixmap("images/", "reload", "svgz,png")));
60
61         QSize icon_size = gui_view.iconSize();
62         moveOutTB->setIconSize(icon_size);
63         moveInTB->setIconSize(icon_size);
64         moveUpTB->setIconSize(icon_size);
65         moveDownTB->setIconSize(icon_size);
66         updateTB->setIconSize(icon_size);
67         // update icon size according to gui view
68         connect(&gui_view, SIGNAL(iconSizeChanged(QSize)),
69                 moveOutTB, SLOT(setIconSize(QSize)));
70         connect(&gui_view, SIGNAL(iconSizeChanged(QSize)),
71                 moveInTB, SLOT(setIconSize(QSize)));
72         connect(&gui_view, SIGNAL(iconSizeChanged(QSize)),
73                 moveUpTB, SLOT(setIconSize(QSize)));
74         connect(&gui_view, SIGNAL(iconSizeChanged(QSize)),
75                 moveDownTB, SLOT(setIconSize(QSize)));
76         connect(&gui_view, SIGNAL(iconSizeChanged(QSize)),
77                 updateTB, SLOT(setIconSize(QSize)));
78
79         // avoid flickering
80         tocTV->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
81
82         tocTV->showColumn(0);
83
84         // hide the pointless QHeader for now
85         // in the future, new columns may appear
86         // like labels, bookmarks, etc...
87         // tocTV->header()->hide();
88         tocTV->header()->setVisible(false);
89
90         // Only one item selected at a time.
91         tocTV->setSelectionMode(QAbstractItemView::SingleSelection);
92
93         // The toc types combo won't change its model.
94         typeCO->setModel(gui_view_.tocModels().nameModel());
95
96         // The filter bar
97         filter_ = new FancyLineEdit(this);
98         filter_->setClearButton(true);
99         filter_->setPlaceholderText(qt_("All items"));
100         filterBarL->addWidget(filter_, 0);
101         filterLA->setBuddy(filter_);
102         setFocusProxy(filter_);
103
104         // Make sure the buttons are disabled when first shown without a loaded
105         // Buffer.
106         enableControls(false);
107
108         // make us responsible for the context menu of the tabbar
109         setContextMenuPolicy(Qt::CustomContextMenu);
110         connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
111                 this, SLOT(showContextMenu(const QPoint &)));
112         connect(tocTV, SIGNAL(customContextMenuRequested(const QPoint &)),
113                 this, SLOT(showContextMenu(const QPoint &)));
114         connect(filter_, SIGNAL(textEdited(QString)),
115                 this, SLOT(filterContents()));
116         connect(filter_, &FancyLineEdit::downPressed,
117                 tocTV, [this](){ focusAndHighlight(tocTV); });
118         connect(activeFilterCO, SIGNAL(activated(int)),
119                 this, SLOT(filterContents()));
120
121         // setting the update timer
122         timer_->setSingleShot(true);
123         connect(timer_, SIGNAL(timeout()), this, SLOT(finishUpdateView()));
124
125         init(QString());
126 }
127
128
129 void TocWidget::showContextMenu(const QPoint & pos)
130 {
131         std::string name = "context-toc-" + fromqstr(current_type_);
132         QMenu * menu = guiApp->menus().menu(toqstr(name), gui_view_);
133         if (!menu)
134                 return;
135         menu->exec(mapToGlobal(pos));
136 }
137
138
139 Inset * TocWidget::itemInset() const
140 {
141         QModelIndex const & index = tocTV->currentIndex();
142         TocItem const & item =
143                 gui_view_.tocModels().currentItem(current_type_, index);
144         DocIterator const & dit = item.dit();
145
146         Inset * inset = nullptr;
147         if (current_type_ == "label"
148                   || current_type_ == "graphics"
149                   || current_type_ == "citation"
150                   || current_type_ == "child")
151                 inset = dit.nextInset();
152
153         else if (current_type_ == "branch"
154                          || current_type_ == "index"
155                          || current_type_ == "change"
156                          || current_type_ == "table"
157                      || current_type_ == "listing"
158                      || current_type_ == "figure")
159                 inset = &dit.inset();
160
161         return inset;
162 }
163
164
165 bool TocWidget::getStatus(Cursor & cur, FuncRequest const & cmd,
166         FuncStatus & status) const
167 {
168         Inset * inset = itemInset();
169         FuncRequest tmpcmd(cmd);
170
171         QModelIndex const & index = tocTV->currentIndex();
172         TocItem const & item =
173                 gui_view_.tocModels().currentItem(current_type_, index);
174
175         switch (cmd.action())
176         {
177         case LFUN_CHANGE_ACCEPT:
178         case LFUN_CHANGE_REJECT:
179         case LFUN_OUTLINE_UP:
180         case LFUN_OUTLINE_DOWN:
181         case LFUN_OUTLINE_IN:
182         case LFUN_OUTLINE_OUT:
183         case LFUN_SECTION_SELECT:
184                 status.setEnabled((bool)item.dit());
185                 return true;
186
187         case LFUN_LABEL_COPY_AS_REFERENCE: {
188                 // For labels in math, we need to supply the label as a string
189                 FuncRequest label_copy(LFUN_LABEL_COPY_AS_REFERENCE, item.str());
190                 if (inset)
191                         return inset->getStatus(cur, label_copy, status);
192                 break;
193         }
194
195         default:
196                 if (inset)
197                         return inset->getStatus(cur, tmpcmd, status);
198         }
199
200         return false;
201 }
202
203
204 void TocWidget::doDispatch(Cursor & cur, FuncRequest const & cmd,
205                 DispatchResult & dr)
206 {
207         Inset * inset = itemInset();
208
209         QModelIndex const & index = tocTV->currentIndex();
210         TocItem const & item =
211                 gui_view_.tocModels().currentItem(current_type_, index);
212
213         // Start an undo group.
214         cur.beginUndoGroup();
215
216         switch (cmd.action())
217         {
218         case LFUN_CHANGE_ACCEPT:
219         case LFUN_CHANGE_REJECT: {
220                 // The action is almost always LYX_UNKNOWN_ACTION, which will
221                 // have the effect of moving the cursor to the location of
222                 // the change. (See TocItem::action.)
223                 dispatch(item.action());
224                 // If we do not reset the origin, then the request will be sent back
225                 // here, and we are in an infinite loop. But we need the dispatch
226                 // machinery to clean up for us, if the cursor is in an inset that
227                 // will be deleted. See bug #10316.
228                 FuncRequest tmpcmd(cmd);
229                 tmpcmd.setOrigin(FuncRequest::INTERNAL);
230                 dispatch(tmpcmd);
231                 dr.forceBufferUpdate();
232                 break;
233         }
234
235         case LFUN_SECTION_SELECT:
236                 dispatch(item.action());
237                 cur.dispatch(cmd);
238                 // necessary to get the selection drawn.
239                 cur.buffer()->changed(true);
240                 gui_view_.setFocus();
241                 break;
242
243         case LFUN_LABEL_COPY_AS_REFERENCE: {
244                 // For labels in math, we need to supply the label as a string
245                 FuncRequest label_copy(LFUN_LABEL_COPY_AS_REFERENCE, item.str());
246                 if (inset)
247                         inset->dispatch(cur, label_copy);
248                 break;
249         }
250
251         case LFUN_OUTLINE_UP:
252         case LFUN_OUTLINE_DOWN:
253         case LFUN_OUTLINE_IN:
254         case LFUN_OUTLINE_OUT:
255                 outline(cmd.action());
256                 break;
257
258         default: {
259                 FuncRequest tmpcmd(cmd);
260                 if (inset)
261                         inset->dispatch(cur, tmpcmd);
262         }
263         }
264         cur.endUndoGroup();
265 }
266
267
268 void TocWidget::on_tocTV_activated(QModelIndex const & index)
269 {
270         goTo(index);
271 }
272
273
274 void TocWidget::on_tocTV_pressed(QModelIndex const & index)
275 {
276         DocIterator const & dit = gui_view_.documentBufferView()->cursor();
277         keep_expanded_ = gui_view_.tocModels().currentIndex(current_type_, dit) == index;
278         Qt::MouseButtons const button = QApplication::mouseButtons();
279         if (button & Qt::LeftButton) {
280                 goTo(index);
281                 gui_view_.setFocus();
282                 gui_view_.activateWindow();
283         }
284 }
285
286
287 void TocWidget::on_tocTV_doubleClicked(QModelIndex const &)
288 {
289         keep_expanded_ = true;
290 }
291
292
293 void TocWidget::goTo(QModelIndex const & index)
294 {
295         LYXERR(Debug::GUI, "goto " << index.row()
296                 << ", " << index.column());
297
298         sendDispatch(gui_view_.tocModels().goTo(current_type_, index));
299 }
300
301
302 void TocWidget::on_updateTB_clicked()
303 {
304         // The backend update can take some time so we disable
305         // the controls while waiting.
306         enableControls(false);
307         gui_view_.currentBufferView()->buffer().updateBuffer();
308 }
309
310
311 void TocWidget::on_sortCB_stateChanged(int state)
312 {
313         gui_view_.tocModels().sort(current_type_, state == Qt::Checked);
314         updateViewNow();
315 }
316
317
318 void TocWidget::on_persistentCB_stateChanged(int state)
319 {
320         persistent_ = state == Qt::Checked;
321 }
322
323
324 #if 0
325 /* FIXME (Ugras 17/11/06):
326 I have implemented a indexDepth function to get the model indices. In my
327 opinion, somebody should derive a new qvariant class for tocModelItem
328 which saves the string data and depth information. That will save the
329 depth calculation.  */
330
331 static int indexDepth(QModelIndex const & index, int depth = -1)
332 {
333         ++depth;
334         return index.parent() == QModelIndex()
335                 ? depth : indexDepth(index.parent(), depth);
336 }
337 #endif
338
339 void TocWidget::on_depthSL_valueChanged(int depth)
340 {
341         if (depth == depth_)
342                 return;
343         setTreeDepth(depth);
344         gui_view_.setFocus();
345 }
346
347
348 void TocWidget::setTreeDepth(int depth, bool const maintain_current)
349 {
350         depth_ = depth;
351         if (!tocTV->model())
352                 return;
353
354         if (maintain_current)
355                 collapseAllOthers(depth);
356         else {
357                 if (depth == 0)
358                         tocTV->collapseAll();
359                 else
360                         tocTV->expandToDepth(depth - 1);
361         }
362 }
363
364
365 void TocWidget::on_typeCO_activated(int index)
366 {
367         if (index == -1)
368                 return;
369         current_type_ = typeCO->itemData(index).toString();
370         updateViewNow();
371         if (typeCO->hasFocus())
372                 gui_view_.setFocus();
373 }
374
375
376 void TocWidget::outline(FuncCode func_code)
377 {
378         QModelIndexList const & list = tocTV->selectionModel()->selectedIndexes();
379         if (list.isEmpty())
380                 return;
381
382         //if another window is active, this attempt will fail,
383         //but it will work at least for the second attempt
384         gui_view_.activateWindow();
385
386         enableControls(false);
387         goTo(list[0]);
388         sendDispatch(FuncRequest(func_code));
389         enableControls(true);
390         gui_view_.setFocus();
391 }
392
393
394 void TocWidget::sendDispatch(FuncRequest fr)
395 {
396         fr.setViewOrigin(&gui_view_);
397         GuiWorkArea * old_wa = gui_view_.currentWorkArea();
398         GuiWorkArea * doc_wa = gui_view_.currentMainWorkArea();
399         /* The ToC command should be dispatched to the document work area,
400          * not the Adv. Find&Replace (which is the only other know
401          * possibility.
402          */
403         if (doc_wa != nullptr && doc_wa != old_wa)
404                 gui_view_.setCurrentWorkArea(doc_wa);
405         DispatchResult const & dr = dispatch(fr);
406         /* If the current workarea has not explicitely changed, and the
407          * original one is still visible, let's reset it.
408          */
409         if (gui_view_.currentWorkArea() == doc_wa
410              && gui_view_.hasVisibleWorkArea(old_wa)
411              && doc_wa != old_wa)
412                 gui_view_.setCurrentWorkArea(old_wa);
413         if (dr.error())
414                 gui_view_.message(dr.message());
415 }
416
417
418 void TocWidget::on_moveUpTB_clicked()
419 {
420         outline(LFUN_OUTLINE_UP);
421 }
422
423
424 void TocWidget::on_moveDownTB_clicked()
425 {
426         outline(LFUN_OUTLINE_DOWN);
427 }
428
429
430 void TocWidget::on_moveInTB_clicked()
431 {
432         outline(LFUN_OUTLINE_IN);
433 }
434
435
436 void TocWidget::on_moveOutTB_clicked()
437 {
438         outline(LFUN_OUTLINE_OUT);
439 }
440
441
442 void TocWidget::select(QModelIndex const & index)
443 {
444         if (!index.isValid()) {
445                 LYXERR(Debug::GUI, "TocWidget::select(): QModelIndex is invalid!");
446                 return;
447         }
448
449         tocTV->scrollTo(index);
450         tocTV->clearSelection();
451         tocTV->setCurrentIndex(index);
452 }
453
454
455 void TocWidget::enableControls(bool enable)
456 {
457         updateTB->setEnabled(enable);
458
459         if (!canOutline())
460                 enable = false;
461
462         moveUpTB->setEnabled(enable);
463         moveDownTB->setEnabled(enable);
464         moveInTB->setEnabled(enable);
465         moveOutTB->setEnabled(enable);
466 }
467
468
469 void TocWidget::updateView()
470 {
471         if (!gui_view_.documentBufferView()) {
472                 tocTV->setModel(nullptr);
473                 depthSL->setMaximum(0);
474                 depthSL->setValue(0);
475                 setEnabled(false);
476                 return;
477         }
478         setEnabled(true);
479         bool const is_sortable = isSortable();
480         sortCB->setEnabled(is_sortable);
481         bool focus = tocTV->hasFocus();
482         tocTV->setEnabled(false);
483         tocTV->setUpdatesEnabled(false);
484
485         QAbstractItemModel * toc_model =
486                         gui_view_.tocModels().model(current_type_);
487         if (tocTV->model() != toc_model) {
488                 tocTV->setModel(toc_model);
489                 tocTV->setEditTriggers(QAbstractItemView::NoEditTriggers);
490                 if (persistent_)
491                         setTreeDepth(depth_);
492         }
493
494         sortCB->blockSignals(true);
495         sortCB->setChecked(is_sortable
496                 && gui_view_.tocModels().isSorted(current_type_));
497         sortCB->blockSignals(false);
498
499         persistentCB->setEnabled(canNavigate());
500
501         bool controls_enabled = toc_model && toc_model->rowCount() > 0
502                 && !gui_view_.documentBufferView()->buffer().isReadonly();
503         enableControls(controls_enabled);
504
505         depthSL->setMaximum(gui_view_.tocModels().depth(current_type_));
506         depthSL->setValue(depth_);
507         tocTV->setEnabled(true);
508         tocTV->setUpdatesEnabled(true);
509         if (focus)
510                 tocTV->setFocus();
511
512         // Expensive operations are on a timer.  We finish the update immediately
513         // for sparse edition actions, i.e. there was no edition/cursor movement
514         // recently, then every 300ms.
515         if (!timer_->isActive() && !keep_expanded_) {
516                 finishUpdateView();
517                 timer_->start(300);
518         }
519 }
520
521
522 void TocWidget::updateViewNow()
523 {
524         timer_->stop();
525         updateView();
526 }
527
528
529 void TocWidget::finishUpdateView()
530 {
531         // Profiling shows that this is the expensive stuff in the context of typing
532         // text and moving with arrows. For bigger operations, this is negligible,
533         // and outweighted by TocModels::reset() anyway.
534         if (canNavigate()) {
535                 if (!persistent_ && !keep_expanded_)
536                         setTreeDepth(depth_, true);
537                 keep_expanded_ = false;
538                 persistentCB->setChecked(persistent_);
539                 // select the item at current cursor location
540                 if (gui_view_.documentBufferView()) {
541                         DocIterator const & dit = gui_view_.documentBufferView()->cursor();
542                         select(gui_view_.tocModels().currentIndex(current_type_, dit));
543                 }
544         }
545         filterContents();
546 }
547
548
549 QModelIndexList TocWidget::getIndices()
550 {
551         QModelIndexList indices = tocTV->model()->match(
552                 tocTV->model()->index(0, 0),
553                 Qt::DisplayRole, ".*", -1,
554 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
555                 Qt::MatchFlags(Qt::MatchRegularExpression|Qt::MatchRecursive));
556 #else
557                 // deprecated in Qt 5.15.
558                 Qt::MatchFlags(Qt::MatchRegExp|Qt::MatchRecursive));
559 #endif
560         return indices;
561 }
562
563
564 void TocWidget::filterContents()
565 {
566         if (!tocTV->model())
567                 return;
568
569         QModelIndexList indices = getIndices();
570
571         bool const show_active =
572                 activeFilterCO->currentIndex() != 2;
573         bool const show_inactive =
574                 activeFilterCO->currentIndex() != 1;
575
576         int size = indices.size();
577         QString const matchstring = filter_ ? filter_->text() : QString();
578         for (int i = 0; i < size; i++) {
579                 QModelIndex index = indices[i];
580                 bool matches = index.data().toString().contains(
581                                         matchstring, Qt::CaseInsensitive);
582                 TocItem const & item =
583                         gui_view_.tocModels().currentItem(current_type_, index);
584                 matches &= (show_active && item.isOutput()) || (show_inactive && !item.isOutput());
585                 tocTV->setRowHidden(index.row(), index.parent(), !matches);
586         }
587         // recursively unhide parents of unhidden children
588         for (int i = size - 1; i >= 0; i--) {
589                 QModelIndex index = indices[i];
590                 if (!tocTV->isRowHidden(index.row(), index.parent())
591                     && index.parent() != QModelIndex())
592                         tocTV->setRowHidden(index.parent().row(),
593                                             index.parent().parent(), false);
594         }
595 }
596
597
598 bool TocWidget::isAncestor(QModelIndex const & ancestor,
599                            QModelIndex const & descendant) const
600 {
601         QModelIndex mi = descendant;
602         while (true) {
603                 if (ancestor == mi.parent())
604                         return true;
605                 if (mi == QModelIndex())
606                         return false;
607                 mi = mi.parent();
608         }
609         return false;
610 }
611
612
613 QModelIndex TocWidget::getAncestor(QModelIndex const & descendant) const
614 {
615         QModelIndex mi = descendant;
616         while (true) {
617                 if (mi.parent() == QModelIndex())
618                         return mi;
619                 mi = mi.parent();
620         }
621         return mi;
622 }
623
624
625 int TocWidget::getItemDepth(QModelIndex const & index_in)
626 {
627         QModelIndex index = index_in;
628         int depth = 1;
629         while (index.parent().isValid())
630         {
631                 index = index.parent();
632                 ++depth;
633         }
634         return depth;
635 }
636
637
638 void TocWidget::collapseAllOthers(int const depth)
639 {
640         if (!tocTV->model())
641                 return;
642
643         QModelIndexList indices = getIndices();
644
645         int size = indices.size();
646         // collapse parents which are not in our ancestry line
647         // and which exceed the requested depth
648         for (int i = size - 1; i >= 0; i--) {
649                 QModelIndex index = indices[i];
650                 if (tocTV->isExpanded(index)
651                     && !isAncestor(index, tocTV->currentIndex())
652                     && depth < getItemDepth(index))
653                         tocTV->collapse(index);
654         }
655 }
656
657
658 static QString decodeType(QString const & str)
659 {
660         QString type = str;
661         if (type.contains("tableofcontents"))
662                 type = "tableofcontents";
663         else if (type.contains("lstlistoflistings"))
664                 type = "listing";
665         else if (type.contains("floatlist")) {
666                 if (type.contains("\"figure"))
667                         type = "figure";
668                 else if (type.contains("\"table"))
669                         type = "table";
670                 else if (type.contains("\"algorithm"))
671                         type = "algorithm";
672         }
673         return type;
674 }
675
676
677 void TocWidget::init(QString const & str)
678 {
679         int new_index;
680         if (str.isEmpty())
681                 new_index = typeCO->findData(current_type_);
682         else
683                 new_index = typeCO->findData(decodeType(str));
684
685         // If everything else fails, settle on the table of contents which is
686         // guaranteed to exist.
687         if (new_index == -1) {
688                 current_type_ = "tableofcontents";
689                 new_index = typeCO->findData(current_type_);
690         } else {
691                 current_type_ = typeCO->itemData(new_index).toString();
692         }
693
694         typeCO->blockSignals(true);
695         typeCO->setCurrentIndex(new_index);
696         typeCO->blockSignals(false);
697         updateViewNow();
698         if (persistent_)
699                 setTreeDepth(depth_);
700 }
701
702 } // namespace frontend
703 } // namespace lyx
704
705 #include "moc_TocWidget.cpp"