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