1 /**
2  * \file GuiWorkArea.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 Abdelrazak Younes
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11 
12 #include <config.h>
13 
14 #include "GuiWorkArea.h"
15 #include "GuiWorkArea_Private.h"
16 
17 #include "ColorCache.h"
18 #include "FontLoader.h"
19 #include "GuiApplication.h"
20 #include "GuiCompleter.h"
21 #include "GuiKeySymbol.h"
22 #include "GuiPainter.h"
23 #include "GuiView.h"
24 #include "Menus.h"
25 #include "qt_helpers.h"
26 
27 #include "Buffer.h"
28 #include "BufferList.h"
29 #include "BufferParams.h"
30 #include "BufferView.h"
31 #include "CoordCache.h"
32 #include "Cursor.h"
33 #include "Font.h"
34 #include "FuncRequest.h"
35 #include "KeySymbol.h"
36 #include "Language.h"
37 #include "LyX.h"
38 #include "LyXRC.h"
39 #include "LyXVC.h"
40 #include "Text.h"
41 #include "TextMetrics.h"
42 #include "Undo.h"
43 #include "version.h"
44 
45 #include "graphics/GraphicsImage.h"
46 #include "graphics/GraphicsLoader.h"
47 
48 #include "support/convert.h"
49 #include "support/debug.h"
50 #include "support/lassert.h"
51 #include "support/TempFile.h"
52 
53 #include "frontends/Application.h"
54 #include "frontends/FontMetrics.h"
55 #include "frontends/WorkAreaManager.h"
56 
57 #include <QContextMenuEvent>
58 #if (QT_VERSION < 0x050000)
59 #include <QInputContext>
60 #endif
61 #include <QDrag>
62 #include <QHelpEvent>
63 #ifdef Q_OS_MAC
64 #include <QProxyStyle>
65 #endif
66 #include <QMainWindow>
67 #include <QMimeData>
68 #include <QMenu>
69 #include <QPainter>
70 #include <QPalette>
71 #include <QScrollBar>
72 #include <QStyleOption>
73 #include <QStylePainter>
74 #include <QTimer>
75 #include <QToolButton>
76 #include <QToolTip>
77 #include <QMenuBar>
78 
79 #include <cmath>
80 #include <iostream>
81 
82 int const TabIndicatorWidth = 3;
83 
84 #undef KeyPress
85 #undef NoModifier
86 
87 using namespace std;
88 using namespace lyx::support;
89 
90 namespace lyx {
91 
92 
93 /// return the LyX mouse button state from Qt's
q_button_state(Qt::MouseButton button)94 static mouse_button::state q_button_state(Qt::MouseButton button)
95 {
96 	mouse_button::state b = mouse_button::none;
97 	switch (button) {
98 		case Qt::LeftButton:
99 			b = mouse_button::button1;
100 			break;
101 		case Qt::MidButton:
102 			b = mouse_button::button2;
103 			break;
104 		case Qt::RightButton:
105 			b = mouse_button::button3;
106 			break;
107 		default:
108 			break;
109 	}
110 	return b;
111 }
112 
113 
114 /// return the LyX mouse button state from Qt's
q_motion_state(Qt::MouseButtons state)115 mouse_button::state q_motion_state(Qt::MouseButtons state)
116 {
117 	mouse_button::state b = mouse_button::none;
118 	if (state & Qt::LeftButton)
119 		b |= mouse_button::button1;
120 	if (state & Qt::MidButton)
121 		b |= mouse_button::button2;
122 	if (state & Qt::RightButton)
123 		b |= mouse_button::button3;
124 	return b;
125 }
126 
127 
128 namespace frontend {
129 
130 class CaretWidget {
131 public:
CaretWidget()132 	CaretWidget() : rtl_(false), l_shape_(false), completable_(false),
133 		x_(0), caret_width_(0)
134 	{}
135 
136 	/* Draw the caret. Parameter \c horiz_offset is not 0 when there
137 	 * has been horizontal scrolling in current row
138 	 */
draw(QPainter & painter,int horiz_offset)139 	void draw(QPainter & painter, int horiz_offset)
140 	{
141 		if (!rect_.isValid())
142 			return;
143 
144 		int const x = x_ - horiz_offset;
145 		int const y = rect_.top();
146 		int const l = x_ - rect_.left();
147 		int const r = rect_.right() - x_;
148 		int const bot = rect_.bottom();
149 
150 		// draw vertical line
151 		painter.fillRect(x, y, caret_width_, rect_.height(), color_);
152 
153 		// draw RTL/LTR indication
154 		painter.setPen(color_);
155 		if (l_shape_) {
156 			if (rtl_)
157 				painter.drawLine(x, bot, x - l, bot);
158 			else
159 				painter.drawLine(x, bot, x + caret_width_ + r, bot);
160 		}
161 
162 		// draw completion triangle
163 		if (completable_) {
164 			int m = y + rect_.height() / 2;
165 			int d = TabIndicatorWidth - 1;
166 			if (rtl_) {
167 				painter.drawLine(x - 1, m - d, x - 1 - d, m);
168 				painter.drawLine(x - 1, m + d, x - 1 - d, m);
169 			} else {
170 				painter.drawLine(x + caret_width_, m - d, x + caret_width_ + d, m);
171 				painter.drawLine(x + caret_width_, m + d, x + caret_width_ + d, m);
172 			}
173 		}
174 	}
175 
update(int x,int y,int h,bool l_shape,bool rtl,bool completable)176 	void update(int x, int y, int h, bool l_shape,
177 		bool rtl, bool completable)
178 	{
179 		color_ = guiApp->colorCache().get(Color_cursor);
180 		l_shape_ = l_shape;
181 		rtl_ = rtl;
182 		completable_ = completable;
183 		x_ = x;
184 
185 		// extension to left and right
186 		int l = 0;
187 		int r = 0;
188 
189 		// RTL/LTR indication
190 		if (l_shape_) {
191 			if (rtl)
192 				l += h / 3;
193 			else
194 				r += h / 3;
195 		}
196 
197 		// completion triangle
198 		if (completable_) {
199 			if (rtl)
200 				l = max(l, TabIndicatorWidth);
201 			else
202 				r = max(r, TabIndicatorWidth);
203 		}
204 
205 		//FIXME: LyXRC::cursor_width should be caret_width
206 		caret_width_ = lyxrc.cursor_width
207 			? lyxrc.cursor_width
208 			: 1 + int((lyxrc.currentZoom + 50) / 200.0);
209 
210 		// compute overall rectangle
211 		rect_ = QRect(x - l, y, caret_width_ + r + l, h);
212 	}
213 
rect()214 	QRect const & rect() { return rect_; }
215 
216 private:
217 	/// caret is in RTL or LTR text
218 	bool rtl_;
219 	/// indication for RTL or LTR
220 	bool l_shape_;
221 	/// triangle to show that a completion is available
222 	bool completable_;
223 	///
224 	QColor color_;
225 	/// rectangle, possibly with l_shape and completion triangle
226 	QRect rect_;
227 	/// x position (were the vertical line is drawn)
228 	int x_;
229 	/// the width of the vertical blinking bar
230 	int caret_width_;
231 };
232 
233 
234 // This is a 'heartbeat' generating synthetic mouse move events when the
235 // cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s
SyntheticMouseEvent()236 SyntheticMouseEvent::SyntheticMouseEvent()
237 	: timeout(200), restart_timeout(true)
238 {}
239 
240 
Private(GuiWorkArea * parent)241 GuiWorkArea::Private::Private(GuiWorkArea * parent)
242 : p(parent), buffer_view_(0), lyx_view_(0), caret_(0),
243   caret_visible_(false), need_resize_(false), preedit_lines_(1),
244   last_pixel_ratio_(1.0), completer_(new GuiCompleter(p, p)),
245   dialog_mode_(false), shell_escape_(false), read_only_(false),
246   clean_(true), externally_modified_(false)
247 {
248 /* Qt on macOS and Wayland does not respect the
249  * Qt::WA_OpaquePaintEvent attribute and resets the widget backing
250  * store at each update. Therefore, we use our own backing store in
251  * these two cases. */
252 #if QT_VERSION >= 0x050000
253 	use_backingstore_ = guiApp->platformName() == "cocoa"
254 		|| guiApp->platformName().contains("wayland");
255 #else
256 #  ifdef Q_OS_MAC
257 	use_backingstore_ = true;
258 #  else
259 	use_backingstore_ = false;
260 #  endif
261 #endif
262 
263 	int const time = QApplication::cursorFlashTime() / 2;
264 	if (time > 0) {
265 		caret_timeout_.setInterval(time);
266 		caret_timeout_.start();
267 	} else {
268 		// let's initialize this just to be safe
269 		caret_timeout_.setInterval(500);
270 	}
271 }
272 
273 
~Private()274 GuiWorkArea::Private::~Private()
275 {
276 	// If something is wrong with the buffer, we can ignore it safely
277 	try {
278 		buffer_view_->buffer().workAreaManager().remove(p);
279 	} catch(...) {}
280 	delete buffer_view_;
281 	delete caret_;
282 	// Completer has a QObject parent and is thus automatically destroyed.
283 	// See #4758.
284 	// delete completer_;
285 }
286 
287 
GuiWorkArea(QWidget *)288 GuiWorkArea::GuiWorkArea(QWidget * /* w */)
289 : d(new Private(this))
290 {
291 	new CompressorProxy(this); // not a leak
292 }
293 
294 
GuiWorkArea(Buffer & buffer,GuiView & gv)295 GuiWorkArea::GuiWorkArea(Buffer & buffer, GuiView & gv)
296 : d(new Private(this))
297 {
298 	new CompressorProxy(this); // not a leak
299 	setGuiView(gv);
300 	buffer.params().display_pixel_ratio = theGuiApp()->pixelRatio();
301 	setBuffer(buffer);
302 	init();
303 }
304 
305 
pixelRatio() const306 double GuiWorkArea::pixelRatio() const
307 {
308 #if QT_VERSION >= 0x050000
309 	return qt_scale_factor * devicePixelRatio();
310 #else
311 	return 1.0;
312 #endif
313 }
314 
315 
init()316 void GuiWorkArea::init()
317 {
318 	// Setup the signals
319 	connect(&d->caret_timeout_, SIGNAL(timeout()),
320 		this, SLOT(toggleCaret()));
321 
322 	// This connection is closed at the same time as this is destroyed.
323 	d->synthetic_mouse_event_.timeout.timeout.connect([this](){
324 			generateSyntheticMouseEvent();
325 		});
326 
327 	d->resetScreen();
328 	// With Qt4.5 a mouse event will happen before the first paint event
329 	// so make sure that the buffer view has an up to date metrics.
330 	d->buffer_view_->resize(viewport()->width(), viewport()->height());
331 	d->caret_ = new frontend::CaretWidget();
332 
333 	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
334 	setAcceptDrops(true);
335 	setMouseTracking(true);
336 	setMinimumSize(100, 70);
337 	setFrameStyle(QFrame::NoFrame);
338 	updateWindowTitle();
339 
340 	d->updateCursorShape();
341 
342 	// we paint our own background
343 	viewport()->setAttribute(Qt::WA_OpaquePaintEvent);
344 
345 	setFocusPolicy(Qt::StrongFocus);
346 
347 	LYXERR(Debug::GUI, "viewport width: " << viewport()->width()
348 		<< "  viewport height: " << viewport()->height());
349 
350 	// Enables input methods for asian languages.
351 	// Must be set when creating custom text editing widgets.
352 	setAttribute(Qt::WA_InputMethodEnabled, true);
353 }
354 
355 
~GuiWorkArea()356 GuiWorkArea::~GuiWorkArea()
357 {
358 	delete d;
359 }
360 
361 
updateCursorShape()362 void GuiWorkArea::Private::updateCursorShape()
363 {
364 	bool const clickable = buffer_view_ && buffer_view_->clickableInset();
365 	p->viewport()->setCursor(clickable ? Qt::PointingHandCursor
366 	                                   : Qt::IBeamCursor);
367 }
368 
369 
setGuiView(GuiView & gv)370 void GuiWorkArea::setGuiView(GuiView & gv)
371 {
372 	d->lyx_view_ = &gv;
373 }
374 
375 
setBuffer(Buffer & buffer)376 void GuiWorkArea::setBuffer(Buffer & buffer)
377 {
378 	delete d->buffer_view_;
379 	d->buffer_view_ = new BufferView(buffer);
380 	buffer.workAreaManager().add(this);
381 
382 	// HACK: Prevents an additional redraw when the scrollbar pops up
383 	// which regularily happens on documents with more than one page.
384 	// The policy  should be set to "Qt::ScrollBarAsNeeded" soon.
385 	// Since we have no geometry information yet, we assume that
386 	// a document needs a scrollbar if there is more then four
387 	// paragraph in the outermost text.
388 	if (buffer.text().paragraphs().size() > 4)
389 		setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
390 	QTimer::singleShot(50, this, SLOT(fixVerticalScrollBar()));
391 	Q_EMIT bufferViewChanged();
392 }
393 
394 
fixVerticalScrollBar()395 void GuiWorkArea::fixVerticalScrollBar()
396 {
397 	if (!isFullScreen())
398 		setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
399 }
400 
401 
close()402 void GuiWorkArea::close()
403 {
404 	d->lyx_view_->removeWorkArea(this);
405 }
406 
407 
setFullScreen(bool full_screen)408 void GuiWorkArea::setFullScreen(bool full_screen)
409 {
410 	d->buffer_view_->setFullScreen(full_screen);
411 	setFrameStyle(QFrame::NoFrame);
412 	if (full_screen) {
413 		setFrameStyle(QFrame::NoFrame);
414 		if (lyxrc.full_screen_scrollbar)
415 			setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
416 	} else
417 		setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
418 }
419 
420 
bufferView()421 BufferView & GuiWorkArea::bufferView()
422 {
423 	return *d->buffer_view_;
424 }
425 
426 
bufferView() const427 BufferView const & GuiWorkArea::bufferView() const
428 {
429 	return *d->buffer_view_;
430 }
431 
432 
stopBlinkingCaret()433 void GuiWorkArea::stopBlinkingCaret()
434 {
435 	d->caret_timeout_.stop();
436 	d->hideCaret();
437 }
438 
439 
startBlinkingCaret()440 void GuiWorkArea::startBlinkingCaret()
441 {
442 	// do not show the cursor if the view is busy
443 	if (view().busy())
444 		return;
445 
446 	Point p;
447 	int h = 0;
448 	d->buffer_view_->caretPosAndHeight(p, h);
449 	// Don't start blinking if the cursor isn't on screen.
450 	if (!d->buffer_view_->cursorInView(p, h))
451 		return;
452 
453 	d->showCaret();
454 
455 	//we're not supposed to cache this value.
456 	int const time = QApplication::cursorFlashTime() / 2;
457 	if (time <= 0)
458 		return;
459 	d->caret_timeout_.setInterval(time);
460 	d->caret_timeout_.start();
461 }
462 
463 
toggleCaret()464 void GuiWorkArea::toggleCaret()
465 {
466 	if (d->caret_visible_)
467 		d->hideCaret();
468 	else
469 		d->showCaret();
470 }
471 
472 
scheduleRedraw(bool update_metrics)473 void GuiWorkArea::scheduleRedraw(bool update_metrics)
474 {
475 	if (!isVisible())
476 		// No need to redraw in this case.
477 		return;
478 
479 	// No need to do anything if this is the current view. The BufferView
480 	// metrics are already up to date.
481 	if (update_metrics || d->lyx_view_ != guiApp->currentView()
482 		|| d->lyx_view_->currentWorkArea() != this) {
483 		// FIXME: it would be nice to optimize for the off-screen case.
484 		d->buffer_view_->cursor().fixIfBroken();
485 		d->buffer_view_->updateMetrics();
486 		d->buffer_view_->cursor().fixIfBroken();
487 	}
488 
489 	// update caret position, because otherwise it has to wait until
490 	// the blinking interval is over
491 	d->updateCaretGeometry();
492 
493 	LYXERR(Debug::WORKAREA, "WorkArea::redraw screen");
494 	viewport()->update();
495 
496 	/// FIXME: is this still true now that paintEvent does the actual painting?
497 	/// \warning: scrollbar updating *must* be done after the BufferView is drawn
498 	/// because \c BufferView::updateScrollbar() is called in \c BufferView::draw().
499 	d->updateScrollbar();
500 	d->lyx_view_->updateStatusBar();
501 
502 	if (lyxerr.debugging(Debug::WORKAREA))
503 		d->buffer_view_->coordCache().dump();
504 
505 	updateWindowTitle();
506 
507 	d->updateCursorShape();
508 }
509 
510 
511 // Keep in sync with GuiWorkArea::processKeySym below
queryKeySym(KeySymbol const & key,KeyModifier mod) const512 bool GuiWorkArea::queryKeySym(KeySymbol const & key, KeyModifier mod) const
513 {
514 	return guiApp->queryKeySym(key, mod);
515 }
516 
517 
518 // Keep in sync with GuiWorkArea::queryKeySym above
processKeySym(KeySymbol const & key,KeyModifier mod)519 void GuiWorkArea::processKeySym(KeySymbol const & key, KeyModifier mod)
520 {
521 	if (d->lyx_view_->isFullScreen() && d->lyx_view_->menuBar()->isVisible()
522 		&& lyxrc.full_screen_menubar) {
523 		// FIXME HACK: we should not have to do this here. See related comment
524 		// in GuiView::event() (QEvent::ShortcutOverride)
525 		d->lyx_view_->menuBar()->hide();
526 	}
527 
528 	// In order to avoid bad surprise in the middle of an operation,
529 	// we better stop the blinking caret...
530 	// the caret gets restarted in GuiView::restartCaret()
531 	stopBlinkingCaret();
532 	guiApp->processKeySym(key, mod);
533 }
534 
535 
dispatch(FuncRequest const & cmd)536 void GuiWorkArea::Private::dispatch(FuncRequest const & cmd)
537 {
538 	// Handle drag&drop
539 	if (cmd.action() == LFUN_FILE_OPEN) {
540 		DispatchResult dr;
541 		lyx_view_->dispatch(cmd, dr);
542 		return;
543 	}
544 
545 	bool const notJustMovingTheMouse =
546 		cmd.action() != LFUN_MOUSE_MOTION || cmd.button() != mouse_button::none;
547 
548 	// In order to avoid bad surprise in the middle of an operation, we better stop
549 	// the blinking caret.
550 	if (notJustMovingTheMouse)
551 		p->stopBlinkingCaret();
552 
553 	buffer_view_->mouseEventDispatch(cmd);
554 
555 	// Skip these when selecting
556 	// FIXME: let GuiView take care of those.
557 	if (cmd.action() != LFUN_MOUSE_MOTION) {
558 		completer_->updateVisibility(false, false);
559 		lyx_view_->updateDialogs();
560 		lyx_view_->updateStatusBar();
561 	}
562 
563 	// GUI tweaks except with mouse motion with no button pressed.
564 	if (notJustMovingTheMouse) {
565 		// Slight hack: this is only called currently when we
566 		// clicked somewhere, so we force through the display
567 		// of the new status here.
568 		// FIXME: let GuiView take care of those.
569 		lyx_view_->clearMessage();
570 
571 		// Show the caret immediately after any operation
572 		p->startBlinkingCaret();
573 	}
574 
575 	updateCursorShape();
576 }
577 
578 
resizeBufferView()579 void GuiWorkArea::Private::resizeBufferView()
580 {
581 	// WARNING: Please don't put any code that will trigger a repaint here!
582 	// We are already inside a paint event.
583 	p->stopBlinkingCaret();
584 	// Warn our container (GuiView).
585 	p->busy(true);
586 
587 	Point point;
588 	int h = 0;
589 	buffer_view_->caretPosAndHeight(point, h);
590 	bool const caret_in_view = buffer_view_->cursorInView(point, h);
591 	buffer_view_->resize(p->viewport()->width(), p->viewport()->height());
592 	if (caret_in_view)
593 		buffer_view_->scrollToCursor();
594 	updateCaretGeometry();
595 
596 	// Update scrollbars which might have changed due different
597 	// BufferView dimension. This is especially important when the
598 	// BufferView goes from zero-size to the real-size for the first time,
599 	// as the scrollbar paramters are then set for the first time.
600 	updateScrollbar();
601 
602 	need_resize_ = false;
603 	p->busy(false);
604 	// Eventually, restart the caret after the resize event.
605 	// We might be resizing even if the focus is on another widget so we only
606 	// restart the caret if we have the focus.
607 	if (p->hasFocus())
608 		QTimer::singleShot(50, p, SLOT(startBlinkingCaret()));
609 }
610 
611 
updateCaretGeometry()612 void GuiWorkArea::Private::updateCaretGeometry()
613 {
614 	Point point;
615 	int h = 0;
616 	buffer_view_->caretPosAndHeight(point, h);
617 	if (!buffer_view_->cursorInView(point, h))
618 		return;
619 
620 	// RTL or not RTL
621 	bool l_shape = false;
622 	Font const & realfont = buffer_view_->cursor().real_current_font;
623 	BufferParams const & bp = buffer_view_->buffer().params();
624 	bool const samelang = realfont.language() == bp.language;
625 	bool const isrtl = realfont.isVisibleRightToLeft();
626 
627 	if (!samelang || isrtl != bp.language->rightToLeft())
628 		l_shape = true;
629 
630 	// The ERT language hack needs fixing up
631 	if (realfont.language() == latex_language)
632 		l_shape = false;
633 
634 	// show caret on screen
635 	Cursor & cur = buffer_view_->cursor();
636 	bool completable = cur.inset().showCompletionCursor()
637 		&& completer_->completionAvailable()
638 		&& !completer_->popupVisible()
639 		&& !completer_->inlineVisible();
640 	caret_visible_ = true;
641 
642 	caret_->update(point.x_, point.y_, h, l_shape, isrtl, completable);
643 }
644 
645 
showCaret()646 void GuiWorkArea::Private::showCaret()
647 {
648 	if (caret_visible_)
649 		return;
650 
651 	updateCaretGeometry();
652 	p->viewport()->update();
653 }
654 
655 
hideCaret()656 void GuiWorkArea::Private::hideCaret()
657 {
658 	if (!caret_visible_)
659 		return;
660 
661 	caret_visible_ = false;
662 	//if (!qApp->focusWidget())
663 		p->viewport()->update();
664 }
665 
666 
updateScrollbar()667 void GuiWorkArea::Private::updateScrollbar()
668 {
669 	// Prevent setRange() and setSliderPosition from causing recursive calls via
670 	// the signal valueChanged. (#10311)
671 	QObject::disconnect(p->verticalScrollBar(), SIGNAL(valueChanged(int)),
672 	                    p, SLOT(scrollTo(int)));
673 	ScrollbarParameters const & scroll_ = buffer_view_->scrollbarParameters();
674 	p->verticalScrollBar()->setRange(scroll_.min, scroll_.max);
675 	p->verticalScrollBar()->setPageStep(scroll_.page_step);
676 	p->verticalScrollBar()->setSingleStep(scroll_.single_step);
677 	p->verticalScrollBar()->setSliderPosition(0);
678 	// Connect to the vertical scroll bar
679 	QObject::connect(p->verticalScrollBar(), SIGNAL(valueChanged(int)),
680 	                 p, SLOT(scrollTo(int)));
681 }
682 
683 
scrollTo(int value)684 void GuiWorkArea::scrollTo(int value)
685 {
686 	stopBlinkingCaret();
687 	d->buffer_view_->scrollDocView(value, true);
688 
689 	if (lyxrc.cursor_follows_scrollbar) {
690 		d->buffer_view_->setCursorFromScrollbar();
691 		// FIXME: let GuiView take care of those.
692 		d->lyx_view_->updateLayoutList();
693 	}
694 	// Show the caret immediately after any operation.
695 	startBlinkingCaret();
696 	// FIXME QT5
697 #ifdef Q_WS_X11
698 	QApplication::syncX();
699 #endif
700 }
701 
702 
event(QEvent * e)703 bool GuiWorkArea::event(QEvent * e)
704 {
705 	switch (e->type()) {
706 	case QEvent::ToolTip: {
707 		QHelpEvent * helpEvent = static_cast<QHelpEvent *>(e);
708 		if (lyxrc.use_tooltip) {
709 			QPoint pos = helpEvent->pos();
710 			if (pos.x() < viewport()->width()) {
711 				QString s = toqstr(d->buffer_view_->toolTip(pos.x(), pos.y()));
712 				QToolTip::showText(helpEvent->globalPos(), formatToolTip(s,35));
713 			}
714 			else
715 				QToolTip::hideText();
716 		}
717 		// Don't forget to accept the event!
718 		e->accept();
719 		return true;
720 	}
721 
722 	case QEvent::ShortcutOverride:
723 		// keyPressEvent is ShortcutOverride-aware and only accepts the event in
724 		// this case
725 		keyPressEvent(static_cast<QKeyEvent *>(e));
726 		return e->isAccepted();
727 
728 	case QEvent::KeyPress: {
729 		// We catch this event in order to catch the Tab or Shift+Tab key press
730 		// which are otherwise reserved to focus switching between controls
731 		// within a dialog.
732 		QKeyEvent * ke = static_cast<QKeyEvent*>(e);
733 		if ((ke->key() == Qt::Key_Tab && ke->modifiers() == Qt::NoModifier)
734 			|| (ke->key() == Qt::Key_Backtab && (
735 				ke->modifiers() == Qt::ShiftModifier
736 				|| ke->modifiers() == Qt::NoModifier))) {
737 			keyPressEvent(ke);
738 			return true;
739 		}
740 		return QAbstractScrollArea::event(e);
741 	}
742 
743 	default:
744 		return QAbstractScrollArea::event(e);
745 	}
746 	return false;
747 }
748 
749 
contextMenuEvent(QContextMenuEvent * e)750 void GuiWorkArea::contextMenuEvent(QContextMenuEvent * e)
751 {
752 	string name;
753 	if (e->reason() == QContextMenuEvent::Mouse)
754 		// the menu name is set on mouse press
755 		name = d->context_menu_name_;
756 	else {
757 		QPoint pos = e->pos();
758 		Cursor const & cur = d->buffer_view_->cursor();
759 		if (e->reason() == QContextMenuEvent::Keyboard && cur.inTexted()) {
760 			// Do not access the context menu of math right in front of before
761 			// the cursor. This does not work when the cursor is in text.
762 			Inset * inset = cur.paragraph().getInset(cur.pos());
763 			if (inset && inset->asInsetMath())
764 				--pos.rx();
765 			else if (cur.pos() > 0) {
766 				Inset * inset = cur.paragraph().getInset(cur.pos() - 1);
767 				if (inset)
768 					++pos.rx();
769 			}
770 		}
771 		name = d->buffer_view_->contextMenu(pos.x(), pos.y());
772 	}
773 
774 	if (name.empty()) {
775 		e->accept();
776 		return;
777 	}
778 	// always show mnemonics when the keyboard is used to show the context menu
779 	// FIXME: This should be fixed in Qt itself
780 	bool const keyboard = (e->reason() == QContextMenuEvent::Keyboard);
781 	QMenu * menu = guiApp->menus().menu(toqstr(name), *d->lyx_view_, keyboard);
782 	if (!menu) {
783 		e->accept();
784 		return;
785 	}
786 	// Position the menu to the right.
787 	// FIXME: menu position should be different for RTL text.
788 	menu->exec(e->globalPos());
789 	e->accept();
790 }
791 
792 
focusInEvent(QFocusEvent * e)793 void GuiWorkArea::focusInEvent(QFocusEvent * e)
794 {
795 	LYXERR(Debug::DEBUG, "GuiWorkArea::focusInEvent(): " << this << endl);
796 	if (d->lyx_view_->currentWorkArea() != this) {
797 		d->lyx_view_->setCurrentWorkArea(this);
798 		d->lyx_view_->currentWorkArea()->bufferView().buffer().updateBuffer();
799 	}
800 
801 	startBlinkingCaret();
802 	QAbstractScrollArea::focusInEvent(e);
803 }
804 
805 
focusOutEvent(QFocusEvent * e)806 void GuiWorkArea::focusOutEvent(QFocusEvent * e)
807 {
808 	LYXERR(Debug::DEBUG, "GuiWorkArea::focusOutEvent(): " << this << endl);
809 	stopBlinkingCaret();
810 	QAbstractScrollArea::focusOutEvent(e);
811 }
812 
813 
mousePressEvent(QMouseEvent * e)814 void GuiWorkArea::mousePressEvent(QMouseEvent * e)
815 {
816 	if (d->dc_event_.active && d->dc_event_ == *e) {
817 		d->dc_event_.active = false;
818 		FuncRequest cmd(LFUN_MOUSE_TRIPLE, e->x(), e->y(),
819 			q_button_state(e->button()), q_key_state(e->modifiers()));
820 		d->dispatch(cmd);
821 		e->accept();
822 		return;
823 	}
824 
825 #if (QT_VERSION < 0x050000) && !defined(__HAIKU__)
826 	inputContext()->reset();
827 #endif
828 
829 	FuncRequest const cmd(LFUN_MOUSE_PRESS, e->x(), e->y(),
830 			q_button_state(e->button()), q_key_state(e->modifiers()));
831 	d->dispatch(cmd);
832 
833 	// Save the context menu on mouse press, because also the mouse
834 	// cursor is set on mouse press. Afterwards, we can either release
835 	// the mousebutton somewhere else, or the cursor might have moved
836 	// due to the DEPM. We need to do this after the mouse has been
837 	// set in dispatch(), because the selection state might change.
838 	if (e->button() == Qt::RightButton)
839 		d->context_menu_name_ = d->buffer_view_->contextMenu(e->x(), e->y());
840 
841 	e->accept();
842 }
843 
844 
mouseReleaseEvent(QMouseEvent * e)845 void GuiWorkArea::mouseReleaseEvent(QMouseEvent * e)
846 {
847 	if (d->synthetic_mouse_event_.timeout.running())
848 		d->synthetic_mouse_event_.timeout.stop();
849 
850 	FuncRequest const cmd(LFUN_MOUSE_RELEASE, e->x(), e->y(),
851 			q_button_state(e->button()), q_key_state(e->modifiers()));
852 	d->dispatch(cmd);
853 	e->accept();
854 }
855 
856 
mouseMoveEvent(QMouseEvent * e)857 void GuiWorkArea::mouseMoveEvent(QMouseEvent * e)
858 {
859 	// we kill the triple click if we move
860 	doubleClickTimeout();
861 	FuncRequest cmd(LFUN_MOUSE_MOTION, e->x(), e->y(),
862 			q_motion_state(e->buttons()), q_key_state(e->modifiers()));
863 
864 	e->accept();
865 
866 	// If we're above or below the work area...
867 	if ((e->y() <= 20 || e->y() >= viewport()->height() - 20)
868 			&& e->buttons() == mouse_button::button1) {
869 		// Make sure only a synthetic event can cause a page scroll,
870 		// so they come at a steady rate:
871 		if (e->y() <= 20)
872 			// _Force_ a scroll up:
873 			cmd.set_y(e->y() - 21);
874 		else
875 			cmd.set_y(e->y() + 21);
876 		// Store the event, to be handled when the timeout expires.
877 		d->synthetic_mouse_event_.cmd = cmd;
878 
879 		if (d->synthetic_mouse_event_.timeout.running()) {
880 			// Discard the event. Note that it _may_ be handled
881 			// when the timeout expires if
882 			// synthetic_mouse_event_.cmd has not been overwritten.
883 			// Ie, when the timeout expires, we handle the
884 			// most recent event but discard all others that
885 			// occurred after the one used to start the timeout
886 			// in the first place.
887 			return;
888 		}
889 
890 		d->synthetic_mouse_event_.restart_timeout = true;
891 		d->synthetic_mouse_event_.timeout.start();
892 		// Fall through to handle this event...
893 
894 	} else if (d->synthetic_mouse_event_.timeout.running()) {
895 		// Store the event, to be possibly handled when the timeout
896 		// expires.
897 		// Once the timeout has expired, normal control is returned
898 		// to mouseMoveEvent (restart_timeout = false).
899 		// This results in a much smoother 'feel' when moving the
900 		// mouse back into the work area.
901 		d->synthetic_mouse_event_.cmd = cmd;
902 		d->synthetic_mouse_event_.restart_timeout = false;
903 		return;
904 	}
905 	d->dispatch(cmd);
906 }
907 
908 
wheelEvent(QWheelEvent * ev)909 void GuiWorkArea::wheelEvent(QWheelEvent * ev)
910 {
911 	// Wheel rotation by one notch results in a delta() of 120 (see
912 	// documentation of QWheelEvent)
913 	// But first we have to ignore horizontal scroll events.
914 #if QT_VERSION < 0x050000
915 	if (ev->orientation() == Qt::Horizontal) {
916 		ev->accept();
917 		return;
918 	}
919 	double const delta = ev->delta() / 120.0;
920 #else
921 	QPoint const aDelta = ev->angleDelta();
922 	// skip horizontal wheel event
923 	if (abs(aDelta.x()) > abs(aDelta.y())) {
924 		ev->accept();
925 		return;
926 	}
927 	double const delta = aDelta.y() / 120.0;
928 #endif
929 
930 	bool zoom = false;
931 	switch (lyxrc.scroll_wheel_zoom) {
932 	case LyXRC::SCROLL_WHEEL_ZOOM_CTRL:
933 		zoom = ev->modifiers() & Qt::ControlModifier;
934 		zoom &= !(ev->modifiers() & (Qt::ShiftModifier | Qt::AltModifier));
935 		break;
936 	case LyXRC::SCROLL_WHEEL_ZOOM_SHIFT:
937 		zoom = ev->modifiers() & Qt::ShiftModifier;
938 		zoom &= !(ev->modifiers() & (Qt::ControlModifier | Qt::AltModifier));
939 		break;
940 	case LyXRC::SCROLL_WHEEL_ZOOM_ALT:
941 		zoom = ev->modifiers() & Qt::AltModifier;
942 		zoom &= !(ev->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier));
943 		break;
944 	case LyXRC::SCROLL_WHEEL_ZOOM_OFF:
945 		break;
946 	}
947 	if (zoom) {
948 		docstring arg = convert<docstring>(int(5 * delta));
949 		lyx::dispatch(FuncRequest(LFUN_BUFFER_ZOOM_IN, arg));
950 		return;
951 	}
952 
953 	// Take into account the desktop wide settings.
954 	int const lines = qApp->wheelScrollLines();
955 	int const page_step = verticalScrollBar()->pageStep();
956 	// Test if the wheel mouse is set to one screen at a time.
957 	// This is according to
958 	// https://doc.qt.io/qt-5/qapplication.html#wheelScrollLines-prop
959 	int scroll_value =
960 		min(lines * verticalScrollBar()->singleStep(), page_step);
961 
962 	// Take into account the rotation and the user preferences.
963 	scroll_value = int(scroll_value * delta * lyxrc.mouse_wheel_speed);
964 	LYXERR(Debug::SCROLLING, "wheelScrollLines = " << lines
965 			<< " delta = " << delta << " scroll_value = " << scroll_value
966 			<< " page_step = " << page_step);
967 	// Now scroll.
968 	verticalScrollBar()->setValue(verticalScrollBar()->value() - scroll_value);
969 
970 	ev->accept();
971 }
972 
973 
generateSyntheticMouseEvent()974 void GuiWorkArea::generateSyntheticMouseEvent()
975 {
976 	int const e_y = d->synthetic_mouse_event_.cmd.y();
977 	int const wh = d->buffer_view_->workHeight();
978 	bool const up = e_y < 0;
979 	bool const down = e_y > wh;
980 
981 	// Set things off to generate the _next_ 'pseudo' event.
982 	int step = 50;
983 	if (d->synthetic_mouse_event_.restart_timeout) {
984 		// This is some magic formulae to determine the speed
985 		// of scrolling related to the position of the mouse.
986 		int time = 200;
987 		if (up || down) {
988 			int dist = up ? -e_y : e_y - wh;
989 			time = max(min(200, 250000 / (dist * dist)), 1) ;
990 
991 			if (time < 40) {
992 				step = 80000 / (time * time);
993 				time = 40;
994 			}
995 		}
996 		d->synthetic_mouse_event_.timeout.setTimeout(time);
997 		d->synthetic_mouse_event_.timeout.start();
998 	}
999 
1000 	// Can we scroll further ?
1001 	int const value = verticalScrollBar()->value();
1002 	if (value == verticalScrollBar()->maximum()
1003 		  || value == verticalScrollBar()->minimum()) {
1004 		d->synthetic_mouse_event_.timeout.stop();
1005 		return;
1006 	}
1007 
1008 	// Scroll
1009 	if (step <= 2 * wh) {
1010 		d->buffer_view_->scroll(up ? -step : step);
1011 		d->buffer_view_->updateMetrics();
1012 	} else {
1013 		d->buffer_view_->scrollDocView(value + (up ? -step : step), false);
1014 	}
1015 
1016 	// In which paragraph do we have to set the cursor ?
1017 	Cursor & cur = d->buffer_view_->cursor();
1018 	// FIXME: we don't know how to handle math.
1019 	Text * text = cur.text();
1020 	if (!text)
1021 		return;
1022 	TextMetrics const & tm = d->buffer_view_->textMetrics(text);
1023 
1024 	// Quit gracefully if there are no metrics, since otherwise next
1025 	// line would crash (bug #10324).
1026 	// This situation seems related to a (not yet understood) timing problem.
1027 	if (tm.empty())
1028 		return;
1029 
1030 	pair<pit_type, const ParagraphMetrics *> pp = up ? tm.first() : tm.last();
1031 	ParagraphMetrics const & pm = *pp.second;
1032 	pit_type const pit = pp.first;
1033 
1034 	if (pm.rows().empty())
1035 		return;
1036 
1037 	// Find the row at which we set the cursor.
1038 	RowList::const_iterator rit = pm.rows().begin();
1039 	RowList::const_iterator rlast = pm.rows().end();
1040 	int yy = pm.position() - pm.ascent();
1041 	for (--rlast; rit != rlast; ++rit) {
1042 		int h = rit->height();
1043 		if ((up && yy + h > 0)
1044 			  || (!up && yy + h > wh - defaultRowHeight()))
1045 			break;
1046 		yy += h;
1047 	}
1048 
1049 	// Find the position of the cursor
1050 	bool bound;
1051 	int x = d->synthetic_mouse_event_.cmd.x();
1052 	pos_type const pos = tm.getPosNearX(*rit, x, bound);
1053 
1054 	// Set the cursor
1055 	cur.pit() = pit;
1056 	cur.pos() = pos;
1057 	cur.boundary(bound);
1058 
1059 	d->buffer_view_->buffer().changed(false);
1060 	return;
1061 }
1062 
1063 
1064 // CompressorProxy adapted from Kuba Ober https://stackoverflow.com/a/21006207
CompressorProxy(GuiWorkArea * wa)1065 CompressorProxy::CompressorProxy(GuiWorkArea * wa) : QObject(wa), flag_(false)
1066 {
1067 	qRegisterMetaType<KeySymbol>("KeySymbol");
1068 	qRegisterMetaType<KeyModifier>("KeyModifier");
1069 	connect(wa, SIGNAL(compressKeySym(KeySymbol, KeyModifier, bool)),
1070 		this, SLOT(slot(KeySymbol, KeyModifier, bool)),
1071 	        Qt::QueuedConnection);
1072 	connect(this, SIGNAL(signal(KeySymbol, KeyModifier)),
1073 		wa, SLOT(processKeySym(KeySymbol, KeyModifier)));
1074 }
1075 
1076 
emitCheck(bool isAutoRepeat)1077 bool CompressorProxy::emitCheck(bool isAutoRepeat)
1078 {
1079 	flag_ = true;
1080 	if (isAutoRepeat)
1081 		QCoreApplication::sendPostedEvents(this, QEvent::MetaCall); // recurse
1082 	bool result = flag_;
1083 	flag_ = false;
1084 	return result;
1085 }
1086 
1087 
slot(KeySymbol sym,KeyModifier mod,bool isAutoRepeat)1088 void CompressorProxy::slot(KeySymbol sym, KeyModifier mod, bool isAutoRepeat)
1089 {
1090 	if (emitCheck(isAutoRepeat))
1091 		Q_EMIT signal(sym, mod);
1092 	else
1093 		LYXERR(Debug::KEY, "system is busy: autoRepeat key event ignored");
1094 }
1095 
1096 
keyPressEvent(QKeyEvent * ev)1097 void GuiWorkArea::keyPressEvent(QKeyEvent * ev)
1098 {
1099 	// this is also called for ShortcutOverride events. In this case, one must
1100 	// not act but simply accept the event explicitly.
1101 	bool const act = (ev->type() != QEvent::ShortcutOverride);
1102 
1103 	// Do not process here some keys if dialog_mode_ is set
1104 	bool const for_dialog_mode = d->dialog_mode_
1105 		&& (ev->modifiers() == Qt::NoModifier
1106 		    || ev->modifiers() == Qt::ShiftModifier)
1107 		&& (ev->key() == Qt::Key_Escape
1108 		    || ev->key() == Qt::Key_Enter
1109 		    || ev->key() == Qt::Key_Return);
1110 	// also do not use autoRepeat to input shortcuts
1111 	bool const autoRepeat = ev->isAutoRepeat();
1112 
1113 	if (for_dialog_mode || (!act && autoRepeat)) {
1114 		ev->ignore();
1115 		return;
1116 	}
1117 
1118 	// intercept some keys if completion popup is visible
1119 	if (d->completer_->popupVisible()) {
1120 		switch (ev->key()) {
1121 		case Qt::Key_Enter:
1122 		case Qt::Key_Return:
1123 			if (act)
1124 				d->completer_->activate();
1125 			ev->accept();
1126 			return;
1127 		}
1128 	}
1129 
1130 	KeyModifier const m = q_key_state(ev->modifiers());
1131 
1132 	if (act && lyxerr.debugging(Debug::KEY)) {
1133 		std::string str;
1134 		if (m & ShiftModifier)
1135 			str += "Shift-";
1136 		if (m & ControlModifier)
1137 			str += "Control-";
1138 		if (m & AltModifier)
1139 			str += "Alt-";
1140 		if (m & MetaModifier)
1141 			str += "Meta-";
1142 		LYXERR(Debug::KEY, " count: " << ev->count() << " text: " << ev->text()
1143 		       << " isAutoRepeat: " << ev->isAutoRepeat() << " key: " << ev->key()
1144 		       << " keyState: " << str);
1145 	}
1146 
1147 	KeySymbol sym;
1148 	setKeySymbol(&sym, ev);
1149 	if (sym.isOK()) {
1150 		if (act) {
1151 			Q_EMIT compressKeySym(sym, m, autoRepeat);
1152 			ev->accept();
1153 		} else
1154 			// here, !autoRepeat, as determined at the beginning
1155 			ev->setAccepted(queryKeySym(sym, m));
1156 	} else {
1157 		ev->ignore();
1158 	}
1159 }
1160 
1161 
doubleClickTimeout()1162 void GuiWorkArea::doubleClickTimeout()
1163 {
1164 	d->dc_event_.active = false;
1165 }
1166 
1167 
mouseDoubleClickEvent(QMouseEvent * ev)1168 void GuiWorkArea::mouseDoubleClickEvent(QMouseEvent * ev)
1169 {
1170 	d->dc_event_ = DoubleClick(ev);
1171 	QTimer::singleShot(QApplication::doubleClickInterval(), this,
1172 			SLOT(doubleClickTimeout()));
1173 	FuncRequest cmd(LFUN_MOUSE_DOUBLE, ev->x(), ev->y(),
1174 			q_button_state(ev->button()), q_key_state(ev->modifiers()));
1175 	d->dispatch(cmd);
1176 	ev->accept();
1177 }
1178 
1179 
resizeEvent(QResizeEvent * ev)1180 void GuiWorkArea::resizeEvent(QResizeEvent * ev)
1181 {
1182 	QAbstractScrollArea::resizeEvent(ev);
1183 	d->need_resize_ = true;
1184 	ev->accept();
1185 }
1186 
1187 
paintPreeditText(GuiPainter & pain)1188 void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
1189 {
1190 	if (preedit_string_.empty())
1191 		return;
1192 
1193 	// FIXME: shall we use real_current_font here? (see #10478)
1194 	FontInfo const font = buffer_view_->cursor().getFont().fontInfo();
1195 	FontMetrics const & fm = theFontMetrics(font);
1196 	int const height = fm.maxHeight();
1197 	int cur_x = caret_->rect().left();
1198 	int cur_y = caret_->rect().bottom();
1199 
1200 	// get attributes of input method cursor.
1201 	// cursor_pos : cursor position in preedit string.
1202 	size_t cursor_pos = 0;
1203 	bool cursor_is_visible = false;
1204 	for (auto const & attr : preedit_attr_) {
1205 		if (attr.type == QInputMethodEvent::Cursor) {
1206 			cursor_pos = attr.start;
1207 			cursor_is_visible = attr.length != 0;
1208 			break;
1209 		}
1210 	}
1211 
1212 	size_t const preedit_length = preedit_string_.length();
1213 
1214 	// get position of selection in input method.
1215 	// FIXME: isn't there a way to do this simplier?
1216 	// rStart : cursor position in selected string in IM.
1217 	size_t rStart = 0;
1218 	// rLength : selected string length in IM.
1219 	size_t rLength = 0;
1220 	if (cursor_pos < preedit_length) {
1221 		for (auto const & attr : preedit_attr_) {
1222 			if (attr.type == QInputMethodEvent::TextFormat) {
1223 				if (attr.start <= int(cursor_pos)
1224 					&& int(cursor_pos) < attr.start + attr.length) {
1225 						rStart = attr.start;
1226 						rLength = attr.length;
1227 						if (!cursor_is_visible)
1228 							cursor_pos += rLength;
1229 						break;
1230 				}
1231 			}
1232 		}
1233 	}
1234 	else {
1235 		rStart = cursor_pos;
1236 		rLength = 0;
1237 	}
1238 
1239 	int const right_margin = buffer_view_->rightMargin();
1240 	Painter::preedit_style ps;
1241 	// Most often there would be only one line:
1242 	preedit_lines_ = 1;
1243 	for (size_t pos = 0; pos != preedit_length; ++pos) {
1244 		char_type const typed_char = preedit_string_[pos];
1245 		// reset preedit string style
1246 		ps = Painter::preedit_default;
1247 
1248 		// if we reached the right extremity of the screen, go to next line.
1249 		if (cur_x + fm.width(typed_char) > p->viewport()->width() - right_margin) {
1250 			cur_x = right_margin;
1251 			cur_y += height + 1;
1252 			++preedit_lines_;
1253 		}
1254 		// preedit strings are displayed with dashed underline
1255 		// and partial strings are displayed white on black indicating
1256 		// that we are in selecting mode in the input method.
1257 		// FIXME: rLength == preedit_length is not a changing condition
1258 		// FIXME: should be put out of the loop.
1259 		if (pos >= rStart
1260 			&& pos < rStart + rLength
1261 			&& !(cursor_pos < rLength && rLength == preedit_length))
1262 			ps = Painter::preedit_selecting;
1263 
1264 		if (pos == cursor_pos
1265 			&& (cursor_pos < rLength && rLength == preedit_length))
1266 			ps = Painter::preedit_cursor;
1267 
1268 		// draw one character and update cur_x.
1269 		cur_x += pain.preeditText(cur_x, cur_y, typed_char, font, ps);
1270 	}
1271 }
1272 
1273 
resetScreen()1274 void GuiWorkArea::Private::resetScreen()
1275 {
1276 	if (use_backingstore_) {
1277 		int const pr = p->pixelRatio();
1278 		screen_ = QImage(static_cast<int>(pr * p->viewport()->width()),
1279 		                 static_cast<int>(pr * p->viewport()->height()),
1280 		                 QImage::Format_ARGB32_Premultiplied);
1281 #  if QT_VERSION >= 0x050000
1282 		screen_.setDevicePixelRatio(pr);
1283 #  endif
1284 	}
1285 }
1286 
1287 
screenDevice()1288 QPaintDevice * GuiWorkArea::Private::screenDevice()
1289 {
1290 	if (use_backingstore_)
1291 		return &screen_;
1292 	else
1293 		return p->viewport();
1294 }
1295 
1296 
updateScreen(QRectF const & rc)1297 void GuiWorkArea::Private::updateScreen(QRectF const & rc)
1298 {
1299 	if (use_backingstore_) {
1300 		QPainter qpain(p->viewport());
1301 		double const pr = p->pixelRatio();
1302 		QRectF const rcs = QRectF(rc.x() * pr, rc.y() * pr,
1303 		                          rc.width() * pr, rc.height() * pr);
1304 		qpain.drawImage(rc, screen_, rcs);
1305 	}
1306 }
1307 
1308 
paintEvent(QPaintEvent * ev)1309 void GuiWorkArea::paintEvent(QPaintEvent * ev)
1310 {
1311 	// Do not trigger the painting machinery if we are not ready (see
1312 	// bug #10989). The second test triggers when in the middle of a
1313 	// dispatch operation.
1314 	if (view().busy() || d->buffer_view_->buffer().undo().activeUndoGroup()) {
1315 		// Since macOS has turned the screen black at this point, our
1316 		// backing store has to be copied to screen (this is a no-op
1317 		// except on macOS).
1318 		d->updateScreen(ev->rect());
1319 		// Ignore this paint event, but request a new one for later.
1320 		viewport()->update(ev->rect());
1321 		ev->accept();
1322 		return;
1323 	}
1324 
1325 	// LYXERR(Debug::PAINTING, "paintEvent begin: x: " << rc.x()
1326 	//	<< " y: " << rc.y() << " w: " << rc.width() << " h: " << rc.height());
1327 
1328 	if (d->need_resize_ || pixelRatio() != d->last_pixel_ratio_) {
1329 		d->resetScreen();
1330 		d->resizeBufferView();
1331 	}
1332 
1333 	d->last_pixel_ratio_ = pixelRatio();
1334 
1335 	GuiPainter pain(d->screenDevice(), pixelRatio());
1336 
1337 	d->buffer_view_->draw(pain, d->caret_visible_);
1338 
1339 	// The preedit text, if needed
1340 	d->paintPreeditText(pain);
1341 
1342 	// and the caret
1343 	if (d->caret_visible_)
1344 		d->caret_->draw(pain, d->buffer_view_->horizScrollOffset());
1345 
1346 	d->updateScreen(ev->rect());
1347 
1348 	ev->accept();
1349 }
1350 
1351 
inputMethodEvent(QInputMethodEvent * e)1352 void GuiWorkArea::inputMethodEvent(QInputMethodEvent * e)
1353 {
1354 	LYXERR(Debug::KEY, "preeditString: " << e->preeditString()
1355 		   << " commitString: " << e->commitString());
1356 
1357 	// insert the processed text in the document (handles undo)
1358 	if (!e->commitString().isEmpty()) {
1359 		FuncRequest cmd(LFUN_SELF_INSERT,
1360 		                qstring_to_ucs4(e->commitString()),
1361 		                FuncRequest::KEYBOARD);
1362 		dispatch(cmd);
1363 		// FIXME: this is supposed to remove traces from preedit
1364 		// string. Can we avoid calling it explicitely?
1365 		d->buffer_view_->updateMetrics();
1366 	}
1367 
1368 	// Hide the caret during the test transformation.
1369 	if (e->preeditString().isEmpty())
1370 		startBlinkingCaret();
1371 	else
1372 		stopBlinkingCaret();
1373 
1374 	if (d->preedit_string_.empty() && e->preeditString().isEmpty()) {
1375 		// Nothing to do
1376 		e->accept();
1377 		return;
1378 	}
1379 
1380 	// The preedit text and its attributes will be used in paintPreeditText
1381 	d->preedit_string_ = qstring_to_ucs4(e->preeditString());
1382 	d->preedit_attr_ = e->attributes();
1383 
1384 
1385 	// redraw area of preedit string.
1386 	int height = d->caret_->rect().height();
1387 	int cur_y = d->caret_->rect().bottom();
1388 	viewport()->update(0, cur_y - height, viewport()->width(),
1389 		(height + 1) * d->preedit_lines_);
1390 
1391 	if (d->preedit_string_.empty()) {
1392 		d->preedit_lines_ = 1;
1393 		e->accept();
1394 		return;
1395 	}
1396 
1397 	// Don't forget to accept the event!
1398 	e->accept();
1399 }
1400 
1401 
inputMethodQuery(Qt::InputMethodQuery query) const1402 QVariant GuiWorkArea::inputMethodQuery(Qt::InputMethodQuery query) const
1403 {
1404 	QRect cur_r(0, 0, 0, 0);
1405 	switch (query) {
1406 		// this is the CJK-specific composition window position and
1407 		// the context menu position when the menu key is pressed.
1408 		case Qt::ImMicroFocus:
1409 			cur_r = d->caret_->rect();
1410 			if (d->preedit_lines_ != 1)
1411 				cur_r.moveLeft(10);
1412 			cur_r.moveBottom(cur_r.bottom()
1413 				+ cur_r.height() * (d->preedit_lines_ - 1));
1414 			// return lower right of caret in LyX.
1415 			return cur_r;
1416 		default:
1417 			return QWidget::inputMethodQuery(query);
1418 	}
1419 }
1420 
1421 
updateWindowTitle()1422 void GuiWorkArea::updateWindowTitle()
1423 {
1424 	Buffer const & buf = bufferView().buffer();
1425 	if (buf.fileName() != d->file_name_
1426 	    || buf.params().shell_escape != d->shell_escape_
1427 	    || buf.hasReadonlyFlag() != d->read_only_
1428 	    || buf.lyxvc().vcstatus() != d->vc_status_
1429 	    || buf.isClean() != d->clean_
1430 	    || buf.notifiesExternalModification() != d->externally_modified_) {
1431 		d->file_name_ = buf.fileName();
1432 		d->shell_escape_ = buf.params().shell_escape;
1433 		d->read_only_ = buf.hasReadonlyFlag();
1434 		d->vc_status_ = buf.lyxvc().vcstatus();
1435 		d->clean_ = buf.isClean();
1436 		d->externally_modified_ = buf.notifiesExternalModification();
1437 		Q_EMIT titleChanged(this);
1438 	}
1439 }
1440 
1441 
isFullScreen() const1442 bool GuiWorkArea::isFullScreen() const
1443 {
1444 	return d->lyx_view_ && d->lyx_view_->isFullScreen();
1445 }
1446 
1447 
inDialogMode() const1448 bool GuiWorkArea::inDialogMode() const
1449 {
1450 	return d->dialog_mode_;
1451 }
1452 
1453 
setDialogMode(bool mode)1454 void GuiWorkArea::setDialogMode(bool mode)
1455 {
1456 	d->dialog_mode_ = mode;
1457 }
1458 
1459 
completer()1460 GuiCompleter & GuiWorkArea::completer()
1461 {
1462 	return *d->completer_;
1463 }
1464 
view() const1465 GuiView const & GuiWorkArea::view() const
1466 {
1467 	return *d->lyx_view_;
1468 }
1469 
1470 
view()1471 GuiView & GuiWorkArea::view()
1472 {
1473 	return *d->lyx_view_;
1474 }
1475 
1476 ////////////////////////////////////////////////////////////////////
1477 //
1478 // EmbeddedWorkArea
1479 //
1480 ////////////////////////////////////////////////////////////////////
1481 
1482 
EmbeddedWorkArea(QWidget * w)1483 EmbeddedWorkArea::EmbeddedWorkArea(QWidget * w): GuiWorkArea(w)
1484 {
1485 	support::TempFile tempfile("embedded.internal");
1486 	tempfile.setAutoRemove(false);
1487 	buffer_ = theBufferList().newInternalBuffer(tempfile.name().absFileName());
1488 	buffer_->setUnnamed(true);
1489 	buffer_->setFullyLoaded(true);
1490 	setBuffer(*buffer_);
1491 	setDialogMode(true);
1492 }
1493 
1494 
~EmbeddedWorkArea()1495 EmbeddedWorkArea::~EmbeddedWorkArea()
1496 {
1497 	// No need to destroy buffer and bufferview here, because it is done
1498 	// in theBufferList() destruction loop at application exit
1499 }
1500 
1501 
closeEvent(QCloseEvent * ev)1502 void EmbeddedWorkArea::closeEvent(QCloseEvent * ev)
1503 {
1504 	disable();
1505 	GuiWorkArea::closeEvent(ev);
1506 }
1507 
1508 
hideEvent(QHideEvent * ev)1509 void EmbeddedWorkArea::hideEvent(QHideEvent * ev)
1510 {
1511 	disable();
1512 	GuiWorkArea::hideEvent(ev);
1513 }
1514 
1515 
sizeHint() const1516 QSize EmbeddedWorkArea::sizeHint () const
1517 {
1518 	// FIXME(?):
1519 	// GuiWorkArea sets the size to the screen's viewport
1520 	// by returning a value this gets overridden
1521 	// EmbeddedWorkArea is now sized to fit in the layout
1522 	// of the parent, and has a minimum size set in GuiWorkArea
1523 	// which is what we return here
1524 	return QSize(100, 70);
1525 }
1526 
1527 
disable()1528 void EmbeddedWorkArea::disable()
1529 {
1530 	stopBlinkingCaret();
1531 	if (view().currentWorkArea() != this)
1532 		return;
1533 	// No problem if currentMainWorkArea() is 0 (setCurrentWorkArea()
1534 	// tolerates it and shows the background logo), what happens if
1535 	// an EmbeddedWorkArea is closed after closing all document WAs
1536 	view().setCurrentWorkArea(view().currentMainWorkArea());
1537 }
1538 
1539 ////////////////////////////////////////////////////////////////////
1540 //
1541 // TabWorkArea
1542 //
1543 ////////////////////////////////////////////////////////////////////
1544 
1545 #ifdef Q_OS_MAC
1546 class NoTabFrameMacStyle : public QProxyStyle {
1547 public:
1548 	///
subElementRect(SubElement element,const QStyleOption * option,const QWidget * widget=0) const1549 	QRect subElementRect(SubElement element, const QStyleOption * option,
1550 			     const QWidget * widget = 0) const
1551 	{
1552 		QRect rect = QProxyStyle::subElementRect(element, option, widget);
1553 		bool noBar = static_cast<QTabWidget const *>(widget)->count() <= 1;
1554 
1555 		// The Qt Mac style puts the contents into a 3 pixel wide box
1556 		// which looks very ugly and not like other Mac applications.
1557 		// Hence we remove this here, and moreover the 16 pixel round
1558 		// frame above if the tab bar is hidden.
1559 		if (element == QStyle::SE_TabWidgetTabContents) {
1560 			rect.adjust(- rect.left(), 0, rect.left(), 0);
1561 			if (noBar)
1562 				rect.setTop(0);
1563 		}
1564 
1565 		return rect;
1566 	}
1567 };
1568 
1569 NoTabFrameMacStyle noTabFrameMacStyle;
1570 #endif
1571 
1572 
TabWorkArea(QWidget * parent)1573 TabWorkArea::TabWorkArea(QWidget * parent)
1574 	: QTabWidget(parent), clicked_tab_(-1), midpressed_tab_(-1)
1575 {
1576 #ifdef Q_OS_MAC
1577 	setStyle(&noTabFrameMacStyle);
1578 #endif
1579 
1580 	QPalette pal = palette();
1581 	pal.setColor(QPalette::Active, QPalette::Button,
1582 		pal.color(QPalette::Active, QPalette::Window));
1583 	pal.setColor(QPalette::Disabled, QPalette::Button,
1584 		pal.color(QPalette::Disabled, QPalette::Window));
1585 	pal.setColor(QPalette::Inactive, QPalette::Button,
1586 		pal.color(QPalette::Inactive, QPalette::Window));
1587 
1588 	QObject::connect(this, SIGNAL(currentChanged(int)),
1589 		this, SLOT(on_currentTabChanged(int)));
1590 
1591 	closeBufferButton = new QToolButton(this);
1592 	closeBufferButton->setPalette(pal);
1593 	// FIXME: rename the icon to closebuffer.png
1594 	closeBufferButton->setIcon(QIcon(getPixmap("images/", "closetab", "svgz,png")));
1595 	closeBufferButton->setText("Close File");
1596 	closeBufferButton->setAutoRaise(true);
1597 	closeBufferButton->setCursor(Qt::ArrowCursor);
1598 	closeBufferButton->setToolTip(qt_("Close File"));
1599 	closeBufferButton->setEnabled(true);
1600 	QObject::connect(closeBufferButton, SIGNAL(clicked()),
1601 		this, SLOT(closeCurrentBuffer()));
1602 	setCornerWidget(closeBufferButton, Qt::TopRightCorner);
1603 
1604 	// set TabBar behaviour
1605 	QTabBar * tb = tabBar();
1606 	tb->setTabsClosable(!lyxrc.single_close_tab_button);
1607 	tb->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab);
1608 	tb->setElideMode(Qt::ElideNone);
1609 	// allow dragging tabs
1610 	tb->setMovable(true);
1611 	// make us responsible for the context menu of the tabbar
1612 	tb->setContextMenuPolicy(Qt::CustomContextMenu);
1613 	connect(tb, SIGNAL(customContextMenuRequested(const QPoint &)),
1614 	        this, SLOT(showContextMenu(const QPoint &)));
1615 	connect(tb, SIGNAL(tabCloseRequested(int)),
1616 	        this, SLOT(closeTab(int)));
1617 
1618 	setUsesScrollButtons(true);
1619 }
1620 
1621 
mousePressEvent(QMouseEvent * me)1622 void TabWorkArea::mousePressEvent(QMouseEvent *me)
1623 {
1624 	if (me->button() == Qt::MidButton)
1625 		midpressed_tab_ = tabBar()->tabAt(me->pos());
1626 	else
1627 		QTabWidget::mousePressEvent(me);
1628 }
1629 
1630 
mouseReleaseEvent(QMouseEvent * me)1631 void TabWorkArea::mouseReleaseEvent(QMouseEvent *me)
1632 {
1633 	if (me->button() == Qt::MidButton) {
1634 		int const midreleased_tab = tabBar()->tabAt(me->pos());
1635 		if (midpressed_tab_ == midreleased_tab && posIsTab(me->pos()))
1636 			closeTab(midreleased_tab);
1637 	} else
1638 		QTabWidget::mouseReleaseEvent(me);
1639 }
1640 
1641 
paintEvent(QPaintEvent * event)1642 void TabWorkArea::paintEvent(QPaintEvent * event)
1643 {
1644 	if (tabBar()->isVisible()) {
1645 		QTabWidget::paintEvent(event);
1646 	} else {
1647 		// Prevent the selected tab to influence the
1648 		// painting of the frame of the tab widget.
1649 		// This is needed for gtk style in Qt.
1650 		QStylePainter p(this);
1651 #if QT_VERSION < 0x050000
1652 		QStyleOptionTabWidgetFrameV2 opt;
1653 #else
1654 		QStyleOptionTabWidgetFrame opt;
1655 #endif
1656 		initStyleOption(&opt);
1657 		opt.rect = style()->subElementRect(QStyle::SE_TabWidgetTabPane,
1658 			&opt, this);
1659 		opt.selectedTabRect = QRect();
1660 		p.drawPrimitive(QStyle::PE_FrameTabWidget, opt);
1661 	}
1662 }
1663 
1664 
posIsTab(QPoint position)1665 bool TabWorkArea::posIsTab(QPoint position)
1666 {
1667 	// tabAt returns -1 if tab does not covers position
1668 	return tabBar()->tabAt(position) > -1;
1669 }
1670 
1671 
mouseDoubleClickEvent(QMouseEvent * event)1672 void TabWorkArea::mouseDoubleClickEvent(QMouseEvent * event)
1673 {
1674 	if (event->button() != Qt::LeftButton)
1675 		return;
1676 
1677 	// this code chunk is unnecessary because it seems the event only makes
1678 	// it this far if it is not on a tab. I'm not sure why this is (maybe
1679 	// it is handled and ended in DragTabBar?), and thus I'm not sure if
1680 	// this is true in all cases and if it will be true in the future so I
1681 	// leave this code for now. (skostysh, 2016-07-21)
1682 	//
1683 	// return early if double click on existing tabs
1684 	if (posIsTab(event->pos()))
1685 		return;
1686 
1687 	dispatch(FuncRequest(LFUN_BUFFER_NEW));
1688 }
1689 
1690 
setFullScreen(bool full_screen)1691 void TabWorkArea::setFullScreen(bool full_screen)
1692 {
1693 	for (int i = 0; i != count(); ++i) {
1694 		if (GuiWorkArea * wa = workArea(i))
1695 			wa->setFullScreen(full_screen);
1696 	}
1697 
1698 	if (lyxrc.full_screen_tabbar)
1699 		showBar(!full_screen && count() > 1);
1700 	else
1701 		showBar(count() > 1);
1702 }
1703 
1704 
showBar(bool show)1705 void TabWorkArea::showBar(bool show)
1706 {
1707 	tabBar()->setEnabled(show);
1708 	tabBar()->setVisible(show);
1709 	closeBufferButton->setVisible(show && lyxrc.single_close_tab_button);
1710 	setTabsClosable(!lyxrc.single_close_tab_button);
1711 }
1712 
1713 
widget(int index) const1714 GuiWorkAreaContainer * TabWorkArea::widget(int index) const
1715 {
1716 	QWidget * w = QTabWidget::widget(index);
1717 	if (!w)
1718 		return nullptr;
1719 	GuiWorkAreaContainer * wac = dynamic_cast<GuiWorkAreaContainer *>(w);
1720 	LATTEST(wac);
1721 	return wac;
1722 }
1723 
1724 
currentWidget() const1725 GuiWorkAreaContainer * TabWorkArea::currentWidget() const
1726 {
1727 	return widget(currentIndex());
1728 }
1729 
1730 
workArea(int index) const1731 GuiWorkArea * TabWorkArea::workArea(int index) const
1732 {
1733 	GuiWorkAreaContainer * w = widget(index);
1734 	if (!w)
1735 		return nullptr;
1736 	return w->workArea();
1737 }
1738 
1739 
currentWorkArea() const1740 GuiWorkArea * TabWorkArea::currentWorkArea() const
1741 {
1742 	return workArea(currentIndex());
1743 }
1744 
1745 
workArea(Buffer & buffer) const1746 GuiWorkArea * TabWorkArea::workArea(Buffer & buffer) const
1747 {
1748 	// FIXME: this method doesn't work if we have more than one work area
1749 	// showing the same buffer.
1750 	for (int i = 0; i != count(); ++i) {
1751 		GuiWorkArea * wa = workArea(i);
1752 		LASSERT(wa, return 0);
1753 		if (&wa->bufferView().buffer() == &buffer)
1754 			return wa;
1755 	}
1756 	return 0;
1757 }
1758 
1759 
closeAll()1760 void TabWorkArea::closeAll()
1761 {
1762 	while (count()) {
1763 		QWidget * wac = widget(0);
1764 		LASSERT(wac, return);
1765 		removeTab(0);
1766 		delete wac;
1767 	}
1768 }
1769 
1770 
indexOfWorkArea(GuiWorkArea * w) const1771 int TabWorkArea::indexOfWorkArea(GuiWorkArea * w) const
1772 {
1773 	for (int index = 0; index < count(); ++index)
1774 		if (workArea(index) == w)
1775 			return index;
1776 	return -1;
1777 }
1778 
1779 
setCurrentWorkArea(GuiWorkArea * work_area)1780 bool TabWorkArea::setCurrentWorkArea(GuiWorkArea * work_area)
1781 {
1782 	LASSERT(work_area, return false);
1783 	int index = indexOfWorkArea(work_area);
1784 	if (index == -1)
1785 		return false;
1786 
1787 	if (index == currentIndex())
1788 		// Make sure the work area is up to date.
1789 		on_currentTabChanged(index);
1790 	else
1791 		// Switch to the work area.
1792 		setCurrentIndex(index);
1793 	work_area->setFocus();
1794 
1795 	return true;
1796 }
1797 
1798 
addWorkArea(Buffer & buffer,GuiView & view)1799 GuiWorkArea * TabWorkArea::addWorkArea(Buffer & buffer, GuiView & view)
1800 {
1801 	GuiWorkArea * wa = new GuiWorkArea(buffer, view);
1802 	GuiWorkAreaContainer * wac = new GuiWorkAreaContainer(wa);
1803 	wa->setUpdatesEnabled(false);
1804 	// Hide tabbar if there's no tab (avoid a resize and a flashing tabbar
1805 	// when hiding it again below).
1806 	if (!(currentWorkArea() && currentWorkArea()->isFullScreen()))
1807 		showBar(count() > 0);
1808 	addTab(wac, wa->windowTitle());
1809 	QObject::connect(wa, SIGNAL(titleChanged(GuiWorkArea *)),
1810 		this, SLOT(updateTabTexts()));
1811 	if (currentWorkArea() && currentWorkArea()->isFullScreen())
1812 		setFullScreen(true);
1813 	else
1814 		// Hide tabbar if there's only one tab.
1815 		showBar(count() > 1);
1816 
1817 	updateTabTexts();
1818 
1819 	return wa;
1820 }
1821 
1822 
removeWorkArea(GuiWorkArea * work_area)1823 bool TabWorkArea::removeWorkArea(GuiWorkArea * work_area)
1824 {
1825 	LASSERT(work_area, return false);
1826 	int index = indexOfWorkArea(work_area);
1827 	if (index == -1)
1828 		return false;
1829 
1830 	work_area->setUpdatesEnabled(false);
1831 	QWidget * wac = widget(index);
1832 	removeTab(index);
1833 	delete wac;
1834 
1835 	if (count()) {
1836 		// make sure the next work area is enabled.
1837 		currentWidget()->setUpdatesEnabled(true);
1838 		if (currentWorkArea() && currentWorkArea()->isFullScreen())
1839 			setFullScreen(true);
1840 		else
1841 			// Show tabbar only if there's more than one tab.
1842 			showBar(count() > 1);
1843 	} else
1844 		lastWorkAreaRemoved();
1845 
1846 	updateTabTexts();
1847 
1848 	return true;
1849 }
1850 
1851 
on_currentTabChanged(int i)1852 void TabWorkArea::on_currentTabChanged(int i)
1853 {
1854 	// returns e.g. on application destruction
1855 	if (i == -1)
1856 		return;
1857 	GuiWorkArea * wa = workArea(i);
1858 	LASSERT(wa, return);
1859 	wa->setUpdatesEnabled(true);
1860 	wa->scheduleRedraw(true);
1861 	wa->setFocus();
1862 	///
1863 	currentWorkAreaChanged(wa);
1864 
1865 	LYXERR(Debug::GUI, "currentTabChanged " << i
1866 		<< " File: " << wa->bufferView().buffer().absFileName());
1867 }
1868 
1869 
closeCurrentBuffer()1870 void TabWorkArea::closeCurrentBuffer()
1871 {
1872 	GuiWorkArea * wa;
1873 	if (clicked_tab_ == -1)
1874 		wa = currentWorkArea();
1875 	else {
1876 		wa = workArea(clicked_tab_);
1877 		LASSERT(wa, return);
1878 	}
1879 	wa->view().closeWorkArea(wa);
1880 }
1881 
1882 
hideCurrentTab()1883 void TabWorkArea::hideCurrentTab()
1884 {
1885 	GuiWorkArea * wa;
1886 	if (clicked_tab_ == -1)
1887 		wa = currentWorkArea();
1888 	else {
1889 		wa = workArea(clicked_tab_);
1890 		LASSERT(wa, return);
1891 	}
1892 	wa->view().hideWorkArea(wa);
1893 }
1894 
1895 
closeTab(int index)1896 void TabWorkArea::closeTab(int index)
1897 {
1898 	on_currentTabChanged(index);
1899 	GuiWorkArea * wa;
1900 	if (index == -1)
1901 		wa = currentWorkArea();
1902 	else {
1903 		wa = workArea(index);
1904 		LASSERT(wa, return);
1905 	}
1906 	wa->view().closeWorkArea(wa);
1907 }
1908 
1909 
1910 ///
1911 class DisplayPath {
1912 public:
1913 	/// make vector happy
DisplayPath()1914 	DisplayPath() : tab_(-1), dottedPrefix_(false) {}
1915 	///
DisplayPath(int tab,FileName const & filename)1916 	DisplayPath(int tab, FileName const & filename)
1917 		: tab_(tab)
1918 	{
1919 		filename_ = (filename.extension() == "lyx") ?
1920 			toqstr(filename.onlyFileNameWithoutExt())
1921 			: toqstr(filename.onlyFileName());
1922 		postfix_ = toqstr(filename.absoluteFilePath()).
1923 			split("/", QString::SkipEmptyParts);
1924 		postfix_.pop_back();
1925 		abs_ = toqstr(filename.absoluteFilePath());
1926 		dottedPrefix_ = false;
1927 	}
1928 
1929 	/// Absolute path for debugging.
abs() const1930 	QString abs() const
1931 	{
1932 		return abs_;
1933 	}
1934 	/// Add the first segment from the postfix or three dots to the prefix.
1935 	/// Merge multiple dot tripples. In fact dots are added lazily, i.e. only
1936 	/// when really needed.
shiftPathSegment(bool dotted)1937 	void shiftPathSegment(bool dotted)
1938 	{
1939 		if (postfix_.count() <= 0)
1940 			return;
1941 
1942 		if (!dotted) {
1943 			if (dottedPrefix_ && !prefix_.isEmpty())
1944 				prefix_ += ellipsisSlash_;
1945 			prefix_ += postfix_.front() + "/";
1946 		}
1947 		dottedPrefix_ = dotted && !prefix_.isEmpty();
1948 		postfix_.pop_front();
1949 	}
1950 	///
displayString() const1951 	QString displayString() const
1952 	{
1953 		if (prefix_.isEmpty())
1954 			return filename_;
1955 
1956 		bool dots = dottedPrefix_ || !postfix_.isEmpty();
1957 		return prefix_ + (dots ? ellipsisSlash_ : "") + filename_;
1958 	}
1959 	///
forecastPathString() const1960 	QString forecastPathString() const
1961 	{
1962 		if (postfix_.count() == 0)
1963 			return displayString();
1964 
1965 		return prefix_
1966 			+ (dottedPrefix_ ? ellipsisSlash_ : "")
1967 			+ postfix_.front() + "/";
1968 	}
1969 	///
final() const1970 	bool final() const { return postfix_.empty(); }
1971 	///
tab() const1972 	int tab() const { return tab_; }
1973 
1974 private:
1975 	/// ".../"
1976 	static QString const ellipsisSlash_;
1977 	///
1978 	QString prefix_;
1979 	///
1980 	QStringList postfix_;
1981 	///
1982 	QString filename_;
1983 	///
1984 	QString abs_;
1985 	///
1986 	int tab_;
1987 	///
1988 	bool dottedPrefix_;
1989 };
1990 
1991 
1992 QString const DisplayPath::ellipsisSlash_ = QString(QChar(0x2026)) + "/";
1993 
1994 
1995 ///
operator <(DisplayPath const & a,DisplayPath const & b)1996 bool operator<(DisplayPath const & a, DisplayPath const & b)
1997 {
1998 	return a.displayString() < b.displayString();
1999 }
2000 
2001 ///
operator ==(DisplayPath const & a,DisplayPath const & b)2002 bool operator==(DisplayPath const & a, DisplayPath const & b)
2003 {
2004 	return a.displayString() == b.displayString();
2005 }
2006 
2007 
updateTabTexts()2008 void TabWorkArea::updateTabTexts()
2009 {
2010 	size_t n = count();
2011 	if (n == 0)
2012 		return;
2013 	std::list<DisplayPath> paths;
2014 	typedef std::list<DisplayPath>::iterator It;
2015 
2016 	// collect full names first: path into postfix, empty prefix and
2017 	// filename without extension
2018 	for (size_t i = 0; i < n; ++i) {
2019 		GuiWorkArea * i_wa = workArea(i);
2020 		FileName const fn = i_wa->bufferView().buffer().fileName();
2021 		paths.push_back(DisplayPath(i, fn));
2022 	}
2023 
2024 	// go through path segments and see if it helps to make the path more unique
2025 	bool somethingChanged = true;
2026 	bool allFinal = false;
2027 	while (somethingChanged && !allFinal) {
2028 		// adding path segments changes order
2029 		paths.sort();
2030 
2031 		LYXERR(Debug::GUI, "updateTabTexts() iteration start");
2032 		somethingChanged = false;
2033 		allFinal = true;
2034 
2035 		// find segments which are not unique (i.e. non-atomic)
2036 		It it = paths.begin();
2037 		It segStart = it;
2038 		QString segString = it->displayString();
2039 		for (; it != paths.end(); ++it) {
2040 			// look to the next item
2041 			It next = it;
2042 			++next;
2043 
2044 			// final?
2045 			allFinal = allFinal && it->final();
2046 
2047 			LYXERR(Debug::GUI, "it = " << it->abs()
2048 			       << " => " << it->displayString());
2049 
2050 			// still the same segment?
2051 			QString nextString;
2052 			if ((next != paths.end()
2053 			     && (nextString = next->displayString()) == segString))
2054 				continue;
2055 			LYXERR(Debug::GUI, "segment ended");
2056 
2057 			// only a trivial one with one element?
2058 			if (it == segStart) {
2059 				// start new segment
2060 				segStart = next;
2061 				segString = nextString;
2062 				continue;
2063 			}
2064 
2065 			// We found a non-atomic segment
2066 			// We know that segStart <= it < next <= paths.end().
2067 			// The assertion below tells coverity about it.
2068 			LATTEST(segStart != paths.end());
2069 			QString dspString = segStart->forecastPathString();
2070 			LYXERR(Debug::GUI, "first forecast found for "
2071 			       << segStart->abs() << " => " << dspString);
2072 			It sit = segStart;
2073 			++sit;
2074 			// Shift path segments and hope for the best
2075 			// that it makes the path more unique.
2076 			somethingChanged = true;
2077 			bool moreUnique = false;
2078 			for (; sit != next; ++sit) {
2079 				if (sit->forecastPathString() != dspString) {
2080 					LYXERR(Debug::GUI, "different forecast found for "
2081 						<< sit->abs() << " => " << sit->forecastPathString());
2082 					moreUnique = true;
2083 					break;
2084 				}
2085 				LYXERR(Debug::GUI, "same forecast found for "
2086 					<< sit->abs() << " => " << dspString);
2087 			}
2088 
2089 			// if the path segment helped, add it. Otherwise add dots
2090 			bool dots = !moreUnique;
2091 			LYXERR(Debug::GUI, "using dots = " << dots);
2092 			for (sit = segStart; sit != next; ++sit) {
2093 				sit->shiftPathSegment(dots);
2094 				LYXERR(Debug::GUI, "shifting "
2095 					<< sit->abs() << " => " << sit->displayString());
2096 			}
2097 
2098 			// start new segment
2099 			segStart = next;
2100 			segString = nextString;
2101 		}
2102 	}
2103 
2104 	// set new tab titles
2105 	for (It it = paths.begin(); it != paths.end(); ++it) {
2106 		int const tab_index = it->tab();
2107 		Buffer const & buf = workArea(tab_index)->bufferView().buffer();
2108 		QString tab_text = it->displayString().replace("&", "&&");
2109 		if (!buf.fileName().empty() && !buf.isClean())
2110 			tab_text += "*";
2111 		QString tab_tooltip = it->abs();
2112 		if (buf.hasReadonlyFlag()) {
2113 			setTabIcon(tab_index, QIcon(getPixmap("images/", "emblem-readonly", "svgz,png")));
2114 			tab_tooltip = qt_("%1 (read only)").arg(tab_tooltip);
2115 		} else
2116 			setTabIcon(tab_index, QIcon());
2117 		if (buf.notifiesExternalModification()) {
2118 			QString const warn = qt_("%1 (modified externally)");
2119 			tab_tooltip = warn.arg(tab_tooltip);
2120 			tab_text += QChar(0x26a0);
2121 		}
2122 		setTabText(tab_index, tab_text);
2123 		setTabToolTip(tab_index, tab_tooltip);
2124 	}
2125 }
2126 
2127 
showContextMenu(const QPoint & pos)2128 void TabWorkArea::showContextMenu(const QPoint & pos)
2129 {
2130 	// which tab?
2131 	clicked_tab_ = tabBar()->tabAt(pos);
2132 	if (clicked_tab_ == -1)
2133 		return;
2134 
2135 	// show tab popup
2136 	QMenu popup;
2137 	popup.addAction(QIcon(getPixmap("images/", "hidetab", "svgz,png")),
2138 		qt_("Hide tab"), this, SLOT(hideCurrentTab()));
2139 	popup.addAction(QIcon(getPixmap("images/", "closetab", "svgz,png")),
2140 		qt_("Close tab"), this, SLOT(closeCurrentBuffer()));
2141 	popup.exec(tabBar()->mapToGlobal(pos));
2142 
2143 	clicked_tab_ = -1;
2144 }
2145 
2146 
moveTab(int fromIndex,int toIndex)2147 void TabWorkArea::moveTab(int fromIndex, int toIndex)
2148 {
2149 	QWidget * w = widget(fromIndex);
2150 	QIcon icon = tabIcon(fromIndex);
2151 	QString text = tabText(fromIndex);
2152 
2153 	setCurrentIndex(fromIndex);
2154 	removeTab(fromIndex);
2155 	insertTab(toIndex, w, icon, text);
2156 	setCurrentIndex(toIndex);
2157 }
2158 
2159 
GuiWorkAreaContainer(GuiWorkArea * wa,QWidget * parent)2160 GuiWorkAreaContainer::GuiWorkAreaContainer(GuiWorkArea * wa, QWidget * parent)
2161 	: QWidget(parent), wa_(wa)
2162 {
2163 	LASSERT(wa, return);
2164 	Ui::WorkAreaUi::setupUi(this);
2165 	layout()->addWidget(wa);
2166 	connect(wa, SIGNAL(titleChanged(GuiWorkArea *)),
2167 	        this, SLOT(updateDisplay()));
2168 	connect(reloadPB, SIGNAL(clicked()), this, SLOT(reload()));
2169 	connect(ignorePB, SIGNAL(clicked()), this, SLOT(ignore()));
2170 	setMessageColour({notificationFrame}, {reloadPB, ignorePB});
2171 	updateDisplay();
2172 }
2173 
2174 
updateDisplay()2175 void GuiWorkAreaContainer::updateDisplay()
2176 {
2177 	Buffer const & buf = wa_->bufferView().buffer();
2178 	notificationFrame->setHidden(!buf.notifiesExternalModification());
2179 	QString const label = qt_("<b>The file %1 changed on disk.</b>")
2180 		.arg(toqstr(buf.fileName().displayName()));
2181 	externalModificationLabel->setText(label);
2182 }
2183 
2184 
dispatch(FuncRequest f) const2185 void GuiWorkAreaContainer::dispatch(FuncRequest f) const
2186 {
2187 	lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
2188 	                          wa_->bufferView().buffer().absFileName()));
2189 	lyx::dispatch(f);
2190 }
2191 
2192 
reload() const2193 void GuiWorkAreaContainer::reload() const
2194 {
2195 	dispatch(FuncRequest(LFUN_BUFFER_RELOAD));
2196 }
2197 
2198 
ignore() const2199 void GuiWorkAreaContainer::ignore() const
2200 {
2201 	dispatch(FuncRequest(LFUN_BUFFER_EXTERNAL_MODIFICATION_CLEAR));
2202 }
2203 
2204 
mouseDoubleClickEvent(QMouseEvent * event)2205 void GuiWorkAreaContainer::mouseDoubleClickEvent(QMouseEvent * event)
2206 {
2207 	// prevent TabWorkArea from opening a new buffer on double click
2208 	event->accept();
2209 }
2210 
2211 
2212 } // namespace frontend
2213 } // namespace lyx
2214 
2215 #include "moc_GuiWorkArea.cpp"
2216