1 //=============================================================================
2 //
3 //   File : KviInput.cpp
4 //   Creation date : Sun Jan 3 1999 23:11:50 by Szymon Stefanek
5 //
6 //   This file is part of the KVIrc IRC client distribution
7 //   Copyright (C) 1999 Szymon Stefanek (pragma at kvirc dot net)
8 //   Copyright (C) 2008 Elvio Basello (hellvis69 at netsons dot org)
9 //
10 //   This program is FREE software. You can redistribute it and/or
11 //   modify it under the terms of the GNU General Public License
12 //   as published by the Free Software Foundation; either version 2
13 //   of the License, or (at your option) any later version.
14 //
15 //   This program is distributed in the HOPE that it will be USEFUL,
16 //   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 //   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
18 //   See the GNU General Public License for more details.
19 //
20 //   You should have received a copy of the GNU General Public License
21 //   along with this program. If not, write to the Free Software Foundation,
22 //   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 //
24 //=============================================================================
25 
26 #define _KVI_DEBUG_CHECK_RANGE_
27 #include "kvi_debug.h"
28 
29 #define _KVI_INPUT_CPP_
30 
31 #include "KviInput.h"
32 #include "KviInputEditor.h"
33 #include "KviOptions.h"
34 #include "KviApplication.h"
35 #include "KviColorSelectionWindow.h"
36 #include "KviTextIconWindow.h"
37 #include "KviWindow.h"
38 #include "KviLocale.h"
39 #include "KviScriptEditor.h"
40 #include "KviHistoryWindow.h"
41 #include "KviUserInput.h"
42 #include "KviShortcut.h"
43 #include "KviTalHBox.h"
44 #include "KviTalToolTip.h"
45 
46 #include <QLabel>
47 #include <QFileDialog>
48 #include <QPainter>
49 #include <QClipboard>
50 #include <QStringList>
51 #include <QApplication>
52 #include <QMessageBox>
53 #include <QLayout>
54 #include <QStyle>
55 #include <QEvent>
56 #include <QMouseEvent>
57 #include <QUrl>
58 #include <QHBoxLayout>
59 #include <QMenu>
60 #include <QPushButton>
61 #include <QFontMetrics>
62 
63 #include <cctype>
64 #include <cstdlib>
65 
66 //This comes from KviApplication.cpp
67 extern KviColorWindow * g_pColorWindow;
68 extern KviTextIconWindow * g_pTextIconWindow;
69 extern KviHistoryWindowWidget * g_pHistoryWindow;
70 extern QMenu * g_pInputPopup;
71 
KviInput(KviWindow * pPar,KviUserListView * pView)72 KviInput::KviInput(KviWindow * pPar, KviUserListView * pView)
73     : QWidget(pPar)
74 {
75 	setObjectName("input_widget");
76 	m_pLayout = new QGridLayout(this);
77 
78 	m_pLayout->setMargin(0);
79 	m_pLayout->setSpacing(0);
80 
81 	m_pWindow = pPar;
82 	m_pMultiLineEditor = nullptr;
83 
84 	m_pHideToolsButton = new QToolButton(this);
85 	m_pHideToolsButton->setObjectName("hide_container_button");
86 
87 	m_pHideToolsButton->setIconSize(QSize(22, 22));
88 	m_pHideToolsButton->setFixedWidth(16);
89 
90 	if(g_pIconManager->getBigIcon("kvi_horizontal_left.png"))
91 		m_pHideToolsButton->setIcon(QIcon(*(g_pIconManager->getBigIcon("kvi_horizontal_left.png"))));
92 
93 	connect(m_pHideToolsButton, SIGNAL(clicked()), this, SLOT(toggleToolButtons()));
94 
95 	m_pButtonContainer = new KviTalHBox(this);
96 	m_pButtonContainer->setSpacing(0);
97 	m_pButtonContainer->setMargin(0);
98 
99 	m_pButtonContainer->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum));
100 
101 	m_pHistoryButton = new QToolButton(m_pButtonContainer);
102 	m_pHistoryButton->setObjectName("historybutton");
103 
104 	m_pHistoryButton->setIconSize(QSize(22, 22));
105 
106 	QIcon is1;
107 	if(KVI_OPTION_BOOL(KviOption_boolEnableInputHistory)) //G&N mar 2005
108 	{
109 		is1.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::History)));
110 		m_pHistoryButton->setIcon(is1);
111 		KviTalToolTip::add(m_pHistoryButton, __tr2qs("Show history Ctrl+PageUp"));
112 		connect(m_pHistoryButton, SIGNAL(clicked()), this, SLOT(historyButtonClicked()));
113 	}
114 	else
115 	{
116 		is1.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::HistoryOff)));
117 		m_pHistoryButton->setIcon(is1);
118 		m_pHistoryButton->setEnabled(false);
119 		KviTalToolTip::add(m_pHistoryButton, __tr2qs("Input history disabled"));
120 	}
121 
122 	m_pIconButton = new QToolButton(m_pButtonContainer);
123 	m_pIconButton->setObjectName("iconbutton");
124 
125 	m_pIconButton->setIconSize(QSize(22, 22));
126 	QIcon is3;
127 	is3.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::BigGrin)));
128 	m_pIconButton->setIcon(is3);
129 	KviTalToolTip::add(m_pIconButton, __tr2qs("Show icons popup Alt+E<br>See also /help texticons"));
130 	connect(m_pIconButton, SIGNAL(clicked()), this, SLOT(iconButtonClicked()));
131 
132 	m_pCommandlineModeButton = new QToolButton(m_pButtonContainer);
133 	m_pCommandlineModeButton->setObjectName("commandlinemodebutton");
134 
135 	m_pCommandlineModeButton->setIconSize(QSize(22, 22));
136 	m_pCommandlineModeButton->setCheckable(true);
137 	QIcon is0;
138 	is0.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::SaySmile)), QIcon::Normal, QIcon::On);
139 	is0.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::SayKvs)), QIcon::Normal, QIcon::Off);
140 	m_pCommandlineModeButton->setIcon(is0);
141 	KviTalToolTip::add(m_pCommandlineModeButton, __tr2qs("User friendly command-line mode Ctrl+Y<br>See also /help commandline"));
142 
143 	if(KVI_OPTION_BOOL(KviOption_boolCommandlineInUserFriendlyModeByDefault))
144 		m_pCommandlineModeButton->setChecked(true);
145 
146 	m_pMultiEditorButton = new QToolButton(m_pButtonContainer);
147 	m_pMultiEditorButton->setObjectName("multieditorbutton");
148 
149 	m_pMultiEditorButton->setCheckable(true);
150 	m_pMultiEditorButton->setIconSize(QSize(22, 22));
151 	QIcon is2;
152 	is2.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::IrcView)), QIcon::Normal, QIcon::On);
153 	is2.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::Terminal)), QIcon::Normal, QIcon::Off);
154 	m_pMultiEditorButton->setIcon(is2);
155 
156 	QString szTip = __tr2qs("Multi-line editor Alt+Return");
157 #ifdef COMPILE_ON_MAC
158 	szTip.replace(QString("Alt+Return;"), QString("⌥↩"));
159 #endif
160 	KviTalToolTip::add(m_pMultiEditorButton, szTip);
161 
162 	connect(m_pMultiEditorButton, SIGNAL(toggled(bool)), this, SLOT(multiLineEditorButtonToggled(bool)));
163 
164 	m_pInputEditor = new KviInputEditor(this, pPar, pView);
165 	connect(m_pInputEditor, SIGNAL(enterPressed()), this, SLOT(inputEditorEnterPressed()));
166 	m_pInputEditor->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Ignored));
167 
168 	m_pMultiEditorButton->setAutoRaise(true);
169 	m_pCommandlineModeButton->setAutoRaise(true);
170 	m_pIconButton->setAutoRaise(true);
171 	m_pHistoryButton->setAutoRaise(true);
172 	m_pHideToolsButton->setAutoRaise(true);
173 
174 	m_pLayout->addWidget(m_pHideToolsButton, 0, 2, 2, 1);
175 	m_pLayout->addWidget(m_pButtonContainer, 0, 1, 2, 1);
176 	m_pLayout->addWidget(m_pInputEditor, 0, 0, 2, 1);
177 
178 	installShortcuts();
179 }
180 
~KviInput()181 KviInput::~KviInput()
182 {
183 	if(m_pMultiLineEditor)
184 		KviScriptEditor::destroyInstance(m_pMultiLineEditor);
185 }
186 
isButtonsHidden()187 bool KviInput::isButtonsHidden()
188 {
189 	return m_pButtonContainer->isHidden();
190 }
191 
setButtonsHidden(bool bHidden)192 void KviInput::setButtonsHidden(bool bHidden)
193 {
194 	if(!m_pHideToolsButton || !m_pButtonContainer)
195 		return;
196 	if(bHidden == m_pButtonContainer->isHidden())
197 		return;
198 	m_pButtonContainer->setHidden(bHidden);
199 	QPixmap * pix = bHidden ? g_pIconManager->getBigIcon("kvi_horizontal_right.png") : g_pIconManager->getBigIcon("kvi_horizontal_left.png");
200 	if(pix)
201 		m_pHideToolsButton->setIcon(QIcon(*pix));
202 }
203 
toggleToolButtons()204 void KviInput::toggleToolButtons()
205 {
206 	setButtonsHidden(!isButtonsHidden());
207 }
208 
inputEditorEnterPressed()209 void KviInput::inputEditorEnterPressed()
210 {
211 	QString szText = m_pInputEditor->text();
212 	KviUserInput::parse(szText, m_pWindow, QString(), m_pCommandlineModeButton->isChecked());
213 	m_pInputEditor->setText("");
214 	m_pInputEditor->clearUndoStack();
215 }
216 
installShortcuts()217 void KviInput::installShortcuts()
218 {
219 	KviShortcut::create(KVI_SHORTCUTS_INPUT_MULTILINE, this, SLOT(toggleMultiLine()), nullptr, Qt::WidgetWithChildrenShortcut);
220 	KviShortcut::create(KVI_SHORTCUTS_INPUT_MULTILINE_2, this, SLOT(toggleMultiLine()), nullptr, Qt::WidgetWithChildrenShortcut);
221 }
222 
keyPressEvent(QKeyEvent * e)223 void KviInput::keyPressEvent(QKeyEvent * e)
224 {
225 	if(e->modifiers() & Qt::ControlModifier)
226 	{
227 		switch(e->key())
228 		{
229 			case Qt::Key_Enter:
230 			case Qt::Key_Return:
231 			{
232 				if(m_pMultiLineEditor)
233 				{
234 					QString szText;
235 					m_pMultiLineEditor->getText(szText);
236 					if(szText.isEmpty())
237 						return;
238 					if(KVI_OPTION_BOOL(KviOption_boolWarnAboutPastingMultipleLines))
239 					{
240 						if(szText.length() > 256)
241 						{
242 							if(szText[0] != '/')
243 							{
244 								int nLines = szText.count('\n') + 1;
245 								if(nLines > 15)
246 								{
247 									QMessageBox pMsgBox;
248 									pMsgBox.setText(__tr2qs("You're about to send a message with %1 lines of text.<br><br>"
249 									                        "This warning is here to prevent you from accidentally "
250 									                        "pasting and sending a really large, potentially unedited message from your clipboard.<br><br>"
251 									                        "Some IRC servers may also consider %1 lines of text a flood, "
252 									                        "in which case you will be disconnected from said server.<br><br>"
253 									                        "Do you still want the message to be sent?").arg(nLines));
254 
255 									pMsgBox.setWindowTitle(__tr2qs("Confirm Sending a Large Message - KVIrc"));
256 									pMsgBox.setIcon(QMessageBox::Question);
257 									QAbstractButton * pAlwaysButton = pMsgBox.addButton(__tr2qs("Always"), QMessageBox::YesRole);
258 									/* QAbstractButton *pYesButton = */ pMsgBox.addButton(__tr2qs("Yes"), QMessageBox::YesRole);
259 									QAbstractButton * pNoButton = pMsgBox.addButton(__tr2qs("No"), QMessageBox::NoRole);
260 									pMsgBox.setDefaultButton(qobject_cast<QPushButton *>(pNoButton));
261 									pMsgBox.exec();
262 									if(pMsgBox.clickedButton() == pAlwaysButton)
263 									{
264 										KVI_OPTION_BOOL(KviOption_boolWarnAboutPastingMultipleLines) = false;
265 									}
266 									else if(pMsgBox.clickedButton() == pNoButton || pMsgBox.clickedButton() == nullptr)
267 									{
268 										return;
269 									}
270 								}
271 							}
272 						}
273 					}
274 					szText.replace('\t', QString(KVI_OPTION_UINT(KviOption_uintSpacesToExpandTabulationInput), ' ')); //expand tabs to spaces
275 					KviUserInput::parse(szText, m_pWindow, QString(), m_pCommandlineModeButton->isChecked());
276 					m_pMultiLineEditor->setText("");
277 				}
278 			}
279 			break;
280 		}
281 	}
282 }
283 
multiLinePaste(const QString & szText)284 void KviInput::multiLinePaste(const QString & szText)
285 {
286 	if(!m_pMultiLineEditor)
287 		multiLineEditorButtonToggled(true);
288 	QString szCompleteText = m_pInputEditor->text();
289 	szCompleteText.append(szText);
290 	m_pMultiLineEditor->setText(szCompleteText);
291 }
292 
multiLineEditorButtonToggled(bool bOn)293 void KviInput::multiLineEditorButtonToggled(bool bOn)
294 {
295 	if(m_pMultiLineEditor)
296 	{
297 		if(bOn)
298 			return;
299 		QString szTmp;
300 		m_pMultiLineEditor->getText(szTmp);
301 		m_pLayout->removeWidget(m_pMultiLineEditor);
302 		m_pLayout->removeWidget(m_pHelpLabel);
303 
304 		KviScriptEditor::destroyInstance(m_pMultiLineEditor);
305 		m_pMultiLineEditor = nullptr;
306 
307 		delete m_pHelpLabel;
308 		m_pHelpLabel = nullptr;
309 
310 		szTmp.replace(QRegExp("[\a\f\n\r\v]"), QString(" "));
311 		szTmp.replace('\t', QString(KVI_OPTION_UINT(KviOption_uintSpacesToExpandTabulationInput), ' ')); //expand tabs to spaces
312 		m_pInputEditor->setText(szTmp);
313 		m_pInputEditor->show();
314 		m_pWindow->childrenTreeChanged(nullptr);
315 		m_pInputEditor->setFocus();
316 		m_pMultiEditorButton->setChecked(false);
317 	}
318 	else
319 	{
320 		if(!bOn)
321 			return;
322 		m_pInputEditor->hide();
323 
324 		m_pHelpLabel = new QLabel();
325 		m_pHelpLabel->setContentsMargins(15, 5, 0, 0);
326 
327 		QString tmpHelpLabel = __tr2qs("Ctrl+Return; submits contents, Alt+Return; hides this editor");
328 #ifdef COMPILE_ON_MAC
329 		tmpHelpLabel.replace(QString("Ctrl+Return;"), QString("⌘↩"));
330 		tmpHelpLabel.replace(QString("Alt+Return;"), QString("⌥↩"));
331 #endif
332 		m_pHelpLabel->setText(tmpHelpLabel);
333 		m_pLayout->addWidget(m_pHelpLabel, 0, 0, 1, 1);
334 
335 		m_pMultiLineEditor = KviScriptEditor::createInstance(this);
336 		m_pMultiLineEditor->setText(m_pInputEditor->text());
337 		m_pLayout->addWidget(m_pMultiLineEditor, 1, 0, 1, 1);
338 
339 		m_pWindow->childrenTreeChanged(m_pMultiLineEditor);
340 		m_pMultiLineEditor->setFocus();
341 		m_pMultiEditorButton->setChecked(true);
342 	}
343 }
344 
iconButtonClicked()345 void KviInput::iconButtonClicked()
346 {
347 	if(!g_pTextIconWindow)
348 		g_pTextIconWindow = new KviTextIconWindow();
349 	QPoint pnt = m_pIconButton->mapToGlobal(QPoint(m_pIconButton->width(), 0));
350 	g_pTextIconWindow->move(pnt.x() - g_pTextIconWindow->width(), pnt.y() - g_pTextIconWindow->height());
351 	g_pTextIconWindow->popup(this, true);
352 }
353 
historyButtonClicked()354 void KviInput::historyButtonClicked()
355 {
356 	if(!g_pHistoryWindow)
357 		g_pHistoryWindow = new KviHistoryWindowWidget();
358 
359 	QPoint pnt = mapToGlobal(QPoint(0, 0));
360 
361 	g_pHistoryWindow->setGeometry(pnt.x(), pnt.y() - KVI_HISTORY_WIN_HEIGHT, width(), KVI_HISTORY_WIN_HEIGHT);
362 	g_pHistoryWindow->popup(this);
363 }
364 
setFocus()365 void KviInput::setFocus()
366 {
367 	// redirect setFocus() to the right children
368 	if(m_pMultiLineEditor)
369 		m_pMultiLineEditor->setFocus();
370 	else
371 		m_pInputEditor->setFocus();
372 }
373 
focusInEvent(QFocusEvent *)374 void KviInput::focusInEvent(QFocusEvent *)
375 {
376 	// if we get a focus in event, redirect the focus to the children
377 	if(m_pMultiLineEditor)
378 		m_pMultiLineEditor->setFocus();
379 	else
380 		m_pInputEditor->setFocus();
381 }
382 
heightHint() const383 int KviInput::heightHint() const
384 {
385 	return m_pMultiLineEditor ? (m_pInputEditor->heightHint() * 6) : m_pInputEditor->heightHint();
386 }
387 
setText(const QString & szText)388 void KviInput::setText(const QString & szText)
389 {
390 	if(m_pMultiLineEditor)
391 		m_pMultiLineEditor->setText(szText);
392 	else
393 		m_pInputEditor->setText(szText);
394 }
395 
insertChar(char c)396 void KviInput::insertChar(char c)
397 {
398 	m_pInputEditor->insertChar(c);
399 }
400 
insertText(const QString & szText)401 void KviInput::insertText(const QString & szText)
402 {
403 	m_pInputEditor->insertText(szText);
404 }
405 
applyOptions()406 void KviInput::applyOptions()
407 {
408 	if(KVI_OPTION_BOOL(KviOption_boolEnableInputHistory))
409 	{
410 		if(!m_pHistoryButton->isEnabled())
411 		{
412 			QIcon is1;
413 			is1.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::History)));
414 			m_pHistoryButton->setIcon(is1);
415 			m_pHistoryButton->setEnabled(true);
416 			KviTalToolTip::add(m_pHistoryButton, __tr2qs("Show history Ctrl+PageUp"));
417 			connect(m_pHistoryButton, SIGNAL(clicked()), this, SLOT(historyButtonClicked()));
418 		}
419 	}
420 	else
421 	{
422 		if(m_pHistoryButton->isEnabled())
423 		{
424 			QIcon is1;
425 			is1.addPixmap(*(g_pIconManager->getSmallIcon(KviIconManager::HistoryOff)));
426 			m_pHistoryButton->setIcon(is1);
427 			m_pHistoryButton->setEnabled(false);
428 			KviTalToolTip::add(m_pHistoryButton, __tr2qs("Input history disabled"));
429 			m_pHistoryButton->disconnect(SIGNAL(clicked()));
430 		}
431 	}
432 
433 	m_pInputEditor->applyOptions();
434 }
435 
setFocusProxy(QWidget *)436 void KviInput::setFocusProxy(QWidget *)
437 {
438 }/* do nothing */
439 
text()440 QString KviInput::text()
441 {
442 	QString szText;
443 	if(m_pMultiLineEditor)
444 		m_pMultiLineEditor->getText(szText);
445 	else
446 		szText = m_pInputEditor->text();
447 	return szText;
448 }
449 
toggleMultiLine()450 void KviInput::toggleMultiLine()
451 {
452 	multiLineEditorButtonToggled(!m_pMultiLineEditor);
453 }
454 
455 /*
456 	@doc: commandline
457 	@title:
458 		The Commandline Input Features
459 	@type:
460 		generic
461 	@short:
462 		Commandline input features
463 	@body:
464 		[big]Principles of operation[/big]
465 		[br]
466 		The idea is simple: anything that starts with a slash [b]/[/b] character
467 		is interpreted as a command. Anything else is plain text that is
468 		sent to the target of the window (channel, query, DCC chat etc.).
469 		[big]The two operating modes[/big]
470 		[br]
471 		The commandline input has two operating modes: the [i]user friendly mode[/i] and
472 		the [i]KVS mode[/i]. In the user friendly mode all the parameters of the commands
473 		are interpreted exactly like you type them. There is no special interpretation
474 		of [b]$[/b], [b]%[/b], [b]-[/b], [b]([/b] and [b];[/b] characters. This allows you to type [i]/me is happy ;)[/i], for example.
475 		In the KVS mode the full parameter interpretation is enabled and the commands
476 		work just like in any other script editor. This means that anything that
477 		starts with a [b]$[/b] is a function call, anything that starts with a % is a variable,
478 		the dash characters after command names are interpreted as switches and [b];[/b] is the
479 		command separator. This in turn does [b]not[/b] allow you to type [i]/me is happy ;)[/i]
480 		because [b];[/b] is the command separator and ) will be interpreted as the beginning
481 		of the next command. In KVS mode you obviously have to escape the ; character
482 		by typing [i]/me is happy \;)[/i]. The user friendly mode is good for everyday chatting
483 		and for novice users while the KVS mode is for experts that know that minimum about
484 		scripting languages. Please note that in the user-friendly mode you're not allowed
485 		to type multiple commands at once :).
486 		[br]
487 		Also look at the [doc:keyboard]keyboard shortcuts[/doc] reference.[br]
488 		If you drop a file on this widget, a <a href="parse.kvihelp">/PARSE &lt;filename&gt;</a> will be executed.[br]
489 		You can enable word substitution in the preferences dialog.[br]
490 		For example, if you choose to substitute [b]afaik[/b] with [b]A[/b]s [b]f[/b]ar [b]a[/b]s [b]I[/b] [b]k[/b]now,[br]
491 		when you will type [b]afaik[/b] somewhere in the command line, and then
492 		press Space or Return, that word will be replaced with [i]As far as I know[/i].[br]
493 		Experiment with it :)[br]
494 		The Tab key activates the completion of the current word.[br]
495 		If a word is prefixed with a [b]/[/b], it is treated as a command to be completed,
496 		if it begins with [b]$[/b], it is treated as a function or identifier to be completed,
497 		otherwise it is treated as a nickname or filename to be completed.
498 		[example]
499 			/ec&lt;Tab&gt; will produce /echo&lt;space&gt;
500 			/echo $loca&lt;Tab&gt; will produce /echo $localhost
501 		[/example]
502 		Multiple matches are listed in the view window and the word is completed
503 		to the common part of all the matches.
504 		[example]
505 			$sel&lt;Tab;&gt; will find multiple matches and produce $selected
506 		[/example]
507 		Experiment with that too :)
508 		[br]
509 */
510