]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiWorkArea.cpp
header cleanup.
[lyx.git] / src / frontends / qt4 / GuiWorkArea.cpp
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
16 #include "GuiApplication.h"
17 #include "QLPainter.h"
18 #include "QKeySymbol.h"
19 #include "qt_helpers.h"
20
21 #include "frontends/LyXView.h"
22
23 #include "BufferView.h"
24 #include "Color.h"
25 #include "debug.h"
26 #include "FuncRequest.h"
27 #include "LyXRC.h"
28 #include "version.h"
29
30 #include "support/filetools.h" // LibFileSearch
31 #include "support/convert.h"
32
33 #include "graphics/GraphicsImage.h"
34 #include "graphics/GraphicsLoader.h"
35
36 #include <QInputContext>
37 #include <QLayout>
38 #include <QMainWindow>
39 #include <QPainter>
40 #include <QScrollBar>
41 #include <QTimer>
42
43 #include <boost/bind.hpp>
44 #include <boost/current_function.hpp>
45
46 #ifdef Q_WS_X11
47 #include <QX11Info>
48 extern "C" int XEventsQueued(Display *display, int mode);
49 #endif
50
51 #ifdef Q_WS_WIN
52 int const CursorWidth = 2;
53 #else
54 int const CursorWidth = 1;
55 #endif
56
57 #undef KeyPress
58 #undef NoModifier 
59
60 using std::endl;
61 using std::string;
62
63 namespace lyx {
64
65 using support::FileName;
66
67 /// return the LyX mouse button state from Qt's
68 static mouse_button::state q_button_state(Qt::MouseButton button)
69 {
70         mouse_button::state b = mouse_button::none;
71         switch (button) {
72                 case Qt::LeftButton:
73                         b = mouse_button::button1;
74                         break;
75                 case Qt::MidButton:
76                         b = mouse_button::button2;
77                         break;
78                 case Qt::RightButton:
79                         b = mouse_button::button3;
80                         break;
81                 default:
82                         break;
83         }
84         return b;
85 }
86
87
88 /// return the LyX mouse button state from Qt's
89 mouse_button::state q_motion_state(Qt::MouseButtons state)
90 {
91         mouse_button::state b = mouse_button::none;
92         if (state & Qt::LeftButton)
93                 b |= mouse_button::button1;
94         if (state & Qt::MidButton)
95                 b |= mouse_button::button2;
96         if (state & Qt::RightButton)
97                 b |= mouse_button::button3;
98         return b;
99 }
100
101
102 namespace frontend {
103
104 class CursorWidget {
105 public:
106         CursorWidget() {}
107
108         void draw(QPainter & painter)
109         {
110                 if (show_ && rect_.isValid()) {
111                         switch (shape_) {
112                         case L_SHAPE:
113                                 painter.fillRect(rect_.x(), rect_.y(), CursorWidth, rect_.height(), color_);
114                                 painter.setPen(color_);
115                                 painter.drawLine(rect_.bottomLeft().x() + CursorWidth, rect_.bottomLeft().y(),
116                                                                                                  rect_.bottomRight().x(), rect_.bottomLeft().y());
117                                 break;
118                         
119                         case REVERSED_L_SHAPE:
120                                 painter.fillRect(rect_.x() + rect_.height() / 3, rect_.y(), CursorWidth, rect_.height(), color_);
121                                 painter.setPen(color_);
122                                 painter.drawLine(rect_.bottomRight().x() - CursorWidth, rect_.bottomLeft().y(),
123                                                                                                          rect_.bottomLeft().x(), rect_.bottomLeft().y());
124                                 break;
125                                         
126                         default:
127                                 painter.fillRect(rect_, color_);
128                                 break;
129                         }
130                 }
131         }
132
133         void update(int x, int y, int h, CursorShape shape)
134         {
135                 color_ = guiApp->colorCache().get(Color::cursor);
136                 shape_ = shape;
137                 switch (shape) {
138                 case L_SHAPE:
139                         rect_ = QRect(x, y, CursorWidth + h / 3, h);
140                         break;
141                 case REVERSED_L_SHAPE:
142                         rect_ = QRect(x - h / 3, y, CursorWidth + h / 3, h);
143                         break;
144                 default: 
145                         rect_ = QRect(x, y, CursorWidth, h);
146                         break;
147                 }
148         }
149
150         void show(bool set_show = true) { show_ = set_show; }
151         void hide() { show_ = false; }
152
153         QRect const & rect() { return rect_; }
154
155 private:
156         ///
157         CursorShape shape_;
158         ///
159         bool show_;
160         ///
161         QColor color_;
162         ///
163         QRect rect_;
164 };
165
166
167 // This is a 'heartbeat' generating synthetic mouse move events when the
168 // cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s
169 SyntheticMouseEvent::SyntheticMouseEvent()
170         : timeout(200), restart_timeout(true),
171           x_old(-1), y_old(-1), scrollbar_value_old(-1.0)
172 {}
173
174
175 GuiWorkArea::GuiWorkArea(Buffer & buf, LyXView & lv)
176         : WorkArea(buf, lv), need_resize_(false), schedule_redraw_(false),
177           preedit_lines_(1)
178 {
179         screen_ = QPixmap(viewport()->width(), viewport()->height());
180         cursor_ = new frontend::CursorWidget();
181         cursor_->hide();
182
183         setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
184         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
185         setAcceptDrops(true);
186         setMouseTracking(true);
187         setMinimumSize(100, 70);
188
189         viewport()->setAutoFillBackground(false);
190         // We don't need double-buffering nor SystemBackground on
191         // the viewport because we have our own backing pixmap.
192         viewport()->setAttribute(Qt::WA_NoSystemBackground);
193
194         setFocusPolicy(Qt::WheelFocus);
195
196         viewport()->setCursor(Qt::IBeamCursor);
197
198         synthetic_mouse_event_.timeout.timeout.connect(
199                 boost::bind(&GuiWorkArea::generateSyntheticMouseEvent,
200                             this));
201
202         // Initialize the vertical Scroll Bar
203         QObject::connect(verticalScrollBar(), SIGNAL(actionTriggered(int)),
204                 this, SLOT(adjustViewWithScrollBar(int)));
205
206         // disable context menu for the scrollbar
207         verticalScrollBar()->setContextMenuPolicy(Qt::NoContextMenu);
208
209         // PageStep only depends on the viewport height.
210         verticalScrollBar()->setPageStep(viewport()->height());
211
212         LYXERR(Debug::GUI) << BOOST_CURRENT_FUNCTION
213                 << "\n Area width\t" << width()
214                 << "\n Area height\t" << height()
215                 << "\n viewport width\t" << viewport()->width()
216                 << "\n viewport height\t" << viewport()->height()
217                 << endl;
218
219         // Enables input methods for asian languages.
220         // Must be set when creating custom text editing widgets.
221         setAttribute(Qt::WA_InputMethodEnabled, true);
222 }
223
224
225 void GuiWorkArea::setScrollbarParams(int h, int scroll_pos, int scroll_line_step)
226 {
227         if (verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOn)
228                 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
229
230         verticalScrollBar()->setTracking(false);
231
232         // do what cursor movement does (some grey)
233         h += height() / 4;
234         int scroll_max_ = std::max(0, h - height());
235
236         verticalScrollBar()->setRange(0, scroll_max_);
237         verticalScrollBar()->setSliderPosition(scroll_pos);
238         verticalScrollBar()->setSingleStep(scroll_line_step);
239         verticalScrollBar()->setValue(scroll_pos);
240
241         verticalScrollBar()->setTracking(true);
242 }
243
244
245 void GuiWorkArea::adjustViewWithScrollBar(int)
246 {
247         scrollBufferView(verticalScrollBar()->sliderPosition());
248         QApplication::syncX();
249 }
250
251
252 void GuiWorkArea::focusInEvent(QFocusEvent * /*event*/)
253 {
254         // No need to do anything if we didn't change views...
255 //      if (theApp() == 0 || &lyx_view_ == theApp()->currentView())
256 //              return;
257
258         theApp()->setCurrentView(*lyx_view_);
259
260         // Repaint the whole screen.
261         // Note: this is different from redraw() as only the backing pixmap
262         // will be redrawn, which is cheap.
263         viewport()->repaint();
264
265         startBlinkingCursor();
266 }
267
268
269 void GuiWorkArea::focusOutEvent(QFocusEvent * /*event*/)
270 {
271         stopBlinkingCursor();
272 }
273
274
275 void GuiWorkArea::mousePressEvent(QMouseEvent * e)
276 {
277         if (dc_event_.active && dc_event_ == *e) {
278                 dc_event_.active = false;
279                 FuncRequest cmd(LFUN_MOUSE_TRIPLE,
280                         e->x(), e->y(),
281                         q_button_state(e->button()));
282                 dispatch(cmd);
283                 return;
284         }
285
286         inputContext()->reset();
287
288         FuncRequest const cmd(LFUN_MOUSE_PRESS, e->x(), e->y(),
289                 q_button_state(e->button()));
290         dispatch(cmd, q_key_state(e->modifiers()));
291 }
292
293
294 void GuiWorkArea::mouseReleaseEvent(QMouseEvent * e)
295 {
296         if (synthetic_mouse_event_.timeout.running())
297                 synthetic_mouse_event_.timeout.stop();
298
299         FuncRequest const cmd(LFUN_MOUSE_RELEASE, e->x(), e->y(),
300                               q_button_state(e->button()));
301         dispatch(cmd);
302 }
303
304
305 void GuiWorkArea::mouseMoveEvent(QMouseEvent * e)
306 {
307         // we kill the triple click if we move
308         doubleClickTimeout();
309         FuncRequest cmd(LFUN_MOUSE_MOTION, e->x(), e->y(),
310                               q_motion_state(e->buttons()));
311
312         // If we're above or below the work area...
313         if (e->y() <= 20 || e->y() >= viewport()->height() - 20) {
314                 // Make sure only a synthetic event can cause a page scroll,
315                 // so they come at a steady rate:
316                 if (e->y() <= 20)
317                         // _Force_ a scroll up:
318                         cmd.y = -40;
319                 else
320                         cmd.y = viewport()->height();
321                 // Store the event, to be handled when the timeout expires.
322                 synthetic_mouse_event_.cmd = cmd;
323
324                 if (synthetic_mouse_event_.timeout.running())
325                         // Discard the event. Note that it _may_ be handled
326                         // when the timeout expires if
327                         // synthetic_mouse_event_.cmd has not been overwritten.
328                         // Ie, when the timeout expires, we handle the
329                         // most recent event but discard all others that
330                         // occurred after the one used to start the timeout
331                         // in the first place.
332                         return;
333                 else {
334                         synthetic_mouse_event_.restart_timeout = true;
335                         synthetic_mouse_event_.timeout.start();
336                         // Fall through to handle this event...
337                 }
338
339         } else if (synthetic_mouse_event_.timeout.running()) {
340                 // Store the event, to be possibly handled when the timeout
341                 // expires.
342                 // Once the timeout has expired, normal control is returned
343                 // to mouseMoveEvent (restart_timeout = false).
344                 // This results in a much smoother 'feel' when moving the
345                 // mouse back into the work area.
346                 synthetic_mouse_event_.cmd = cmd;
347                 synthetic_mouse_event_.restart_timeout = false;
348                 return;
349         }
350
351         // Has anything changed on-screen since the last QMouseEvent
352         // was received?
353         double const scrollbar_value = verticalScrollBar()->value();
354         if (e->x() != synthetic_mouse_event_.x_old ||
355             e->y() != synthetic_mouse_event_.y_old ||
356             scrollbar_value != synthetic_mouse_event_.scrollbar_value_old) {
357                 // Yes it has. Store the params used to check this.
358                 synthetic_mouse_event_.x_old = e->x();
359                 synthetic_mouse_event_.y_old = e->y();
360                 synthetic_mouse_event_.scrollbar_value_old = scrollbar_value;
361
362                 // ... and dispatch the event to the LyX core.
363                 dispatch(cmd);
364         }
365 }
366
367
368 void GuiWorkArea::wheelEvent(QWheelEvent * e)
369 {
370         // Wheel rotation by one notch results in a delta() of 120 (see
371         // documentation of QWheelEvent)
372         int const lines = qApp->wheelScrollLines() * e->delta() / 120;
373         verticalScrollBar()->setValue(verticalScrollBar()->value() -
374                         lines *  verticalScrollBar()->singleStep());
375         adjustViewWithScrollBar();
376 }
377
378
379 void GuiWorkArea::generateSyntheticMouseEvent()
380 {
381 // Set things off to generate the _next_ 'pseudo' event.
382         if (synthetic_mouse_event_.restart_timeout)
383                 synthetic_mouse_event_.timeout.start();
384
385         // Has anything changed on-screen since the last timeout signal
386         // was received?
387         double const scrollbar_value = verticalScrollBar()->value();
388         if (scrollbar_value != synthetic_mouse_event_.scrollbar_value_old) {
389                 // Yes it has. Store the params used to check this.
390                 synthetic_mouse_event_.scrollbar_value_old = scrollbar_value;
391
392                 // ... and dispatch the event to the LyX core.
393                 dispatch(synthetic_mouse_event_.cmd);
394         }
395 }
396
397
398 void GuiWorkArea::keyPressEvent(QKeyEvent * e)
399 {
400         // do nothing if there are other events
401         // (the auto repeated events come too fast)
402         // \todo FIXME: remove hard coded Qt keys, process the key binding
403 #ifdef Q_WS_X11
404         if (XEventsQueued(QX11Info::display(), 0) > 1 && e->isAutoRepeat() 
405                         && (Qt::Key_PageDown || Qt::Key_PageUp)) {
406                 LYXERR(Debug::KEY)      
407                         << BOOST_CURRENT_FUNCTION << endl
408                         << "system is busy: scroll key event ignored" << endl;
409                 e->ignore();
410                 return;
411         }
412 #endif
413
414         LYXERR(Debug::KEY) << BOOST_CURRENT_FUNCTION
415                 << " count=" << e->count()
416                 << " text=" << fromqstr(e->text())
417                 << " isAutoRepeat=" << e->isAutoRepeat()
418                 << " key=" << e->key()
419                 << endl;
420
421         boost::shared_ptr<QKeySymbol> sym(new QKeySymbol);
422         sym->set(e);
423         processKeySym(sym, q_key_state(e->modifiers()));
424 }
425
426 void GuiWorkArea::doubleClickTimeout() {
427         dc_event_.active = false;
428 }
429
430 void GuiWorkArea::mouseDoubleClickEvent(QMouseEvent * e)
431 {
432         dc_event_ = double_click(e);
433         QTimer::singleShot(QApplication::doubleClickInterval(), this,
434                            SLOT(doubleClickTimeout()));
435         FuncRequest cmd(LFUN_MOUSE_DOUBLE,
436                         e->x(), e->y(),
437                         q_button_state(e->button()));
438         dispatch(cmd);
439 }
440
441
442 void GuiWorkArea::resizeEvent(QResizeEvent * ev)
443 {
444         QAbstractScrollArea::resizeEvent(ev);
445         need_resize_ = true;
446 }
447
448
449 void GuiWorkArea::update(int x, int y, int w, int h)
450 {
451         viewport()->repaint(x, y, w, h);
452 }
453
454
455 void GuiWorkArea::paintEvent(QPaintEvent * ev)
456 {
457         QRect const rc = ev->rect();
458         /*
459         LYXERR(Debug::PAINTING) << "paintEvent begin: x: " << rc.x()
460                 << " y: " << rc.y()
461                 << " w: " << rc.width()
462                 << " h: " << rc.height() << endl;
463         */
464
465         if (need_resize_) {
466                 verticalScrollBar()->setPageStep(viewport()->height());
467                 screen_ = QPixmap(viewport()->width(), viewport()->height());
468                 resizeBufferView();
469                 updateScreen();
470                 WorkArea::hideCursor();
471                 WorkArea::showCursor();
472                 need_resize_ = false;
473         }
474
475         QPainter pain(viewport());
476         pain.drawPixmap(rc, screen_, rc);
477         cursor_->draw(pain);
478 }
479
480
481 void GuiWorkArea::expose(int x, int y, int w, int h)
482 {
483         updateScreen();
484         update(x, y, w, h);
485 }
486
487
488 void GuiWorkArea::updateScreen()
489 {
490         QLPainter pain(&screen_);
491         verticalScrollBar()->show();
492         buffer_view_->draw(pain);
493 }
494
495
496 void GuiWorkArea::showCursor(int x, int y, int h, CursorShape shape)
497 {
498         if (schedule_redraw_) {
499                 buffer_view_->update(Update::Force);
500                 updateScreen();
501                 viewport()->update(QRect(0, 0, viewport()->width(), viewport()->height()));
502                 schedule_redraw_ = false;
503                 // Show the cursor immediately after the update.
504                 hideCursor();
505                 toggleCursor();
506                 return;
507         }
508
509         cursor_->update(x, y, h, shape);
510         cursor_->show();
511         viewport()->update(cursor_->rect());
512 }
513
514
515 void GuiWorkArea::removeCursor()
516 {
517         cursor_->hide();
518         //if (!qApp->focusWidget())
519                 viewport()->update(cursor_->rect());
520 }
521
522
523 void GuiWorkArea::inputMethodEvent(QInputMethodEvent * e)
524 {
525         QString const & commit_string = e->commitString();
526         docstring const & preedit_string
527                 = qstring_to_ucs4(e->preeditString());
528
529         if (!commit_string.isEmpty()) {
530
531                 LYXERR(Debug::KEY) << BOOST_CURRENT_FUNCTION
532                         << " preeditString =" << fromqstr(e->preeditString())
533                         << " commitString  =" << fromqstr(e->commitString())
534                         << endl;
535
536                 int key = 0;
537
538                 // FIXME Iwami 04/01/07: we should take care also of UTF16 surrogates here.
539                 for (int i = 0; i < commit_string.size(); ++i) {
540                         QKeyEvent ev(QEvent::KeyPress, key, Qt::NoModifier, commit_string[i]);
541                         keyPressEvent(&ev);
542                 }
543         }
544
545         // Hide the cursor during the kana-kanji transformation.
546         if (preedit_string.empty())
547                 startBlinkingCursor();
548         else
549                 stopBlinkingCursor();
550
551         // last_width : for checking if last preedit string was/wasn't empty.
552         static bool last_width = false;
553         if (!last_width && preedit_string.empty()) {
554                 // if last_width is last length of preedit string.
555                 e->accept();
556                 return;
557         }
558
559         QLPainter pain(&screen_);
560         buffer_view_->updateMetrics(false);
561         buffer_view_->draw(pain);
562         Font font = buffer_view_->cursor().getFont();
563         FontMetrics const & fm = theFontMetrics(font);
564         int height = fm.maxHeight();
565         int cur_x = cursor_->rect().left();
566         int cur_y = cursor_->rect().bottom();
567
568         // redraw area of preedit string.
569         update(0, cur_y - height, GuiWorkArea::width(),
570                 (height + 1) * preedit_lines_);
571
572         if (preedit_string.empty()) {
573                 last_width = false;
574                 preedit_lines_ = 1;
575                 e->accept();
576                 return;
577         }
578         last_width = true;
579
580         // att : stores an IM attribute.
581         QList<QInputMethodEvent::Attribute> const & att = e->attributes();
582
583         // get attributes of input method cursor.
584         // cursor_pos : cursor position in preedit string.
585         size_t cursor_pos = 0;
586         bool cursor_is_visible = false;
587         for (int i = 0; i < att.size(); ++i) {
588                 if (att.at(i).type == QInputMethodEvent::Cursor) {
589                         cursor_pos = att.at(i).start;
590                         cursor_is_visible = att.at(i).length != 0;
591                         break;
592                 }
593         }
594
595         size_t preedit_length = preedit_string.length();
596
597         // get position of selection in input method.
598         // FIXME: isn't there a way to do this simplier?
599         // rStart : cursor position in selected string in IM.
600         size_t rStart = 0;
601         // rLength : selected string length in IM.
602         size_t rLength = 0;
603         if (cursor_pos < preedit_length) {
604                 for (int i = 0; i < att.size(); ++i) {
605                         if (att.at(i).type == QInputMethodEvent::TextFormat) {
606                                 if (att.at(i).start <= int(cursor_pos)
607                                         && int(cursor_pos) < att.at(i).start + att.at(i).length) {
608                                                 rStart = att.at(i).start;
609                                                 rLength = att.at(i).length;
610                                                 if (!cursor_is_visible)
611                                                         cursor_pos += rLength;
612                                                 break;
613                                 }
614                         }
615                 }
616         }
617         else {
618                 rStart = cursor_pos;
619                 rLength = 0;
620         }
621
622         int const right_margin = rightMargin();
623         Painter::preedit_style ps;
624         // Most often there would be only one line:
625         preedit_lines_ = 1;
626         for (size_t pos = 0; pos != preedit_length; ++pos) {
627                 char_type const typed_char = preedit_string[pos];
628                 // reset preedit string style
629                 ps = Painter::preedit_default;
630
631                 // if we reached the right extremity of the screen, go to next line.
632                 if (cur_x + fm.width(typed_char) > GuiWorkArea::width() - right_margin) {
633                         cur_x = right_margin;
634                         cur_y += height + 1;
635                         ++preedit_lines_;
636                 }
637                 // preedit strings are displayed with dashed underline
638                 // and partial strings are displayed white on black indicating
639                 // that we are in selecting mode in the input method.
640                 // FIXME: rLength == preedit_length is not a changing condition
641                 // FIXME: should be put out of the loop.
642                 if (pos >= rStart
643                         && pos < rStart + rLength
644                         && !(cursor_pos < rLength && rLength == preedit_length))
645                         ps = Painter::preedit_selecting;
646
647                 if (pos == cursor_pos
648                         && (cursor_pos < rLength && rLength == preedit_length))
649                         ps = Painter::preedit_cursor;
650
651                 // draw one character and update cur_x.
652                 cur_x += pain.preeditText(cur_x, cur_y, typed_char, font, ps);
653         }
654
655         // update the preedit string screen area.
656         update(0, cur_y - preedit_lines_*height, GuiWorkArea::width(),
657                 (height + 1) * preedit_lines_);
658
659         // Don't forget to accept the event!
660         e->accept();
661 }
662
663
664 QVariant GuiWorkArea::inputMethodQuery(Qt::InputMethodQuery query) const
665 {
666         QRect cur_r(0,0,0,0);
667         switch (query) {
668                 // this is the CJK-specific composition window position.
669                 case Qt::ImMicroFocus:
670                         cur_r = cursor_->rect();
671                         if (preedit_lines_ != 1)
672                                 cur_r.moveLeft(10);
673                         cur_r.moveBottom(cur_r.bottom() + cur_r.height() * preedit_lines_);
674                         // return lower right of cursor in LyX.
675                         return cur_r;
676                 default:
677                         return QWidget::inputMethodQuery(query);
678         }
679 }
680
681 } // namespace frontend
682 } // namespace lyx
683
684 #include "GuiWorkArea_moc.cpp"