3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
7 * \author Abdelrazak Younes
9 * Full author contact details are available in file CREDITS.
14 #include "TocWidget.h"
16 #include "GuiApplication.h"
18 #include "qt_helpers.h"
20 #include "FancyLineEdit.h"
23 #include "BufferView.h"
25 #include "CutAndPaste.h"
26 #include "FuncRequest.h"
27 #include "FuncStatus.h"
30 #include "TocBackend.h"
32 #include "insets/InsetCommand.h"
33 #include "insets/InsetRef.h"
35 #include "support/debug.h"
36 #include "support/lassert.h"
38 #include <QHeaderView>
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))
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")));
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)));
80 tocTV->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
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);
90 // Only one item selected at a time.
91 tocTV->setSelectionMode(QAbstractItemView::SingleSelection);
93 // The toc types combo won't change its model.
94 typeCO->setModel(gui_view_.tocModels().nameModel());
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_);
104 // Make sure the buttons are disabled when first shown without a loaded
106 enableControls(false);
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()));
121 // setting the update timer
122 timer_->setSingleShot(true);
123 connect(timer_, SIGNAL(timeout()), this, SLOT(finishUpdateView()));
129 void TocWidget::showContextMenu(const QPoint & pos)
131 std::string name = "context-toc-" + fromqstr(current_type_);
132 QMenu * menu = guiApp->menus().menu(toqstr(name), gui_view_);
135 menu->exec(mapToGlobal(pos));
139 Inset * TocWidget::itemInset() const
141 QModelIndex const & index = tocTV->currentIndex();
142 TocItem const & item =
143 gui_view_.tocModels().currentItem(current_type_, index);
144 DocIterator const & dit = item.dit();
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();
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();
165 bool TocWidget::getStatus(Cursor & cur, FuncRequest const & cmd,
166 FuncStatus & status) const
168 Inset * inset = itemInset();
169 FuncRequest tmpcmd(cmd);
171 QModelIndex const & index = tocTV->currentIndex();
172 TocItem const & item =
173 gui_view_.tocModels().currentItem(current_type_, index);
175 switch (cmd.action())
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());
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());
191 return inset->getStatus(cur, label_copy, status);
197 return inset->getStatus(cur, tmpcmd, status);
204 void TocWidget::doDispatch(Cursor & cur, FuncRequest const & cmd,
207 Inset * inset = itemInset();
209 QModelIndex const & index = tocTV->currentIndex();
210 TocItem const & item =
211 gui_view_.tocModels().currentItem(current_type_, index);
213 // Start an undo group.
214 cur.beginUndoGroup();
216 switch (cmd.action())
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);
231 dr.forceBufferUpdate();
235 case LFUN_SECTION_SELECT:
236 dispatch(item.action());
238 // necessary to get the selection drawn.
239 cur.buffer()->changed(true);
240 gui_view_.setFocus();
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());
247 inset->dispatch(cur, label_copy);
251 case LFUN_OUTLINE_UP:
252 case LFUN_OUTLINE_DOWN:
253 case LFUN_OUTLINE_IN:
254 case LFUN_OUTLINE_OUT:
255 outline(cmd.action());
259 FuncRequest tmpcmd(cmd);
261 inset->dispatch(cur, tmpcmd);
268 void TocWidget::on_tocTV_activated(QModelIndex const & index)
274 void TocWidget::on_tocTV_pressed(QModelIndex const & index)
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) {
281 gui_view_.setFocus();
282 gui_view_.activateWindow();
287 void TocWidget::on_tocTV_doubleClicked(QModelIndex const &)
289 keep_expanded_ = true;
293 void TocWidget::goTo(QModelIndex const & index)
295 LYXERR(Debug::GUI, "goto " << index.row()
296 << ", " << index.column());
298 sendDispatch(gui_view_.tocModels().goTo(current_type_, index));
302 void TocWidget::on_updateTB_clicked()
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();
311 void TocWidget::on_sortCB_stateChanged(int state)
313 gui_view_.tocModels().sort(current_type_, state == Qt::Checked);
318 void TocWidget::on_persistentCB_stateChanged(int state)
320 persistent_ = state == Qt::Checked;
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. */
331 static int indexDepth(QModelIndex const & index, int depth = -1)
334 return index.parent() == QModelIndex()
335 ? depth : indexDepth(index.parent(), depth);
339 void TocWidget::on_depthSL_valueChanged(int depth)
344 gui_view_.setFocus();
348 void TocWidget::setTreeDepth(int depth, bool const maintain_current)
354 if (maintain_current)
355 collapseAllOthers(depth);
358 tocTV->collapseAll();
360 tocTV->expandToDepth(depth - 1);
365 void TocWidget::on_typeCO_activated(int index)
369 current_type_ = typeCO->itemData(index).toString();
371 if (typeCO->hasFocus())
372 gui_view_.setFocus();
376 void TocWidget::outline(FuncCode func_code)
378 QModelIndexList const & list = tocTV->selectionModel()->selectedIndexes();
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();
386 enableControls(false);
388 sendDispatch(FuncRequest(func_code));
389 enableControls(true);
390 gui_view_.setFocus();
394 void TocWidget::sendDispatch(FuncRequest fr)
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
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.
409 if (gui_view_.currentWorkArea() == doc_wa
410 && gui_view_.hasVisibleWorkArea(old_wa)
412 gui_view_.setCurrentWorkArea(old_wa);
414 gui_view_.message(dr.message());
418 void TocWidget::on_moveUpTB_clicked()
420 outline(LFUN_OUTLINE_UP);
424 void TocWidget::on_moveDownTB_clicked()
426 outline(LFUN_OUTLINE_DOWN);
430 void TocWidget::on_moveInTB_clicked()
432 outline(LFUN_OUTLINE_IN);
436 void TocWidget::on_moveOutTB_clicked()
438 outline(LFUN_OUTLINE_OUT);
442 void TocWidget::select(QModelIndex const & index)
444 if (!index.isValid()) {
445 LYXERR(Debug::GUI, "TocWidget::select(): QModelIndex is invalid!");
449 tocTV->scrollTo(index);
450 tocTV->clearSelection();
451 tocTV->setCurrentIndex(index);
455 void TocWidget::enableControls(bool enable)
457 updateTB->setEnabled(enable);
462 moveUpTB->setEnabled(enable);
463 moveDownTB->setEnabled(enable);
464 moveInTB->setEnabled(enable);
465 moveOutTB->setEnabled(enable);
469 void TocWidget::updateView()
471 if (!gui_view_.documentBufferView()) {
472 tocTV->setModel(nullptr);
473 depthSL->setMaximum(0);
474 depthSL->setValue(0);
479 bool const is_sortable = isSortable();
480 sortCB->setEnabled(is_sortable);
481 bool focus = tocTV->hasFocus();
482 tocTV->setEnabled(false);
483 tocTV->setUpdatesEnabled(false);
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);
491 setTreeDepth(depth_);
494 sortCB->blockSignals(true);
495 sortCB->setChecked(is_sortable
496 && gui_view_.tocModels().isSorted(current_type_));
497 sortCB->blockSignals(false);
499 persistentCB->setEnabled(canNavigate());
501 bool controls_enabled = toc_model && toc_model->rowCount() > 0
502 && !gui_view_.documentBufferView()->buffer().isReadonly();
503 enableControls(controls_enabled);
505 depthSL->setMaximum(gui_view_.tocModels().depth(current_type_));
506 depthSL->setValue(depth_);
507 tocTV->setEnabled(true);
508 tocTV->setUpdatesEnabled(true);
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_) {
522 void TocWidget::updateViewNow()
529 void TocWidget::finishUpdateView()
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.
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));
549 QModelIndexList TocWidget::getIndices()
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));
557 // deprecated in Qt 5.15.
558 Qt::MatchFlags(Qt::MatchRegExp|Qt::MatchRecursive));
564 void TocWidget::filterContents()
569 QModelIndexList indices = getIndices();
571 bool const show_active =
572 activeFilterCO->currentIndex() != 2;
573 bool const show_inactive =
574 activeFilterCO->currentIndex() != 1;
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);
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);
598 bool TocWidget::isAncestor(QModelIndex const & ancestor,
599 QModelIndex const & descendant) const
601 QModelIndex mi = descendant;
603 if (ancestor == mi.parent())
605 if (mi == QModelIndex())
613 QModelIndex TocWidget::getAncestor(QModelIndex const & descendant) const
615 QModelIndex mi = descendant;
617 if (mi.parent() == QModelIndex())
625 int TocWidget::getItemDepth(QModelIndex const & index_in)
627 QModelIndex index = index_in;
629 while (index.parent().isValid())
631 index = index.parent();
638 void TocWidget::collapseAllOthers(int const depth)
643 QModelIndexList indices = getIndices();
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);
658 static QString decodeType(QString const & str)
661 if (type.contains("tableofcontents"))
662 type = "tableofcontents";
663 else if (type.contains("lstlistoflistings"))
665 else if (type.contains("floatlist")) {
666 if (type.contains("\"figure"))
668 else if (type.contains("\"table"))
670 else if (type.contains("\"algorithm"))
677 void TocWidget::init(QString const & str)
681 new_index = typeCO->findData(current_type_);
683 new_index = typeCO->findData(decodeType(str));
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_);
691 current_type_ = typeCO->itemData(new_index).toString();
694 typeCO->blockSignals(true);
695 typeCO->setCurrentIndex(new_index);
696 typeCO->blockSignals(false);
699 setTreeDepth(depth_);
702 } // namespace frontend
705 #include "moc_TocWidget.cpp"