]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiViewSource.cpp
Make string-widget combination more l7n friendly
[lyx.git] / src / frontends / qt / GuiViewSource.cpp
1 /**
2  * \file GuiViewSource.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 Bo Peng
8  * \author Abdelrazak Younes
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "GuiViewSource.h"
16
17 #include "GuiApplication.h"
18 #include "LaTeXHighlighter.h"
19 #include "qt_helpers.h"
20
21 #include "BufferParams.h"
22 #include "BufferView.h"
23 #include "Cursor.h"
24 #include "Format.h"
25 #include "GuiView.h"
26 #include "FuncRequest.h"
27 #include "LyX.h"
28 #include "TexRow.h"
29
30 #include "support/debug.h"
31 #include "support/lassert.h"
32 #include "support/docstream.h"
33 #include "support/docstring_list.h"
34 #include "support/gettext.h"
35
36 #include <QBoxLayout>
37 #include <QScrollBar>
38 #include <QSettings>
39 #include <QTextCursor>
40 #include <QTextDocument>
41 #include <QTimer>
42 #include <QVariant>
43
44 using namespace std;
45
46 namespace lyx {
47 namespace frontend {
48
49 ViewSourceWidget::ViewSourceWidget(QWidget * parent)
50         :       QWidget(parent),
51                 document_(new QTextDocument(this)),
52                 highlighter_(new LaTeXHighlighter(document_))
53 {
54         setupUi(this);
55
56         connect(contentsCO, SIGNAL(activated(int)),
57                 this, SLOT(contentsChanged()));
58         connect(autoUpdateCB, SIGNAL(toggled(bool)),
59                 updatePB, SLOT(setDisabled(bool)));
60         connect(autoUpdateCB, SIGNAL(toggled(bool)),
61                 this, SLOT(contentsChanged()));
62         connect(masterPerspectiveCB, SIGNAL(toggled(bool)),
63                 this, SLOT(contentsChanged()));
64         connect(updatePB, SIGNAL(clicked()),
65                 this, SIGNAL(needUpdate()));
66         connect(outputFormatCO, SIGNAL(activated(int)),
67                 this, SLOT(setViewFormat(int)));
68
69         // setting a document at this point trigger an assertion in Qt
70         // so we disable the signals here:
71         document_->blockSignals(true);
72         viewSourceTV->setDocument(document_);
73         // reset selections
74         setText();
75         document_->blockSignals(false);
76         viewSourceTV->setReadOnly(true);
77         ///dialog_->viewSourceTV->setAcceptRichText(false);
78         // this is personal. I think source code should be in fixed-size font
79         viewSourceTV->setFont(guiApp->typewriterSystemFont());
80         // again, personal taste
81         viewSourceTV->setWordWrapMode(QTextOption::NoWrap);
82
83         // catch double click events
84         viewSourceTV->viewport()->installEventFilter(this);
85 }
86
87
88 void ViewSourceWidget::getContent(BufferView const & view,
89                         Buffer::OutputWhat output, docstring & str, string const & format,
90                         bool master)
91 {
92         // get the *top* level paragraphs that contain the cursor,
93         // or the selected text
94         pit_type par_begin;
95         pit_type par_end;
96
97         if (!view.cursor().selection()) {
98                 par_begin = view.cursor().bottom().pit();
99                 par_end = par_begin;
100         } else {
101                 par_begin = view.cursor().selectionBegin().bottom().pit();
102                 par_end = view.cursor().selectionEnd().bottom().pit();
103         }
104         if (par_begin > par_end)
105                 swap(par_begin, par_end);
106         odocstringstream ostr;
107         texrow_ = view.buffer()
108                 .getSourceCode(ostr, format, par_begin, par_end + 1, output, master);
109         //ensure that the last line can always be selected in its full width
110         str = ostr.str() + "\n";
111 }
112
113
114 bool ViewSourceWidget::setText(QString const & qstr)
115 {
116         bool const changed = document_->toPlainText() != qstr;
117         viewSourceTV->setExtraSelections(QList<QTextEdit::ExtraSelection>());
118         if (changed)
119                 document_->setPlainText(qstr);
120         return changed;
121 }
122
123
124 void ViewSourceWidget::contentsChanged()
125 {
126         if (autoUpdateCB->isChecked())
127                 Q_EMIT needUpdate();
128 }
129
130
131 void ViewSourceWidget::setViewFormat(int const index)
132 {
133         outputFormatCO->setCurrentIndex(index);
134         string format = fromqstr(outputFormatCO->itemData(index).toString());
135         if (view_format_ != format) {
136                 view_format_ = format;
137                 Q_EMIT needUpdate();
138         }
139 }
140
141
142 int ViewSourceWidget::updateDelay() const
143 {
144         const int long_delay = 400;
145         const int short_delay = 60;
146         // a shorter delay if just the current paragraph is shown
147         return (contentsCO->currentIndex() == 0) ? short_delay : long_delay;
148 }
149
150
151 void GuiViewSource::scheduleUpdate()
152 {
153         update_timer_->start(widget_->updateDelay());
154 }
155
156
157 void GuiViewSource::scheduleUpdateNow()
158 {
159         update_timer_->start(0);
160 }
161
162
163 void GuiViewSource::realUpdateView()
164 {
165         widget_->updateView(bufferview());
166         updateTitle();
167 }
168
169
170 void ViewSourceWidget::updateView(BufferView const * bv)
171 {
172         if (!bv) {
173                 setText();
174                 setEnabled(false);
175                 return;
176         }
177
178         setEnabled(true);
179
180         // we will try to get that much space around the cursor
181         int const v_margin = 3;
182         int const h_margin = 10;
183         // we will try to preserve this
184         int const h_scroll = viewSourceTV->horizontalScrollBar()->value();
185
186         Buffer::OutputWhat output = Buffer::CurrentParagraph;
187         if (contentsCO->currentIndex() == 1)
188                 output = Buffer::FullSource;
189         else if (contentsCO->currentIndex() == 2)
190                 output = Buffer::OnlyPreamble;
191         else if (contentsCO->currentIndex() == 3)
192                 output = Buffer::OnlyBody;
193
194         docstring content;
195         getContent(*bv, output, content, view_format_,
196                    masterPerspectiveCB->isChecked());
197         QString old = document_->toPlainText();
198         QString qcontent = toqstr(content);
199         if (guiApp->currentView()->develMode()) {
200                 // output tex<->row correspondences in the source panel if the "-dbg latex"
201                 // option is given.
202                 if (texrow_ && lyx::lyxerr.debugging(Debug::OUTFILE)) {
203                         QStringList list = qcontent.split(QChar('\n'));
204                         docstring_list dlist;
205                         for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it)
206                                 dlist.push_back(from_utf8(fromqstr(*it)));
207                         texrow_->prepend(dlist);
208                         qcontent.clear();
209                         for (docstring_list::iterator it = dlist.begin(); it != dlist.end(); ++it)
210                                 qcontent += toqstr(*it) + '\n';
211                 }
212         }
213
214         // prevent gotoCursor()
215         QSignalBlocker blocker(viewSourceTV);
216         bool const changed = setText(qcontent);
217
218         if (changed && !texrow_) {
219                 // position-to-row is unavailable
220                 // we jump to the first modification
221                 int length = min(old.length(), qcontent.length());
222                 int pos = 0;
223                 for (; pos < length && old.at(pos) == qcontent.at(pos); ++pos) {}
224                 QTextCursor c = QTextCursor(viewSourceTV->document());
225                 //get some space below the cursor
226                 c.setPosition(pos);
227                 c.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor,v_margin);
228                 viewSourceTV->setTextCursor(c);
229                 //get some space on the right of the cursor
230                 viewSourceTV->horizontalScrollBar()->setValue(h_scroll);
231                 c.setPosition(pos);
232                 const int block = c.blockNumber();
233                 for (int i = h_margin; i && block == c.blockNumber(); --i) {
234                         c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor);
235                 }
236                 c.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor);
237                 viewSourceTV->setTextCursor(c);
238                 //back to the position
239                 c.setPosition(pos);
240                 //c.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor,1);
241                 viewSourceTV->setTextCursor(c);
242
243         } else if (texrow_) {
244                 // Use the available position-to-row conversion to highlight
245                 // the current selection in the source
246                 std::pair<int,int> rows = texrow_->rowFromCursor(bv->cursor());
247                 int const beg_row = rows.first;
248                 int const end_row = rows.second;
249
250                 QTextCursor c = QTextCursor(viewSourceTV->document());
251
252                 c.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor,
253                                            beg_row - 1);
254                 const int beg_sel = c.position();
255                 //get some space above the cursor
256                 c.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor,
257                                            v_margin);
258                 viewSourceTV->setTextCursor(c);
259                 c.setPosition(beg_sel, QTextCursor::MoveAnchor);
260
261                 c.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor,
262                                            end_row - beg_row +1);
263                 const int end_sel = c.position();
264                 //get some space below the cursor
265                 c.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor,
266                                            v_margin - 1);
267                 viewSourceTV->setTextCursor(c);
268                 c.setPosition(end_sel, QTextCursor::KeepAnchor);
269
270                 viewSourceTV->setTextCursor(c);
271
272                 //the real highlighting is done with an ExtraSelection
273                 QTextCharFormat format;
274                 {
275                 // We create a new color with the slightly altered lightness
276                 // of the Base and the hue and saturation of the Highlight brush
277                 QPalette const palette = viewSourceTV->palette();
278                 QBrush extraselbrush = palette.base();
279                 int const extrasellightness =
280                                 (palette.text().color().black() > palette.window().color().black()) ?
281                                         extraselbrush.color().darker(107).lightness()// light mode
282                                       : extraselbrush.color().darker().lightness();// dark mode
283                 QColor const highlight = palette.highlight().color().toHsl();
284                 QColor const extraselcol = QColor::fromHsl(highlight.hue(),
285                                                            highlight.hslSaturation(),
286                                                            extrasellightness);
287                 extraselbrush.setColor(extraselcol);
288                 format.setBackground(extraselbrush);
289                 }
290                 format.setProperty(QTextFormat::FullWidthSelection, true);
291                 QTextEdit::ExtraSelection sel;
292                 sel.format = format;
293                 sel.cursor = c;
294                 viewSourceTV->setExtraSelections(
295                         QList<QTextEdit::ExtraSelection>() << sel);
296
297                 //clean up
298                 c.clearSelection();
299                 viewSourceTV->setTextCursor(c);
300                 viewSourceTV->horizontalScrollBar()->setValue(h_scroll);
301         } // else if (texrow)
302 }
303
304
305 docstring ViewSourceWidget::currentFormatName(BufferView const * bv) const
306 {
307         // Compute the actual format used
308         string const format = !bv ? ""
309                 : flavor2format(bv->buffer().params().getOutputFlavor(view_format_));
310         Format const * f = theFormats().getFormat(format.empty() ? view_format_ : format);
311         return f ? f->prettyname() : from_utf8(view_format_);
312 }
313
314
315 bool ViewSourceWidget::eventFilter(QObject * obj, QEvent * ev)
316 {
317         // this event filter is installed on the viewport of the QTextView
318         if (obj == viewSourceTV->viewport() &&
319             ev->type() == QEvent::MouseButtonDblClick) {
320                 goToCursor();
321                 return true;
322         }
323         return false;
324 }
325
326
327 void ViewSourceWidget::goToCursor() const
328 {
329         if (!texrow_)
330                 return;
331         int row = viewSourceTV->textCursor().blockNumber() + 1;
332         dispatch(texrow_->goToFuncFromRow(row));
333 }
334
335
336
337 void ViewSourceWidget::updateDefaultFormat(BufferView const & bv)
338 {
339         QSignalBlocker blocker(outputFormatCO);
340         outputFormatCO->clear();
341         outputFormatCO->addItem(qt_("Default"),
342                                 QVariant(QString("default")));
343
344         int index = 0;
345         for (string const & fmt_name : bv.buffer().params().backends()) {
346                 Format const * fmt = theFormats().getFormat(fmt_name);
347                 if (!fmt) {
348                         LYXERR0("Can't find format for backend " << fmt_name << "!");
349                         continue;
350                 }
351                 QString const pretty = toqstr(translateIfPossible(fmt->prettyname()));
352                 outputFormatCO->addItem(pretty, QVariant(toqstr(fmt_name)));
353                 if (fmt_name == view_format_)
354                         index = outputFormatCO->count() - 1;
355         }
356         setViewFormat(index);
357 }
358
359
360 void ViewSourceWidget::resizeEvent (QResizeEvent * event)
361 {
362         QSize const & formSize = gridLayout->sizeHint();
363         // minimize the size of the part that contains the buttons
364         if (width() * formSize.height() < height() * formSize.width()) {
365                 layout_->setDirection(QBoxLayout::TopToBottom);
366         } else {
367                 layout_->setDirection(QBoxLayout::LeftToRight);
368         }
369         QWidget::resizeEvent(event);
370 }
371
372
373 void ViewSourceWidget::saveSession(QSettings & settings, QString const & session_key) const
374 {
375         settings.setValue(session_key + "/output", toqstr(view_format_));
376         settings.setValue(session_key + "/contents", contentsCO->currentIndex());
377         settings.setValue(session_key + "/autoupdate", autoUpdateCB->isChecked());
378         settings.setValue(session_key + "/masterview",
379                                           masterPerspectiveCB->isChecked());
380 }
381
382
383 void ViewSourceWidget::restoreSession(QString const & session_key)
384 {
385         QSettings settings;
386         view_format_ = fromqstr(settings.value(session_key + "/output", 0)
387                                 .toString());
388         contentsCO->setCurrentIndex(settings
389                                                                 .value(session_key + "/contents", 0)
390                                                                 .toInt());
391         masterPerspectiveCB->setChecked(settings
392                                                                         .value(session_key + "/masterview", false)
393                                                                         .toBool());
394         bool const checked = settings
395                 .value(session_key + "/autoupdate", true)
396                 .toBool();
397         autoUpdateCB->setChecked(checked);
398         if (checked)
399                 Q_EMIT needUpdate();
400 }
401
402
403 GuiViewSource::GuiViewSource(GuiView & parent,
404                 Qt::DockWidgetArea area, Qt::WindowFlags flags)
405         : DockView(parent, "view-source", qt_("Code Preview"), area, flags),
406           widget_(new ViewSourceWidget(this)),
407           update_timer_(new QTimer(this))
408 {
409         setWidget(widget_);
410
411         // setting the update timer
412         update_timer_->setSingleShot(true);
413         connect(update_timer_, SIGNAL(timeout()),
414                 this, SLOT(realUpdateView()));
415
416         connect(widget_, SIGNAL(needUpdate()), this, SLOT(scheduleUpdateNow()));
417 }
418
419
420 void GuiViewSource::onBufferViewChanged()
421 {
422         widget_->setText();
423         widget_->setEnabled(static_cast<bool>(bufferview()));
424 }
425
426
427 void GuiViewSource::updateView()
428 {
429         if (widget_->autoUpdateCB->isChecked()) {
430                 widget_->setEnabled(static_cast<bool>(bufferview()));
431                 scheduleUpdate();
432         }
433         widget_->masterPerspectiveCB->setEnabled(buffer().parent());
434         updateTitle();
435 }
436
437
438 void GuiViewSource::enableView(bool enable)
439 {
440         widget_->setEnabled(static_cast<bool>(bufferview()));
441         if (bufferview())
442                 widget_->updateDefaultFormat(*bufferview());
443         if (!enable)
444                 // In the opposite case, updateView() will be called anyway.
445                 widget_->contentsChanged();
446 }
447
448
449 bool GuiViewSource::initialiseParams(string const & /*source*/)
450 {
451         updateTitle();
452         return true;
453 }
454
455
456 void GuiViewSource::updateTitle()
457 {
458         docstring const format = widget_->currentFormatName(bufferview());
459         QString const title = format.empty() ? qt_("Code Preview")
460                 : qt_("%1[[preview format name]] Preview")
461                   .arg(toqstr(translateIfPossible(format)));
462         setTitle(title);
463         setWindowTitle(title);
464 }
465
466
467 void GuiViewSource::saveSession(QSettings & settings) const
468 {
469         Dialog::saveSession(settings);
470         widget_->saveSession(settings, sessionKey());
471 }
472
473
474 void GuiViewSource::restoreSession()
475 {
476         DockView::restoreSession();
477         widget_->restoreSession(sessionKey());
478 }
479
480
481 } // namespace frontend
482 } // namespace lyx
483
484 #include "moc_GuiViewSource.cpp"