/*
This program is free software ; you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation ; either version 2 of the License , or
( at your option ) any later version .
*/
/*
The line input widget with chat enhanced functions
begin : Tue Mar 5 2002
copyright : ( C ) 2002 by Dario Abatianni
email : eisfuchs @ tigress . com
*/
# include "ircinput.h"
# include "konversationapplication.h"
# include "multilineedit.h"
# include "chatwindow.h"
# include "ircview.h"
# include <private/tqrichtext_p.h>
# include <tqclipboard.h>
# include <tqregexp.h>
# include <tqdom.h>
# include <tqevent.h>
# include <tqobject.h>
# include <tqwhatsthis.h>
# include <tqpopupmenu.h>
# include <kapplication.h>
# include <kmessagebox.h>
# include <klocale.h>
# include <kcompletionbox.h>
# define MAXHISTORY 100
# define RICHTEXT 0
IRCInput : : IRCInput ( TQWidget * tqparent ) : KTextEdit ( tqparent )
{
m_lastHeight = document ( ) - > height ( ) ;
//I am not terribly interested in finding out where this value comes from
//nor in compensating for it if my guess is incorrect. so, cache it.
m_qtBoxPadding = m_lastHeight - fontMetrics ( ) . lineSpacing ( ) ;
connect ( KApplication : : kApplication ( ) , TQT_SIGNAL ( appearanceChanged ( ) ) , this , TQT_SLOT ( updateAppearance ( ) ) ) ;
m_multiRow = Preferences : : useMultiRowInputBox ( ) ;
m_useSelection = false ;
// connect history signal
connect ( this , TQT_SIGNAL ( history ( bool ) ) , this , TQT_SLOT ( getHistory ( bool ) ) ) ;
// add one empty line to the history (will be overwritten with newest entry)
historyList . prepend ( TQString ( ) ) ;
// reset history line counter
lineNum = 0 ;
// reset completion mode
setCompletionMode ( ' \0 ' ) ;
completionBox = new KCompletionBox ( this ) ;
connect ( completionBox , TQT_SIGNAL ( activated ( const TQString & ) ) , this , TQT_SLOT ( insertCompletion ( const TQString & ) ) ) ;
// widget may not be resized vertically
tqsetSizePolicy ( TQSizePolicy ( TQSizePolicy : : MinimumExpanding , TQSizePolicy : : Fixed ) ) ;
//NoWrap coupled with the size policy constrains the line edit to be one row high
setWordWrap ( m_multiRow ? WidgetWidth : NoWrap ) ;
setHScrollBarMode ( AlwaysOff ) ;
setVScrollBarMode ( AlwaysOff ) ;
# if RICHTEXT == 1
setAutoFormatting ( TQTextEdit : : AutoNone ) ;
setTextFormat ( RichText ) ;
# else
setTextFormat ( PlainText ) ;
# endif
TQWhatsThis : : add ( this , i18n ( " <qt>The input line is where you type messages to be sent the channel, query, or server. A message sent to a channel is seen by everyone on the channel, whereas a message in a query is sent only to the person in the query with you.<p>To automatically complete the nickname you began typing, press Tab. If you have not begun typing, the last successfully completed nickname will be used.<p>You can also send special commands:<br><table><tr><th>/me <i>action</i></th><td>shows up as an action in the channel or query. For example: <em>/me sings a song</em> will show up in the channel as 'Nick sings a song'.</td></tr><tr><th>/whois <i>nickname</i></th><td>shows information about this person, including what channels they are in.</td></tr></table><p>For more commands, see the Konversation Handbook.<p>A message cannot contain multiple lines.</qt> " ) ) ;
m_disableSpellCheckTimer = new TQTimer ( this ) ;
connect ( m_disableSpellCheckTimer , TQT_SIGNAL ( timeout ( ) ) , this , TQT_SLOT ( disableSpellChecking ( ) ) ) ;
}
IRCInput : : ~ IRCInput ( )
{
}
void IRCInput : : showEvent ( TQShowEvent * /* e */ )
{
m_disableSpellCheckTimer - > stop ( ) ;
setCheckSpellingEnabled ( Preferences : : spellChecking ( ) ) ;
}
void IRCInput : : hideEvent ( TQHideEvent * /* event */ )
{
Preferences : : setSpellChecking ( checkSpellingEnabled ( ) ) ;
// If we disable spell-checking here immediately, tab switching will
// be very slow. If we delay it by five seconds, a user would have to
// need more than five seconds to switch between all his tabs before
// the slowdown starts to occur (show event stops the timer, i.e. wrap-
// around is not an issue). Unless he has unlikely amounts of channels,
// needing more than five seconds indicates very slow switching speed,
// which makes the delay a non-issue to begin with. Hence this fixes
// the problem on the surface. In the KDE 4 version, we want to look
// into having only one spell-checker instance instead of starting and
// stopping at all.
m_disableSpellCheckTimer - > start ( 5000 , true ) ;
}
void IRCInput : : disableSpellChecking ( )
{
setCheckSpellingEnabled ( false ) ;
}
void IRCInput : : slotSpellCheckDone ( const TQString & s )
{
// NOTE: kdelibs 3.5's KSpell stupidly adds newlines to its
// buffer at some point for god-knows-what-reason, and for-
// gets to remove them again before handing the result back.
// There's a FIXME to the effect in KSpell::check. This is
// a workaround.
if ( s = = text ( ) | | s = = ( text ( ) + ' \n ' + ' \n ' ) )
return ;
setText ( s . simplifyWhiteSpace ( ) ) ;
}
void IRCInput : : updateAppearance ( )
{
m_multiRow = Preferences : : useMultiRowInputBox ( ) ;
setWordWrap ( m_multiRow ? WidgetWidth : NoWrap ) ;
m_lastHeight = heightForWidth ( tqsizeHint ( ) . width ( ) ) ;
ensureCursorVisible ( ) ; //appears to trigger updateGeometry
}
void IRCInput : : resizeContents ( int w , int h )
{
if ( document ( ) - > height ( ) ! = m_lastHeight ) {
m_lastHeight = document ( ) - > height ( ) ;
updateGeometry ( ) ;
}
KTextEdit : : resizeContents ( w , h ) ;
}
// widget must be only one line high - luckily QT will enforce this via wrappping policy
TQSize IRCInput : : tqsizeHint ( ) const
{
constPolish ( ) ;
int ObscurePadding = 4 ;
int f = 2 * frameWidth ( ) ;
int w = 12 * ( kMax ( fontMetrics ( ) . lineSpacing ( ) , 14 ) + f + ObscurePadding ) ;
int h = m_lastHeight - m_qtBoxPadding + f + ObscurePadding ;
return TQSize ( w , h ) ;
}
TQPopupMenu * IRCInput : : createPopupMenu ( const TQPoint & pos )
{
TQPopupMenu * menu = KTextEdit : : createPopupMenu ( pos ) ;
menu - > removeItemAt ( menu - > count ( ) - 1 ) ;
menu - > removeItemAt ( menu - > count ( ) - 1 ) ;
return menu ;
}
TQString IRCInput : : text ( ) const
{
# if RICHTEXT == 1
TQString content = KTextEdit : : text ( ) ;
TQDomDocument document ;
document . setContent ( content , false ) ;
TQDomNodeList nodes = document . elementsByTagName ( " p " ) ;
if ( nodes . count ( ) )
{
TQDomElement node = nodes . item ( 0 ) . toElement ( ) ;
return node . text ( ) ;
}
return TQString ( ) ;
# else
return KTextEdit : : text ( ) ;
# endif
}
void IRCInput : : setText ( const TQString & text )
{
// reimplemented to set cursor at the end of the new text
KTextEdit : : setText ( text ) ;
setCursorPosition ( 0 , text . length ( ) + 1 ) ;
}
// FIXME - find a better way to do this. eventfilters introduce nebulous behaviour
//take text events from IRCView and TopicLabel
bool IRCInput : : eventFilter ( TQObject * object , TQEvent * event )
{
if ( object - > isA ( " IRCView " ) | | object - > isA ( " Konversation::TopicLabel " ) )
{
if ( event - > type ( ) = = TQEvent : : KeyPress )
{
TQKeyEvent * ke = TQT_TQKEYEVENT ( event ) ;
// Allow tab to be handled naturally by the widget.
// Once it runs out of links it goes to the next control.
if ( ke - > key ( ) = = Key_Tab & & ( ke - > state ( ) = = 0 | | ke - > state ( ) = = TQt : : ShiftButton ) )
return false ;
if ( ! ke - > text ( ) . isEmpty ( ) & & ( ( ke - > state ( ) & ( TQt : : ShiftButton | TQt : : Keypad ) ) | | ke - > state ( ) = = 0 ) )
{
setFocus ( ) ;
KonversationApplication : : sendEvent ( this , event ) ;
return true ;
}
}
}
return KTextEdit : : eventFilter ( object , event ) ;
}
// Take care of Tab, Cursor and so on
void IRCInput : : keyPressEvent ( TQKeyEvent * e )
{
switch ( e - > key ( ) )
{
case Key_Tab :
emit nickCompletion ( ) ;
return ;
break ;
case Key_Up :
if ( m_multiRow & & ( e - > state ( ) ! = ( TQt : : ShiftButton | TQt : : ControlButton ) ) )
break ;
emit history ( true ) ;
return ;
break ;
case Key_Down :
if ( m_multiRow & & ( e - > state ( ) ! = ( TQt : : ShiftButton | TQt : : ControlButton ) ) )
break ;
emit history ( false ) ;
return ;
break ;
case Key_Enter :
case Key_Return :
{
if ( text ( ) . length ( ) ) addHistory ( text ( ) ) ;
if ( completionBox - > isHidden ( ) )
{
// Reset completion mode
setCompletionMode ( ' \0 ' ) ;
// Ctrl+Enter is a special case in which commands should be send as normal messages
if ( e - > state ( ) & ControlButton )
{
emit envelopeCommand ( ) ;
}
else
{
setText ( static_cast < KonversationApplication * > ( kapp ) - > doAutotqreplace ( text ( ) , true ) ) ;
emit submit ( ) ;
}
}
else
{
insertCompletion ( completionBox - > currentText ( ) ) ;
completionBox - > hide ( ) ;
}
// prevent widget from adding lines
return ;
}
break ;
default :
// Check if the keystroke actually produced text. If not it was just a qualifier.
if ( ! e - > text ( ) . isEmpty ( ) | | ( ( e - > key ( ) > = TQt : : Key_Home ) & & ( e - > key ( ) < = TQt : : Key_Down ) ) )
{
if ( getCompletionMode ( ) ! = ' \0 ' )
{
setCompletionMode ( ' \0 ' ) ;
emit endCompletion ( ) ;
}
completionBox - > hide ( ) ;
}
// support ASCII BEL
if ( e - > ascii ( ) = = 7 )
insert ( " %G " ) ;
// support ^U (delete text in input box)
else if ( e - > ascii ( ) = = 21 )
setText ( " " ) ;
}
KTextEdit : : keyPressEvent ( e ) ;
}
void IRCInput : : addHistory ( const TQString & line )
{
// Only add line if it's not the same as the last was
if ( historyList [ 1 ] ! = line )
{
// Replace empty first entry with line
historyList [ 0 ] = line ;
// Add new empty entry to history
historyList . prepend ( TQString ( ) ) ;
// Remove oldest line in history, if the list grows beyond MAXHISTORY
if ( historyList . count ( ) > MAXHISTORY ) historyList . remove ( historyList . last ( ) ) ;
}
// Reset history counter
lineNum = 0 ;
}
void IRCInput : : getHistory ( bool up )
{
// preserve text
historyList [ lineNum ] = text ( ) ;
// Did the user press cursor up?
if ( up )
{
// increment the line counter
lineNum + + ;
// if we are past the end of the list, go to the last entry
if ( lineNum = = historyList . count ( ) ) lineNum - - ;
}
// no, it was cursor down
else
{
// If we are at the top of the lest, arrow-down shall add the text to the history and clear the field for new input
if ( lineNum = = 0 )
{
if ( text ( ) . length ( ) ) addHistory ( text ( ) ) ;
setText ( " " ) ;
}
// If we aren't at the top of the list, decrement the line counter
else
{
lineNum - - ;
}
}
// replace the text in the input field with history
setText ( historyList [ lineNum ] ) ;
}
/**
* Work around the fact that while TQTextEdit : : paste ( ) is virtual , whether we are
* pasting from middle button or control - V is PRIVATE and NO ACCESSOR is given .
*/
void IRCInput : : contentsMouseReleaseEvent ( TQMouseEvent * ev )
{
if ( ev - > button ( ) = = Qt : : MidButton )
{
m_useSelection = true ;
}
// Reset completion
setCompletionMode ( ' \0 ' ) ;
emit endCompletion ( ) ;
KTextEdit : : contentsMouseReleaseEvent ( ev ) ;
m_useSelection = false ;
}
void IRCInput : : paste ( bool useSelection )
{
m_useSelection = useSelection ;
paste ( ) ;
m_useSelection = false ;
}
void IRCInput : : paste ( )
{
TQClipboard * cb = KApplication : : kApplication ( ) - > tqclipboard ( ) ;
setFocus ( ) ;
// Copy text from the clipboard (paste)
TQString pasteText ;
if ( m_useSelection )
{
pasteText = cb - > text ( TQClipboard : : Selection ) ;
}
else
{
pasteText = cb - > text ( TQClipboard : : Clipboard ) ;
}
// is there any text in the clipboard?
if ( ! pasteText . isEmpty ( ) )
{
//End completion on paste
setCompletionMode ( ' \0 ' ) ;
emit endCompletion ( ) ;
bool signal = false ;
// tqreplace \r with \n to make xterm pastes happy
pasteText . tqreplace ( " \r " , " \n " ) ;
// remove blank lines
while ( pasteText . tqcontains ( " \n \n " ) )
pasteText . tqreplace ( " \n \n " , " \n " ) ;
TQRegExp reTopSpace ( " ^ * \n " ) ;
while ( pasteText . tqcontains ( reTopSpace ) )
pasteText . remove ( reTopSpace ) ;
TQRegExp reBottomSpace ( " \n *$ " ) ;
while ( pasteText . tqcontains ( reBottomSpace ) )
pasteText . remove ( reBottomSpace ) ;
// Escape % when var expansion is enabled
if ( ! Preferences : : disableExpansion ( ) )
{
pasteText . tqreplace ( ' % ' , " %% " ) ;
}
// does the text contain at least one newline character?
if ( pasteText . tqfind ( ' \n ' ) ! = - 1 )
{
// make comparisons easier (avoid signed / unsigned warnings)
unsigned int pos = pasteText . tqfind ( ' \n ' ) ;
unsigned int rpos = pasteText . tqfindRev ( ' \n ' ) ;
// emit the signal if there's a line break in the middle of the text
if ( pos > 0 & & pos ! = ( pasteText . length ( ) - 1 ) )
signal = true ;
// emit the signal if there's more than one line break in the text
if ( pos ! = rpos )
signal = true ;
// Remove the \n from end of the line if there's only one \n
if ( ! signal )
pasteText . remove ( ' \n ' ) ;
}
else
{
insert ( pasteText ) ;
return ;
}
// should we signal the application due to newlines in the paste?
if ( signal )
{
// if there is text in the input line
if ( ! text ( ) . isEmpty ( ) )
{
// prepend text to the paste
pasteText = text ( ) + ' \n ' + pasteText ;
}
// ask the user on long pastes
if ( checkPaste ( pasteText ) )
{
// signal pasted text
emit textPasted ( pasteText ) ;
// remember old line, in case the user does not paste eventually
addHistory ( pasteText ) ;
// delete input text
setText ( " " ) ;
}
}
// otherwise let the KLineEdit handle the pasting
else KTextEdit : : paste ( ) ;
}
}
bool IRCInput : : checkPaste ( TQString & text )
{
int doPaste = KMessageBox : : Yes ;
//text is now preconditioned when you get here
int lines = text . tqcontains ( ' \n ' ) ;
if ( text . length ( ) > 256 | | lines )
{
doPaste = KMessageBox : : warningYesNoCancel
( this ,
i18n ( " <qt>You are attempting to paste a large portion of text (%1 bytes or %2 lines) into "
" the chat. This can cause connection resets or flood kills. "
" Do you really want to continue?</qt> " ) . tqarg ( text . length ( ) ) . tqarg ( lines + 1 ) ,
i18n ( " Large Paste Warning " ) ,
i18n ( " Paste " ) ,
i18n ( " &Edit... " ) ,
" LargePaste " ,
KMessageBox : : Dangerous ) ;
}
if ( doPaste = = KMessageBox : : No )
{
TQString ret ( MultilineEdit : : edit ( this , text ) ) ;
if ( ret . isEmpty ( ) )
return false ;
text = ret ;
return true ;
}
return ( doPaste = = KMessageBox : : Yes ) ;
}
void IRCInput : : showCompletionList ( const TQStringList & nicks )
{
completionBox - > setItems ( nicks ) ;
completionBox - > popup ( ) ;
}
void IRCInput : : insertCompletion ( const TQString & nick )
{
int pos ; // = cursorPosition();
int oldPos ; // = cursorPosition();
getCursorPosition ( & oldPos , & pos ) ;
oldPos = pos ;
TQString line = text ( ) ;
while ( pos & & line [ pos - 1 ] ! = ' ' ) pos - - ;
line . remove ( pos , oldPos - pos ) ;
// did we find the nick in the middle of the line?
if ( pos )
{
TQString addMiddle ( Preferences : : nickCompleteSuffixMiddle ( ) ) ;
line . insert ( pos , nick + addMiddle ) ;
pos + = nick . length ( ) + addMiddle . length ( ) ;
}
// no, it was at the beginning
else
{
setLastCompletion ( nick ) ;
TQString addStart ( Preferences : : nickCompleteSuffixStart ( ) ) ;
line . insert ( pos , nick + addStart ) ;
pos + = nick . length ( ) + addStart . length ( ) ;
}
setText ( line ) ;
setCursorPosition ( 0 , pos ) ;
}
void IRCInput : : setLastCompletion ( const TQString & completion )
{
m_lastCompletion = completion ;
}
// Accessor methods
void IRCInput : : setCompletionMode ( char mode ) { completionMode = mode ; }
char IRCInput : : getCompletionMode ( ) { return completionMode ; }
void IRCInput : : setOldCursorPosition ( int pos ) { oldPos = pos ; }
int IRCInput : : getOldCursorPosition ( ) { return oldPos ; }
# include "ircinput.moc"