#include "GuiWorkArea_Private.h"
#include "ColorCache.h"
-#include "FontLoader.h"
#include "GuiApplication.h"
#include "GuiCompleter.h"
#include "GuiKeySymbol.h"
#include "Font.h"
#include "FuncRequest.h"
#include "KeySymbol.h"
-#include "Language.h"
#include "LyX.h"
#include "LyXRC.h"
#include "LyXVC.h"
#include "Undo.h"
#include "version.h"
-#include "graphics/GraphicsImage.h"
-#include "graphics/GraphicsLoader.h"
-
#include "support/convert.h"
#include "support/debug.h"
#include "support/lassert.h"
#include "support/TempFile.h"
#include "frontends/Application.h"
+#include "frontends/CaretGeometry.h"
+
#include "frontends/FontMetrics.h"
#include "frontends/WorkAreaManager.h"
#include <cmath>
#include <iostream>
-int const TabIndicatorWidth = 3;
-
#undef KeyPress
#undef NoModifier
case Qt::LeftButton:
b = mouse_button::button1;
break;
- case Qt::MidButton:
+ case Qt::MiddleButton:
b = mouse_button::button2;
break;
case Qt::RightButton:
mouse_button::state b = mouse_button::none;
if (state & Qt::LeftButton)
b |= mouse_button::button1;
- if (state & Qt::MidButton)
+ if (state & Qt::MiddleButton)
b |= mouse_button::button2;
if (state & Qt::RightButton)
b |= mouse_button::button3;
namespace frontend {
-class CaretWidget {
-public:
- CaretWidget() : rtl_(false), l_shape_(false), completable_(false),
- x_(0), caret_width_(0)
- {}
-
- void draw(QPainter & painter)
- {
- if (!rect_.isValid())
- return;
-
- int y = rect_.top();
- int l = x_ - rect_.left();
- int r = rect_.right() - x_;
- int bot = rect_.bottom();
-
- // draw vertical line
- painter.fillRect(x_, y, caret_width_, rect_.height(), color_);
-
- // draw RTL/LTR indication
- painter.setPen(color_);
- if (l_shape_) {
- if (rtl_)
- painter.drawLine(x_, bot, x_ - l + 1, bot);
- else
- painter.drawLine(x_, bot, x_ + caret_width_ + r - 1, bot);
- }
-
- // draw completion triangle
- if (completable_) {
- int m = y + rect_.height() / 2;
- int d = TabIndicatorWidth - 1;
- if (rtl_) {
- painter.drawLine(x_ - 1, m - d, x_ - 1 - d, m);
- painter.drawLine(x_ - 1, m + d, x_ - 1 - d, m);
- } else {
- painter.drawLine(x_ + caret_width_, m - d, x_ + caret_width_ + d, m);
- painter.drawLine(x_ + caret_width_, m + d, x_ + caret_width_ + d, m);
- }
- }
- }
-
- void update(int x, int y, int h, bool l_shape,
- bool rtl, bool completable)
- {
- color_ = guiApp->colorCache().get(Color_cursor);
- l_shape_ = l_shape;
- rtl_ = rtl;
- completable_ = completable;
- x_ = x;
-
- // extension to left and right
- int l = 0;
- int r = 0;
-
- // RTL/LTR indication
- if (l_shape_) {
- if (rtl)
- l += h / 3;
- else
- r += h / 3;
- }
-
- // completion triangle
- if (completable_) {
- if (rtl)
- l = max(l, TabIndicatorWidth);
- else
- r = max(r, TabIndicatorWidth);
- }
-
- //FIXME: LyXRC::cursor_width should be caret_width
- caret_width_ = lyxrc.cursor_width
- ? lyxrc.cursor_width
- : 1 + int((lyxrc.currentZoom + 50) / 200.0);
-
- // compute overall rectangle
- rect_ = QRect(x - l, y, caret_width_ + r + l, h);
- }
-
- QRect const & rect() { return rect_; }
-
-private:
- /// caret is in RTL or LTR text
- bool rtl_;
- /// indication for RTL or LTR
- bool l_shape_;
- /// triangle to show that a completion is available
- bool completable_;
- ///
- QColor color_;
- /// rectangle, possibly with l_shape and completion triangle
- QRect rect_;
- /// x position (were the vertical line is drawn)
- int x_;
- /// the width of the vertical blinking bar
- int caret_width_;
-};
-
-
// This is a 'heartbeat' generating synthetic mouse move events when the
// cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s
SyntheticMouseEvent::SyntheticMouseEvent()
GuiWorkArea::Private::Private(GuiWorkArea * parent)
-: p(parent), buffer_view_(0), lyx_view_(0), caret_(0),
- caret_visible_(false), need_resize_(false), preedit_lines_(1),
- last_pixel_ratio_(1.0), completer_(new GuiCompleter(p, p)),
- dialog_mode_(false), shell_escape_(false), read_only_(false),
- clean_(true), externally_modified_(false)
-{
-/* Qt on macOS and Wayland does not respect the
- * Qt::WA_OpaquePaintEvent attribute and resets the widget backing
- * store at each update. Therefore, we use our own backing store in
- * these two cases. */
-#if QT_VERSION >= 0x050000
+ : p(parent), completer_(new GuiCompleter(p, p))
+{
+ /* Qt on macOS and Wayland does not respect the
+ * Qt::WA_OpaquePaintEvent attribute and resets the widget backing
+ * store at each update. Therefore, we use our own backing store
+ * in these two cases.
+ */
use_backingstore_ = guiApp->platformName() == "cocoa"
|| guiApp->platformName().contains("wayland");
-#else
-# ifdef Q_OS_MAC
- use_backingstore_ = true;
-# else
- use_backingstore_ = false;
-# endif
-#endif
int const time = QApplication::cursorFlashTime() / 2;
if (time > 0) {
buffer_view_->buffer().workAreaManager().remove(p);
} catch(...) {}
delete buffer_view_;
- delete caret_;
// Completer has a QObject parent and is thus automatically destroyed.
// See #4758.
// delete completer_;
// With Qt4.5 a mouse event will happen before the first paint event
// so make sure that the buffer view has an up to date metrics.
d->buffer_view_->resize(viewport()->width(), viewport()->height());
- d->caret_ = new frontend::CaretWidget();
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setAcceptDrops(true);
buffer.workAreaManager().add(this);
// HACK: Prevents an additional redraw when the scrollbar pops up
- // which regularily happens on documents with more than one page.
+ // which regularly happens on documents with more than one page.
// The policy should be set to "Qt::ScrollBarAsNeeded" soon.
// Since we have no geometry information yet, we assume that
// a document needs a scrollbar if there is more then four
void GuiWorkArea::setFullScreen(bool full_screen)
{
d->buffer_view_->setFullScreen(full_screen);
- setFrameStyle(QFrame::NoFrame);
- if (full_screen) {
- setFrameStyle(QFrame::NoFrame);
- if (lyxrc.full_screen_scrollbar)
- setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- } else
+ if (full_screen && lyxrc.full_screen_scrollbar)
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ else
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
}
if (view().busy())
return;
- // Don't start blinking if the cursor isn't on screen.
- if (!d->buffer_view_->caretInView())
+ // Don't start blinking if the cursor isn't on screen, unless we
+ // are not ready to know whether the cursor is on screen.
+ if (!d->buffer_view_->buffer().undo().activeUndoGroup()
+ && !d->buffer_view_->caretInView())
return;
d->showCaret();
// update caret position, because otherwise it has to wait until
// the blinking interval is over
- d->updateCaretGeometry();
+ d->resetCaret();
LYXERR(Debug::WORKAREA, "WorkArea::redraw screen");
viewport()->update();
buffer_view_->resize(p->viewport()->width(), p->viewport()->height());
if (caret_in_view)
buffer_view_->scrollToCursor();
- updateCaretGeometry();
+ resetCaret();
// Update scrollbars which might have changed due different
// BufferView dimension. This is especially important when the
// BufferView goes from zero-size to the real-size for the first time,
- // as the scrollbar paramters are then set for the first time.
+ // as the scrollbar parameters are then set for the first time.
updateScrollbar();
need_resize_ = false;
}
-void GuiWorkArea::Private::updateCaretGeometry()
+void GuiWorkArea::Private::resetCaret()
{
+ // Don't start blinking if the cursor isn't on screen.
if (!buffer_view_->caretInView())
return;
- Point point;
- int h = 0;
- buffer_view_->caretPosAndHeight(point, h);
-
- // RTL or not RTL
- bool l_shape = false;
- Font const & realfont = buffer_view_->cursor().real_current_font;
- BufferParams const & bp = buffer_view_->buffer().params();
- bool const samelang = realfont.language() == bp.language;
- bool const isrtl = realfont.isVisibleRightToLeft();
-
- if (!samelang || isrtl != bp.language->rightToLeft())
- l_shape = true;
-
- // The ERT language hack needs fixing up
- if (realfont.language() == latex_language)
- l_shape = false;
-
- // show caret on screen
- Cursor & cur = buffer_view_->cursor();
- bool completable = cur.inset().showCompletionCursor()
+ // completion indicator
+ Cursor const & cur = buffer_view_->cursor();
+ bool const completable = cur.inset().showCompletionCursor()
&& completer_->completionAvailable()
&& !completer_->popupVisible()
&& !completer_->inlineVisible();
+
+ buffer_view_->buildCaretGeometry(completable);
+
+ needs_caret_geometry_update_ = true;
caret_visible_ = true;
+}
- //int cur_x = buffer_view_->getPos(cur).x_;
- // We may have decided to slide the cursor row so that caret
- // is visible.
- point.x_ -= buffer_view_->horizScrollOffset();
- caret_->update(point.x_, point.y_, h, l_shape, isrtl, completable);
+void GuiWorkArea::Private::updateCaretGeometry()
+{
+ // we cannot update geometry if not ready and we do not need to if
+ // caret is not in view.
+ if (buffer_view_->buffer().undo().activeUndoGroup()
+ || !buffer_view_->caretInView())
+ return;
+
+
+ needs_caret_geometry_update_ = false;
}
if (caret_visible_)
return;
- updateCaretGeometry();
+ resetCaret();
p->viewport()->update();
}
}
+/* Draw the caret. Parameter \c horiz_offset is not 0 when there
+ * has been horizontal scrolling in current row
+ */
+void GuiWorkArea::Private::drawCaret(QPainter & painter, int horiz_offset) const
+{
+ if (buffer_view_->caretGeometry().shapes.empty())
+ return;
+
+ QColor const color = guiApp->colorCache().get(Color_cursor);
+ painter.setPen(color);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ for (auto const & shape : buffer_view_->caretGeometry().shapes) {
+ bool first = true;
+ QPainterPath path;
+ for (Point const & p : shape) {
+ if (first) {
+ path.moveTo(p.x_ - horiz_offset, p.y_);
+ first = false;
+ } else
+ path.lineTo(p.x_ - horiz_offset, p.y_);
+ }
+ painter.fillPath(path, color);
+ }
+ painter.setRenderHint(QPainter::Antialiasing, false);
+}
+
+
void GuiWorkArea::Private::updateScrollbar()
{
// Prevent setRange() and setSliderPosition from causing recursive calls via
FuncRequest const cmd(LFUN_MOUSE_RELEASE, e->x(), e->y(),
q_button_state(e->button()), q_key_state(e->modifiers()));
+#if (QT_VERSION > QT_VERSION_CHECK(5,10,1) && \
+ QT_VERSION < QT_VERSION_CHECK(5,15,1))
+ d->synthetic_mouse_event_.cmd = cmd; // QtBug QAbstractScrollArea::mouseMoveEvent
+#endif
d->dispatch(cmd);
e->accept();
}
void GuiWorkArea::mouseMoveEvent(QMouseEvent * e)
{
+#if (QT_VERSION > QT_VERSION_CHECK(5,10,1) && \
+ QT_VERSION < QT_VERSION_CHECK(5,15,1))
+ // cancel the event if the coordinates didn't change, this is due to QtBug
+ // QAbstractScrollArea::mouseMoveEvent, the event is triggered falsely when quickly
+ // double tapping a touchpad. To test: try to select a word by quickly double tapping
+ // on a touchpad while hovering the cursor over that word in the work area.
+ // This bug does not occur on Qt versions 5.10.1 and below. Only Windows seems to be affected.
+ // ML thread: https://www.mail-archive.com/lyx-devel@lists.lyx.org/msg211699.html
+ // Qt bugtracker: https://bugreports.qt.io/browse/QTBUG-85431
+ // Bug was fixed in Qt 5.15.1
+ if (e->x() == d->synthetic_mouse_event_.cmd.x() && // QtBug QAbstractScrollArea::mouseMoveEvent
+ e->y() == d->synthetic_mouse_event_.cmd.y()) // QtBug QAbstractScrollArea::mouseMoveEvent
+ return; // QtBug QAbstractScrollArea::mouseMoveEvent
+#endif
+
// we kill the triple click if we move
doubleClickTimeout();
FuncRequest cmd(LFUN_MOUSE_MOTION, e->x(), e->y(),
cur.boundary(bound);
d->buffer_view_->buffer().changed(false);
- return;
}
// FIXME: shall we use real_current_font here? (see #10478)
FontInfo const font = buffer_view_->cursor().getFont().fontInfo();
FontMetrics const & fm = theFontMetrics(font);
- int const height = fm.maxHeight();
- int cur_x = caret_->rect().left();
- int cur_y = caret_->rect().bottom();
+ Point point;
+ Dimension dim;
+ buffer_view_->caretPosAndDim(point, dim);
+ int cur_x = point.x_;
+ int cur_y = point.y_ + dim.height();
// get attributes of input method cursor.
// cursor_pos : cursor position in preedit string.
bool cursor_is_visible = false;
for (auto const & attr : preedit_attr_) {
if (attr.type == QInputMethodEvent::Cursor) {
- cursor_pos = attr.start;
+ cursor_pos = size_t(attr.start);
cursor_is_visible = attr.length != 0;
break;
}
size_t const preedit_length = preedit_string_.length();
// get position of selection in input method.
- // FIXME: isn't there a way to do this simplier?
+ // FIXME: isn't there a simpler way to do this?
// rStart : cursor position in selected string in IM.
size_t rStart = 0;
// rLength : selected string length in IM.
if (attr.type == QInputMethodEvent::TextFormat) {
if (attr.start <= int(cursor_pos)
&& int(cursor_pos) < attr.start + attr.length) {
- rStart = attr.start;
- rLength = attr.length;
+ rStart = size_t(attr.start);
+ rLength = size_t(attr.length);
if (!cursor_is_visible)
cursor_pos += rLength;
break;
// if we reached the right extremity of the screen, go to next line.
if (cur_x + fm.width(typed_char) > p->viewport()->width() - right_margin) {
cur_x = right_margin;
- cur_y += height + 1;
+ cur_y += dim.height() + 1;
++preedit_lines_;
}
// preedit strings are displayed with dashed underline
d->last_pixel_ratio_ = pixelRatio();
- GuiPainter pain(d->screenDevice(), pixelRatio());
+ GuiPainter pain(d->screenDevice(), pixelRatio(), d->lyx_view_->develMode());
d->buffer_view_->draw(pain, d->caret_visible_);
d->paintPreeditText(pain);
// and the caret
- if (d->caret_visible_)
- d->caret_->draw(pain);
+ // FIXME: the code would be a little bit simpler if caret geometry
+ // was updated unconditionally. Some profiling is required to see
+ // how expensive this is (especially when idle).
+ if (d->caret_visible_) {
+ if (d->needs_caret_geometry_update_)
+ d->updateCaretGeometry();
+ d->drawCaret(pain, d->buffer_view_->horizScrollOffset());
+ }
d->updateScreen(ev->rect());
FuncRequest::KEYBOARD);
dispatch(cmd);
// FIXME: this is supposed to remove traces from preedit
- // string. Can we avoid calling it explicitely?
+ // string. Can we avoid calling it explicitly?
d->buffer_view_->updateMetrics();
}
// redraw area of preedit string.
- int height = d->caret_->rect().height();
- int cur_y = d->caret_->rect().bottom();
- viewport()->update(0, cur_y - height, viewport()->width(),
- (height + 1) * d->preedit_lines_);
+ // int height = d->caret_->dim.height();
+ // int cur_y = d->caret_->y;
+ // viewport()->update(0, cur_y, viewport()->width(),
+ // (height + 1) * d->preedit_lines_);
+ viewport()->update();
if (d->preedit_string_.empty()) {
d->preedit_lines_ = 1;
QVariant GuiWorkArea::inputMethodQuery(Qt::InputMethodQuery query) const
{
- QRect cur_r(0, 0, 0, 0);
switch (query) {
// this is the CJK-specific composition window position and
// the context menu position when the menu key is pressed.
- case Qt::ImMicroFocus:
- cur_r = d->caret_->rect();
- if (d->preedit_lines_ != 1)
- cur_r.moveLeft(10);
- cur_r.moveBottom(cur_r.bottom()
- + cur_r.height() * (d->preedit_lines_ - 1));
- // return lower right of caret in LyX.
- return cur_r;
- default:
- return QWidget::inputMethodQuery(query);
+#if (QT_VERSION < 0x050000)
+ case Qt::ImMicroFocus: {
+#else
+ case Qt::ImCursorRectangle: {
+#endif
+ CaretGeometry const & cg = bufferView().caretGeometry();
+ return QRect(cg.left - 10 * (d->preedit_lines_ != 1),
+ cg.top + cg.height() * d->preedit_lines_,
+ cg.width(), cg.height());
+ }
+ default:
+ return QWidget::inputMethodQuery(query);
}
}
void TabWorkArea::mousePressEvent(QMouseEvent *me)
{
- if (me->button() == Qt::MidButton)
+ if (me->button() == Qt::MiddleButton)
midpressed_tab_ = tabBar()->tabAt(me->pos());
else
QTabWidget::mousePressEvent(me);
void TabWorkArea::mouseReleaseEvent(QMouseEvent *me)
{
- if (me->button() == Qt::MidButton) {
+ if (me->button() == Qt::MiddleButton) {
int const midreleased_tab = tabBar()->tabAt(me->pos());
if (midpressed_tab_ == midreleased_tab && posIsTab(me->pos()))
closeTab(midreleased_tab);
// showing the same buffer.
for (int i = 0; i != count(); ++i) {
GuiWorkArea * wa = workArea(i);
- LASSERT(wa, return 0);
+ LASSERT(wa, return nullptr);
if (&wa->bufferView().buffer() == &buffer)
return wa;
}
- return 0;
+ return nullptr;
}
void TabWorkArea::updateTabTexts()
{
- size_t n = count();
+ int const n = count();
if (n == 0)
return;
std::list<DisplayPath> paths;
// collect full names first: path into postfix, empty prefix and
// filename without extension
- for (size_t i = 0; i < n; ++i) {
+ for (int i = 0; i < n; ++i) {
GuiWorkArea * i_wa = workArea(i);
FileName const fn = i_wa->bufferView().buffer().fileName();
paths.push_back(DisplayPath(i, fn));
}
-void GuiWorkAreaContainer::dispatch(FuncRequest f) const
+void GuiWorkAreaContainer::dispatch(FuncRequest const & f) const
{
lyx::dispatch(FuncRequest(LFUN_BUFFER_SWITCH,
wa_->bufferView().buffer().absFileName()));