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