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