1 /*
2  * This file is part of Licq, an instant messaging client for UNIX.
3  * Copyright (C) 1999-2012 Licq developers <licq-dev@googlegroups.com>
4  *
5  * Licq is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * Licq is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Licq; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  */
19 
20 // written by Graham Roff <graham@licq.org>
21 // contributions by Dirk A. Mueller <dirk@licq.org>
22 
23 #include "mledit.h"
24 
25 #ifndef USE_KDE
26 #include <QAction>
27 #endif
28 #include <QKeyEvent>
29 #include <QMenu>
30 
31 #include "config/general.h"
32 #include "config/shortcuts.h"
33 
34 #ifdef HAVE_HUNSPELL
35 # include "spellchecker.h"
36 #endif
37 
38 
39 using namespace LicqQtGui;
40 /* TRANSLATOR LicqQtGui::MLEdit */
41 
MLEdit(bool wordWrap,QWidget * parent,bool useFixedFont,const char * name)42 MLEdit::MLEdit(bool wordWrap, QWidget* parent, bool useFixedFont, const char* name)
43   : MLEDIT_BASE(parent),
44 #ifdef HAVE_HUNSPELL
45     mySpellChecker(NULL),
46 #endif
47     myUseFixedFont(useFixedFont),
48     myFixSetTextNewlines(true),
49     myLastKeyWasReturn(false),
50     myLinesHint(0)
51 {
52   setObjectName(name);
53   setAcceptRichText(false);
54   setTabChangesFocus(true);
55 
56   if (!wordWrap)
57     setLineWrapMode(NoWrap);
58 
59   updateFont();
60   connect(Config::General::instance(), SIGNAL(fontChanged()), SLOT(updateFont()));
61 }
62 
~MLEdit()63 MLEdit::~MLEdit()
64 {
65   // Empty
66 }
67 
68 #ifndef USE_KDE
setCheckSpellingEnabled(bool check)69 void MLEdit::setCheckSpellingEnabled(bool check)
70 {
71 #ifdef HAVE_HUNSPELL
72   if (check && mySpellChecker == NULL && !mySpellingDictionary.isEmpty())
73     mySpellChecker = new SpellChecker(this->document(), mySpellingDictionary);
74   if (!check && mySpellChecker != NULL)
75     delete mySpellChecker;
76 #else
77   Q_UNUSED(check);
78 #endif
79 }
80 
checkSpellingEnabled() const81 bool MLEdit::checkSpellingEnabled() const
82 {
83 #ifdef HAVE_HUNSPELL
84   return (mySpellChecker != NULL);
85 #else
86   return false;
87 #endif
88 }
89 #endif
90 
91 #ifdef HAVE_HUNSPELL
setSpellingDictionary(const QString & dicFile)92 void MLEdit::setSpellingDictionary(const QString& dicFile)
93 {
94   mySpellingDictionary = dicFile;
95   if (mySpellChecker != NULL)
96     mySpellChecker->setDictionary(dicFile);
97   else
98     setCheckSpellingEnabled(true);
99 }
100 #endif
101 
appendNoNewLine(const QString & s)102 void MLEdit::appendNoNewLine(const QString& s)
103 {
104   GotoEnd();
105   insertPlainText(s);
106 }
107 
GotoEnd()108 void MLEdit::GotoEnd()
109 {
110   moveCursor(QTextCursor::End);
111 }
112 
setBackground(const QColor & color)113 void MLEdit::setBackground(const QColor& color)
114 {
115   QPalette pal = palette();
116 
117   pal.setColor(QPalette::Active, QPalette::Base, color);
118   pal.setColor(QPalette::Inactive, QPalette::Base, color);
119 
120   setPalette(pal);
121 }
122 
setForeground(const QColor & color)123 void MLEdit::setForeground(const QColor& color)
124 {
125   QPalette pal = palette();
126 
127   pal.setColor(QPalette::Active, QPalette::Text, color);
128   pal.setColor(QPalette::Inactive, QPalette::Text, color);
129 
130   setPalette(pal);
131 }
132 
clearKeepUndo()133 void MLEdit::clearKeepUndo()
134 {
135   QTextCursor cr = textCursor();
136   cr.select(QTextCursor::Document);
137   cr.removeSelectedText();
138 }
139 
deleteLine()140 void MLEdit::deleteLine()
141 {
142   QTextCursor cr = textCursor();
143   cr.select(QTextCursor::BlockUnderCursor);
144   if (!cr.hasSelection())
145     cr.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
146   if (!cr.hasSelection())
147     cr.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
148   cr.removeSelectedText();
149 }
150 
deleteLineBackwards()151 void MLEdit::deleteLineBackwards()
152 {
153   QTextCursor cr = textCursor();
154   cr.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
155   if (!cr.hasSelection())
156     cr.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
157   cr.removeSelectedText();
158 }
159 
deleteWordBackwards()160 void MLEdit::deleteWordBackwards()
161 {
162   QTextCursor cr = textCursor();
163   cr.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
164   cr.removeSelectedText();
165 }
166 
keyPressEvent(QKeyEvent * event)167 void MLEdit::keyPressEvent(QKeyEvent* event)
168 {
169   // Get flag from last time and reset it before any possible returns
170   bool lastKeyWasReturn = myLastKeyWasReturn;
171   myLastKeyWasReturn = false;
172 
173   // Ctrl+Return will either trigger dialog or (if disabled) insert a normal line break
174   if (event->modifiers() == Qt::ControlModifier &&
175       (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter))
176   {
177     if (Config::General::instance()->useDoubleReturn())
178       insertPlainText(QString("\n"));
179     else
180       emit ctrlEnterPressed();
181     return;
182   }
183 
184   if (event->modifiers() == Qt::NoModifier)
185   {
186     switch (event->key())
187     {
188       case Qt::Key_Return:
189       case Qt::Key_Enter:
190         if (lastKeyWasReturn && Config::General::instance()->useDoubleReturn())
191         {
192           // Return pressed twice, remove the previous line break and emit signal
193           QTextCursor cr = textCursor();
194           cr.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
195           cr.removeSelectedText();
196           emit ctrlEnterPressed();
197           return;
198         }
199         else
200         {
201           // Return pressed once
202           myLastKeyWasReturn = true;
203         }
204         break;
205       case Qt::Key_Insert:
206         if (overwriteMode())
207         {
208           setOverwriteMode(false);
209           setCursorWidth(1);
210         }
211         else
212         {
213           setOverwriteMode(true);
214           setCursorWidth(2);
215         }
216         break;
217     }
218   }
219 
220   if (event->key() == Qt::Key_PageDown && event->modifiers() == Qt::ShiftModifier)
221   {
222     emit scrollDownPressed();
223     return;
224   }
225   if (event->key() == Qt::Key_PageUp && event->modifiers() == Qt::ShiftModifier)
226   {
227     emit scrollUpPressed();
228     return;
229   }
230 
231   Config::Shortcuts* shortcuts = Config::Shortcuts::instance();
232   QKeySequence ks = QKeySequence(event->key() | event->modifiers());
233 
234   if (ks == shortcuts->getShortcut(Config::Shortcuts::InputClear))
235     return clearKeepUndo();
236   if (ks == shortcuts->getShortcut(Config::Shortcuts::InputDeleteLine))
237     return deleteLine();
238   if (ks == shortcuts->getShortcut(Config::Shortcuts::InputDeleteLineBack))
239     return deleteLineBackwards();
240   if (ks == shortcuts->getShortcut(Config::Shortcuts::InputDeleteWordBack))
241     return deleteWordBackwards();
242 
243   MLEDIT_BASE::keyPressEvent(event);
244 }
245 
mousePressEvent(QMouseEvent * event)246 void MLEdit::mousePressEvent(QMouseEvent* event)
247 {
248   emit clicked();
249   MLEDIT_BASE::mousePressEvent(event);
250 }
251 
252 #ifndef USE_KDE
contextMenuEvent(QContextMenuEvent * event)253 void MLEdit::contextMenuEvent(QContextMenuEvent* event)
254 {
255   QMenu* menu=createStandardContextMenu();
256 
257   if (!isReadOnly())
258   {
259 #ifdef HAVE_HUNSPELL
260     if (mySpellChecker != NULL)
261     {
262       // Save position so we know which word to replace
263       myMenuPos = event->pos();
264 
265       // Get word under cursor
266       QTextCursor cr = cursorForPosition(myMenuPos);
267       cr.select(QTextCursor::WordUnderCursor);
268       QString word = cr.selectedText();
269       if (!word.isEmpty())
270       {
271         // Get spelling suggestions
272         QStringList suggestions = mySpellChecker->getSuggestions(word);
273         if (!suggestions.isEmpty())
274         {
275           // Add spelling suggestions at the top of the menu
276           QAction* firstAction = menu->actions().first();
277           foreach (QString w, suggestions)
278           {
279             QAction* a = new QAction(w, menu);
280             connect(a, SIGNAL(triggered()), SLOT(replaceWord()));
281             menu->insertAction(firstAction, a);
282           }
283           menu->insertSeparator(firstAction);
284         }
285       }
286     }
287 #endif
288 
289     QAction* tabul = new QAction(tr("Allow Tabulations"), menu);
290     tabul->setCheckable(true);
291     tabul->setChecked(!tabChangesFocus());
292     connect(tabul, SIGNAL(triggered()), SLOT(toggleAllowTab()));
293     menu->addAction(tabul);
294   }
295 
296   menu->exec(event->globalPos());
297   delete menu;
298 }
299 #endif
300 
301 #ifdef HAVE_HUNSPELL
replaceWord()302 void MLEdit::replaceWord()
303 {
304   QAction* a = qobject_cast<QAction*>(sender());
305   if (a == NULL)
306     return;
307 
308   // Mark the word under the cursor and replace it with the text from the menu item selected
309   QTextCursor cr = cursorForPosition(myMenuPos);
310   cr.select(QTextCursor::WordUnderCursor);
311   cr.insertText(a->text());
312 }
313 #endif
314 
updateFont()315 void MLEdit::updateFont()
316 {
317   setFont(myUseFixedFont ? Config::General::instance()->fixedFont() :
318       Config::General::instance()->editFont());
319 
320   // Get height of current font
321   myFontHeight = fontMetrics().height();
322 
323   // Set minimum height of text area to one line of text.
324   setMinimumHeight(heightForLines(1));
325 }
326 
heightForLines(int lines) const327 int MLEdit::heightForLines(int lines) const
328 {
329   // We need to add frame width as we're calculating height for the widget, not just the viewport.
330   // The reason for the last constant is unknown, but seems the same regardless of font size and gui style
331   return lines*myFontHeight + 2*frameWidth() + 8;
332 }
333 
setSizeHintLines(int lines)334 void MLEdit::setSizeHintLines(int lines)
335 {
336   myLinesHint = lines;
337 }
338 
sizeHint() const339 QSize MLEdit::sizeHint() const
340 {
341   QSize s = MLEDIT_BASE::sizeHint();
342   if (myLinesHint > 0)
343     s.setHeight(heightForLines(myLinesHint));
344   return s;
345 }
346 
toggleAllowTab()347 void MLEdit::toggleAllowTab()
348 {
349   setTabChangesFocus(!tabChangesFocus());
350 }
351 
352 #if 0
353 //TODO: This may or may not be needed for KTextEdit in KDE 4
354 
355 #ifdef MLEDIT_USE_KTEXTEDIT
356 /**
357  * @return the number of characters @a c at the end of @a str.
358  */
359 static unsigned int countCharRev(const QString& str, const QChar c)
360 {
361   unsigned int count = 0;
362   for (int pos = str.length() - 1; pos >= 0; pos--)
363   {
364     if (str.at(pos) != c)
365       break;
366     count += 1;
367   }
368   return count;
369 }
370 #endif
371 
372 /*
373  * KTextEdit adds a menu entry for doing spell checking. Unfortunatly KSpell
374  * (which is what KTextEdit uses to do the spell check) messes with the newlines
375  * at the end of the text it checks. That's why we need the hack below. It uses
376  * the fact that setText(const QString&) is non-virtual and only calls the
377  * virtual setText(const QString&, const QString&) with a null context.
378  *
379  * When KTextEdit calls setText(correctedText) after the spell check is done
380  * it will call QTextEdit::setText (since it's non-virtual). QTextEdit will
381  * then call setText(correctedText, QString::null) which will end up in the
382  * setText below (since it's virtual). And with m_fixSetTextNewlines set to
383  * true we can fix so that there is as many newlines at the end of the corrected
384  * text as there is in the old.
385  *
386  * On the other hand, when any class that uses MLEditWrap calls
387  * myMLEditWrapInstance->setText(myText) the call will end up at
388  * MLEditWrap::setText(myText) which will set m_fixSetTextNewlines to false before
389  * calling QTextEdit::setText(myText).
390  */
391 void MLEditWrap::setText(const QString& text)
392 {
393   m_fixSetTextNewlines = false;
394   MLEditWrapBase::setText(text);
395 }
396 
397 void MLEditWrap::setText(const QString& txt, const QString& context)
398 {
399   const bool modified = isModified(); // don't let setText reset this flag
400 #ifdef MLEDIT_USE_KTEXTEDIT
401   const QString current = text();
402   if (m_fixSetTextNewlines && context.isNull())
403   {
404     const unsigned int currentNL = countCharRev(current, '\n');
405     const unsigned int txtNL = countCharRev(txt, '\n');
406     if (currentNL > txtNL)
407       MLEditWrapBase::setText(txt + QString().fill('\n', currentNL - txtNL), context);
408     else if (txtNL > currentNL)
409       MLEditWrapBase::setText(txt.left(txt.length() - (txtNL - currentNL)), context);
410     else
411       MLEditWrapBase::setText(txt, context);
412   }
413   else
414 #endif
415     MLEditWrapBase::setText(txt, context);
416 
417   setModified(modified);
418   m_fixSetTextNewlines = true;
419 }
420 #endif
421