]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiWorkArea.C
simplify exit code, old code doesn't work on the Mac
[lyx.git] / src / frontends / qt4 / GuiWorkArea.C
1 /**
2  * \file GuiWorkArea.C
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 "QLyXKeySym.h"
19 #include "qt_helpers.h"
20
21 #include "gettext.h"
22 #include "LyXView.h"
23
24 #include "BufferView.h"
25 #include "rowpainter.h"
26 #include "debug.h"
27 #include "funcrequest.h"
28 #include "LColor.h"
29 #include "version.h"
30 #include "lyxrc.h"
31
32 #include "support/filetools.h" // LibFileSearch
33 #include "support/os.h"
34
35 #include "graphics/GraphicsImage.h"
36 #include "graphics/GraphicsLoader.h"
37
38 #include <QLayout>
39 #include <QMainWindow>
40 #include <QMimeData>
41 #include <QUrl>
42 #include <QDragEnterEvent>
43 #include <QPainter>
44 #include <QScrollBar>
45
46 #include <boost/bind.hpp>
47 #include <boost/current_function.hpp>
48
49 // Abdel (26/06/2006):
50 // On windows-XP the UserGuide PageDown scroll test is faster without event pruning (16 s)
51 // than with it (23 s).
52 #ifdef Q_WS_WIN
53 int const CursorWidth = 2;
54  #define USE_EVENT_PRUNING 0
55 #else
56 int const CursorWidth = 1;
57  #define USE_EVENT_PRUNING 0
58 #endif
59
60
61 using std::endl;
62 using std::string;
63
64 namespace os = lyx::support::os;
65
66
67 namespace lyx {
68
69 /// return the LyX key state from Qt's
70 static key_modifier::state q_key_state(Qt::KeyboardModifiers state)
71 {
72         key_modifier::state k = key_modifier::none;
73         if (state & Qt::ControlModifier)
74                 k |= key_modifier::ctrl;
75         if (state & Qt::ShiftModifier)
76                 k |= key_modifier::shift;
77         if (state & Qt::AltModifier || state & Qt::MetaModifier)
78                 k |= key_modifier::alt;
79         return k;
80 }
81
82
83 /// return the LyX mouse button state from Qt's
84 static mouse_button::state q_button_state(Qt::MouseButton button)
85 {
86         mouse_button::state b = mouse_button::none;
87         switch (button) {
88                 case Qt::LeftButton:
89                         b = mouse_button::button1;
90                         break;
91                 case Qt::MidButton:
92                         b = mouse_button::button2;
93                         break;
94                 case Qt::RightButton:
95                         b = mouse_button::button3;
96                         break;
97                 default:
98                         break;
99         }
100         return b;
101 }
102
103
104 /// return the LyX mouse button state from Qt's
105 mouse_button::state q_motion_state(Qt::MouseButtons state)
106 {
107         mouse_button::state b = mouse_button::none;
108         if (state & Qt::LeftButton)
109                 b |= mouse_button::button1;
110         if (state & Qt::MidButton)
111                 b |= mouse_button::button2;
112         if (state & Qt::RightButton)
113                 b |= mouse_button::button3;
114         return b;
115 }
116
117
118 namespace frontend {
119
120 class CursorWidget {
121 public:
122         CursorWidget() {}
123
124         void draw(QPainter & painter)
125         {
126                 // FIXME: do something depending on the cursor shape.
127                 if (show_ && rect_.isValid())
128                         painter.fillRect(rect_, color_);
129         }
130
131         void update(int x, int y, int h, CursorShape shape)
132         {
133                 color_ = guiApp->colorCache().get(LColor::cursor);
134                 rect_ = QRect(x, y, CursorWidth, h);
135                 shape_ = shape;
136         }
137
138         void show(bool set_show = true) { show_ = set_show; }
139         void hide() { show_ = false; }
140
141         QRect const & rect() { return rect_; }
142
143 private:
144         ///
145         CursorShape shape_;
146         ///
147         bool show_;
148         ///
149         QColor color_;
150         ///
151         QRect rect_;
152 };
153
154
155 // This is a 'heartbeat' generating synthetic mouse move events when the
156 // cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s
157 SyntheticMouseEvent::SyntheticMouseEvent()
158         : timeout(200), restart_timeout(true),
159           x_old(-1), y_old(-1), scrollbar_value_old(-1.0)
160 {}
161
162
163 GuiWorkArea::GuiWorkArea(int w, int h, int id, LyXView & lyx_view)
164         : WorkArea(id, lyx_view)
165 {
166         cursor_ = new frontend::CursorWidget();
167         cursor_->hide();
168
169         setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
170         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
171         setAcceptDrops(true);
172         setMinimumSize(100, 70);
173
174         viewport()->setAutoFillBackground(false);
175         viewport()->setAttribute(Qt::WA_OpaquePaintEvent);
176         setFocusPolicy(Qt::WheelFocus);
177
178         viewport()->setCursor(Qt::IBeamCursor);
179
180         resize(w, h);
181
182         synthetic_mouse_event_.timeout.timeout.connect(
183                 boost::bind(&GuiWorkArea::generateSyntheticMouseEvent,
184                             this));
185
186         // Initialize the vertical Scroll Bar
187         QObject::connect(verticalScrollBar(), SIGNAL(actionTriggered(int)),
188                 this, SLOT(adjustViewWithScrollBar(int)));
189
190         // PageStep only depends on the viewport height.
191         verticalScrollBar()->setPageStep(viewport()->height());
192
193         lyxerr[Debug::GUI] << BOOST_CURRENT_FUNCTION
194                 << "\n Area width\t" << width()
195                 << "\n Area height\t" << height()
196                 << "\n viewport width\t" << viewport()->width()
197                 << "\n viewport height\t" << viewport()->height()
198                 << endl;
199
200         if (USE_EVENT_PRUNING) {
201                 // This is the keyboard buffering stuff...
202                 // I don't see any need for this under windows. The keyboard is reactive
203                 // enough...
204
205                 if ( !QObject::connect(&step_timer_, SIGNAL(timeout()),
206                         this, SLOT(keyeventTimeout())) )
207                         lyxerr[Debug::GUI] << "ERROR: keyeventTimeout cannot connect!" << endl;
208
209                 // Start the timer, one-shot.
210                 step_timer_.setSingleShot(true);
211                 step_timer_.start(50);
212         }
213
214         // Enables input methods for asian languages.
215         // Must be set when creating custom text editing widgets.
216         setAttribute(Qt::WA_InputMethodEnabled, true);
217 }
218
219
220 void GuiWorkArea::setScrollbarParams(int h, int scroll_pos, int scroll_line_step)
221 {
222         if (verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOn)
223                 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
224
225         verticalScrollBar()->setTracking(false);
226
227         // do what cursor movement does (some grey)
228         h += height() / 4;
229         int scroll_max_ = std::max(0, h - height());
230
231         verticalScrollBar()->setRange(0, scroll_max_);
232         verticalScrollBar()->setSliderPosition(scroll_pos);
233         verticalScrollBar()->setSingleStep(scroll_line_step);
234         verticalScrollBar()->setValue(scroll_pos);
235
236         verticalScrollBar()->setTracking(true);
237 }
238
239
240 void GuiWorkArea::adjustViewWithScrollBar(int)
241 {
242         scrollBufferView(verticalScrollBar()->sliderPosition());
243 }
244
245
246 void GuiWorkArea::dragEnterEvent(QDragEnterEvent * event)
247 {
248         if (event->mimeData()->hasUrls())
249                 event->accept();
250         /// \todo Ask lyx-devel is this is enough:
251         /// if (event->mimeData()->hasFormat("text/plain"))
252         ///     event->acceptProposedAction();
253 }
254
255
256 void GuiWorkArea::dropEvent(QDropEvent* event)
257 {
258         QList<QUrl> files = event->mimeData()->urls();
259         if (files.isEmpty())
260                 return;
261
262         lyxerr[Debug::GUI] << "GuiWorkArea::dropEvent: got URIs!" << endl;
263         for (int i = 0; i!=files.size(); ++i) {
264                 string const file = os::internal_path(fromqstr(files.at(i).toLocalFile()));
265                 if (!file.empty())
266                         dispatch(FuncRequest(LFUN_FILE_OPEN, file));
267         }
268 }
269
270
271 void GuiWorkArea::focusInEvent(QFocusEvent * /*event*/)
272 {
273         // No need to do anything if we didn't change views...
274         if (theApp == 0 || &lyx_view_ == &theApp->currentView())
275                 return;
276
277         theApp->setCurrentView(lyx_view_);
278
279         // FIXME: it would be better to send a signal "newBuffer()"
280         // in BufferList that could be connected to the different tabbars.
281         lyx_view_.updateTab();
282
283         startBlinkingCursor();
284
285         //FIXME: Use case: Two windows share the same buffer.
286         // The first window is resize. This modify the inner Buffer
287         // structure because Paragraph has a notion of line break and
288         // thus line width (this is very bad!).
289         // When switching to the other window which does not have the
290         // same size, LyX crashes because the line break is not adapted
291         // the this BufferView width.
292         // The following line fix the crash by resizing the BufferView 
293         // on a focusInEvent(). That is not a good fix but it is a fix
294         // nevertheless. The bad side effect is that when the two
295         // BufferViews show the same portion of the Buffer, the second 
296         // BufferView will show the same line breaks as the first one;
297         // even though those line breaks are not adapted to the second
298         // BufferView width... such is life!
299         resizeBufferView();
300 }
301
302
303 void GuiWorkArea::focusOutEvent(QFocusEvent * /*event*/)
304 {
305         // No need to do anything if we didn't change views...
306         if (&lyx_view_ == &theApp->currentView())
307                 return;
308
309         stopBlinkingCursor();
310 }
311
312
313 void GuiWorkArea::mousePressEvent(QMouseEvent * e)
314 {
315         if (dc_event_.active && dc_event_ == *e) {
316                 dc_event_.active = false;
317                 FuncRequest cmd(LFUN_MOUSE_TRIPLE,
318                         dc_event_.x, dc_event_.y,
319                         q_button_state(dc_event_.state));
320                 dispatch(cmd);
321                 return;
322         }
323
324         FuncRequest const cmd(LFUN_MOUSE_PRESS, e->x(), e->y(),
325                               q_button_state(e->button()));
326         dispatch(cmd);
327 }
328
329
330 void GuiWorkArea::mouseReleaseEvent(QMouseEvent * e)
331 {
332         if (synthetic_mouse_event_.timeout.running())
333                 synthetic_mouse_event_.timeout.stop();
334
335         FuncRequest const cmd(LFUN_MOUSE_RELEASE, e->x(), e->y(),
336                               q_button_state(e->button()));
337         dispatch(cmd);
338 }
339
340
341 void GuiWorkArea::mouseMoveEvent(QMouseEvent * e)
342 {
343         FuncRequest cmd(LFUN_MOUSE_MOTION, e->x(), e->y(),
344                               q_motion_state(e->buttons()));
345
346         // If we're above or below the work area...
347         if (e->y() <= 20 || e->y() >= viewport()->height() - 20) {
348                 // Make sure only a synthetic event can cause a page scroll,
349                 // so they come at a steady rate:
350                 if (e->y() <= 20)
351                         // _Force_ a scroll up:
352                         cmd.y = -40;
353                 else
354                         cmd.y = viewport()->height();
355                 // Store the event, to be handled when the timeout expires.
356                 synthetic_mouse_event_.cmd = cmd;
357
358                 if (synthetic_mouse_event_.timeout.running())
359                         // Discard the event. Note that it _may_ be handled
360                         // when the timeout expires if
361                         // synthetic_mouse_event_.cmd has not been overwritten.
362                         // Ie, when the timeout expires, we handle the
363                         // most recent event but discard all others that
364                         // occurred after the one used to start the timeout
365                         // in the first place.
366                         return;
367                 else {
368                         synthetic_mouse_event_.restart_timeout = true;
369                         synthetic_mouse_event_.timeout.start();
370                         // Fall through to handle this event...
371                 }
372
373         } else if (synthetic_mouse_event_.timeout.running()) {
374                 // Store the event, to be possibly handled when the timeout
375                 // expires.
376                 // Once the timeout has expired, normal control is returned
377                 // to mouseMoveEvent (restart_timeout = false).
378                 // This results in a much smoother 'feel' when moving the
379                 // mouse back into the work area.
380                 synthetic_mouse_event_.cmd = cmd;
381                 synthetic_mouse_event_.restart_timeout = false;
382                 return;
383         }
384
385         // Has anything changed on-screen since the last QMouseEvent
386         // was received?
387         double const scrollbar_value = verticalScrollBar()->value();
388         if (e->x() != synthetic_mouse_event_.x_old ||
389             e->y() != synthetic_mouse_event_.y_old ||
390             scrollbar_value != synthetic_mouse_event_.scrollbar_value_old) {
391                 // Yes it has. Store the params used to check this.
392                 synthetic_mouse_event_.x_old = e->x();
393                 synthetic_mouse_event_.y_old = e->y();
394                 synthetic_mouse_event_.scrollbar_value_old = scrollbar_value;
395
396                 // ... and dispatch the event to the LyX core.
397                 dispatch(cmd);
398         }
399 }
400
401
402 void GuiWorkArea::wheelEvent(QWheelEvent * e)
403 {
404         // Wheel rotation by one notch results in a delta() of 120 (see
405         // documentation of QWheelEvent)
406         int const lines = qApp->wheelScrollLines() * e->delta() / 120;
407         verticalScrollBar()->setValue(verticalScrollBar()->value() -
408                         lines *  verticalScrollBar()->singleStep());
409         adjustViewWithScrollBar();
410 }
411
412
413 void GuiWorkArea::generateSyntheticMouseEvent()
414 {
415 // Set things off to generate the _next_ 'pseudo' event.
416         if (synthetic_mouse_event_.restart_timeout)
417                 synthetic_mouse_event_.timeout.start();
418
419         // Has anything changed on-screen since the last timeout signal
420         // was received?
421         double const scrollbar_value = verticalScrollBar()->value();
422         if (scrollbar_value != synthetic_mouse_event_.scrollbar_value_old) {
423                 // Yes it has. Store the params used to check this.
424                 synthetic_mouse_event_.scrollbar_value_old = scrollbar_value;
425
426                 // ... and dispatch the event to the LyX core.
427                 dispatch(synthetic_mouse_event_.cmd);
428         }
429 }
430
431
432 void GuiWorkArea::keyPressEvent(QKeyEvent * e)
433 {
434         lyxerr[Debug::KEY] << BOOST_CURRENT_FUNCTION
435                 << " count=" << e->count()
436                 << " text=" << fromqstr(e->text())
437                 << " isAutoRepeat=" << e->isAutoRepeat()
438                 << " key=" << e->key()
439                 << endl;
440
441         if (USE_EVENT_PRUNING) {
442                 keyeventQueue_.push(boost::shared_ptr<QKeyEvent>(new QKeyEvent(*e)));
443         }
444         else {
445                 boost::shared_ptr<QLyXKeySym> sym(new QLyXKeySym);
446                 sym->set(e);
447                 processKeySym(sym, q_key_state(e->modifiers()));
448         }
449 }
450
451
452 // This is used only if USE_EVENT_PRUNING is defined...
453 void GuiWorkArea::keyeventTimeout()
454 {
455         bool handle_autos = true;
456
457         while (!keyeventQueue_.empty()) {
458                 boost::shared_ptr<QKeyEvent> ev = keyeventQueue_.front();
459
460                 // We never handle more than one auto repeated
461                 // char in a list of queued up events.
462                 if (!handle_autos && ev->isAutoRepeat()) {
463                         keyeventQueue_.pop();
464                         continue;
465                 }
466
467                 boost::shared_ptr<QLyXKeySym> sym(new QLyXKeySym);
468                 sym->set(ev.get());
469
470                 lyxerr[Debug::GUI] << BOOST_CURRENT_FUNCTION
471                                    << " count=" << ev->count()
472                                    << " text=" <<  fromqstr(ev->text())
473                                    << " isAutoRepeat=" << ev->isAutoRepeat()
474                                    << " key=" << ev->key()
475                                    << endl;
476
477                 processKeySym(sym, q_key_state(ev->modifiers()));
478                 keyeventQueue_.pop();
479
480                 handle_autos = false;
481         }
482
483         // Restart the timer.
484         step_timer_.setSingleShot(true);
485         step_timer_.start(25);
486 }
487
488
489 void GuiWorkArea::mouseDoubleClickEvent(QMouseEvent * e)
490 {
491         dc_event_ = double_click(e);
492
493         if (!dc_event_.active)
494                 return;
495
496         dc_event_.active = false;
497
498         FuncRequest cmd(LFUN_MOUSE_DOUBLE,
499                 dc_event_.x, dc_event_.y,
500                 q_button_state(dc_event_.state));
501         dispatch(cmd);
502 }
503
504
505 void GuiWorkArea::resizeEvent(QResizeEvent * ev)
506 {
507         stopBlinkingCursor();
508         screen_ = QPixmap(ev->size().width(), ev->size().height());
509         verticalScrollBar()->setPageStep(viewport()->height());
510         QAbstractScrollArea::resizeEvent(ev);
511         resizeBufferView();
512         startBlinkingCursor();
513 }
514
515
516 void GuiWorkArea::update(int x, int y, int w, int h)
517 {
518         viewport()->update(x, y, w, h);
519 }
520
521
522 void GuiWorkArea::doGreyOut(QLPainter & pain)
523 {
524         setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
525
526         pain.fillRectangle(0, 0, width(), height(),
527                 LColor::bottomarea);
528
529         //if (!lyxrc.show_banner)
530         //      return;
531         lyxerr[Debug::GUI] << "show banner: " << lyxrc.show_banner << endl;
532         /// The text to be written on top of the pixmap
533         QString const text = lyx_version ? QString(lyx_version) : qt_("unknown version");
534         string const file = support::libFileSearch("images", "banner", "ppm");
535         if (file.empty())
536                 return;
537
538         QPixmap pm(toqstr(file));
539         if (!pm) {
540                 lyxerr << "could not load splash screen: '" << file << "'" << endl;
541                 return;
542         }
543
544         QFont font;
545         // The font used to display the version info
546         font.setStyleHint(QFont::SansSerif);
547         font.setWeight(QFont::Bold);
548         font.setPointSize(LyXFont::SIZE_NORMAL);
549
550         int const w = pm.width();
551         int const h = pm.height();
552
553         int x = (width() - w) / 2;
554         int y = (height() - h) / 2;
555
556         pain.drawPixmap(x, y, pm);
557
558         x += 260;
559         y += 265;
560
561         pain.setPen(QColor(255, 255, 0));
562         pain.setFont(font);
563         pain.drawText(x, y, text);
564 }
565
566
567 void GuiWorkArea::paintEvent(QPaintEvent * ev)
568 {
569         QRect const rc = ev->rect(); 
570         lyxerr[Debug::PAINTING] << "paintEvent begin: x: " << rc.x()
571                 << " y: " << rc.y()
572                 << " w: " << rc.width()
573                 << " h: " << rc.height() << endl;
574
575         QPainter pain(viewport());
576         pain.drawPixmap(rc, screen_, rc);
577         cursor_->draw(pain);
578 }
579
580
581 void GuiWorkArea::expose(int x, int y, int w, int h)
582 {
583         QLPainter pain(&screen_);
584
585         if (greyed_out_) {
586                 lyxerr[Debug::GUI] << "splash screen requested" << endl;
587                 doGreyOut(pain);
588                 verticalScrollBar()->hide();
589                 update(0, 0, width(), height());
590                 return;
591         }
592
593         verticalScrollBar()->show();
594         paintText(*buffer_view_, pain);
595         update(x, y, w, h);
596 }
597
598
599 void GuiWorkArea::showCursor(int x, int y, int h, CursorShape shape)
600 {
601         cursor_->update(x, y, h, shape);
602         cursor_->show();
603         viewport()->update(cursor_->rect());
604 }
605
606
607 void GuiWorkArea::removeCursor()
608 {
609         cursor_->hide();
610         //if (!qApp->focusWidget())
611                 viewport()->update(cursor_->rect());
612 }
613
614
615 void GuiWorkArea::inputMethodEvent(QInputMethodEvent * e)
616 {
617         QString const & text = e->commitString();
618         if (!text.isEmpty()) {
619
620                 lyxerr[Debug::KEY] << BOOST_CURRENT_FUNCTION
621                         << " preeditString =" << fromqstr(e->preeditString())
622                         << " commitString  =" << fromqstr(e->commitString())
623                         << endl;
624
625                 int key = 0;
626                 // needed to make math superscript work on some systems
627                 // ideally, such special coding should not be necessary
628                 if (text == "^")
629                         key = Qt::Key_AsciiCircum;
630                 // FIXME: Needs for investigation, this key is not really used,
631                 // the ctor below just check if key is different from 0.
632                 QKeyEvent ev(QEvent::KeyPress, key, Qt::NoModifier, text);
633                 keyPressEvent(&ev);
634         }
635         e->accept();
636 }
637
638 } // namespace frontend
639 } // namespace lyx
640
641 #include "GuiWorkArea_moc.cpp"