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