You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abakus/src/editor.cpp

898 lines
22 KiB

/* This file was part of the SpeedCrunch project
Copyright (C) 2004,2005 Ariya Hidayat <ariya@kde.org>
And is now part of abakus.
Copyright (c) 2005 Michael Pyne <michael.pyne@kdemail.net>
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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "function.h"
#include "valuemanager.h"
#include "editor.h"
#include "evaluator.h"
#include "result.h"
#include <tqapplication.h>
#include <tqlabel.h>
#include <tqlineedit.h>
#include <tqlistbox.h>
#include <tqpainter.h>
#include <tqregexp.h>
#include <tqstringlist.h>
#include <tqstyle.h>
#include <tqsyntaxhighlighter.h>
#include <tqtimer.h>
#include <tqtooltip.h>
#include <tqmessagebox.h>
#include <tqvbox.h>
#include <netwm.h>
#include <fixx11h.h> // netwm.h includes X11 headers which conflict with qevent
#include <tqevent.h>
#include <kdebug.h>
#include <algorithm>
// XXX: QT 4: Replace this with qBinaryFind().
using std::binary_search;
class CalcResultLabel : public TQLabel
{
public:
CalcResultLabel(TQWidget *parent, const char *name, int WFlags) :
TQLabel(parent, name, WFlags)
{
}
protected:
virtual void mousePressEvent(TQMouseEvent *)
{
hide();
}
};
class EditorHighlighter : public TQSyntaxHighlighter
{
public:
EditorHighlighter( Editor* );
int highlightParagraph ( const TQString & text, int );
private:
Editor* editor;
};
class Editor::Private
{
public:
Evaluator* eval;
TQStringList history;
int index;
bool autoCompleteEnabled;
EditorCompletion* completion;
TQTimer* completionTimer;
bool autoCalcEnabled;
char format;
int decimalDigits;
TQTimer* autoCalcTimer;
TQLabel* autoCalcLabel;
bool syntaxHighlightEnabled;
EditorHighlighter* highlighter;
TQMap<ColorType,TQColor> highlightColors;
TQTimer* matchingTimer;
};
class EditorCompletion::Private
{
public:
Editor* editor;
TQVBox *completionPopup;
TQListBox *completionListBox;
};
class ChoiceItem: public TQListBoxText
{
public:
ChoiceItem( TQListBox*, const TQString& );
void setMinNameWidth (int w) { minNameWidth = w; }
int nameWidth() const;
protected:
void paint( TQPainter* p );
private:
TQString item;
TQString desc;
int minNameWidth;
};
ChoiceItem::ChoiceItem( TQListBox* listBox, const TQString& text ):
TQListBoxText( listBox, text ), minNameWidth(0)
{
TQStringList list = TQStringList::split( ':', text );
if( list.count() ) item = list[0];
if( list.count()>1 ) desc = list[1];
}
// Returns width of this particular list item's name.
int ChoiceItem::nameWidth() const
{
if(item.isEmpty())
return 0;
TQFontMetrics fm = listBox()->fontMetrics();
return fm.width( item );
}
void ChoiceItem::paint( TQPainter* painter )
{
int itemHeight = height( listBox() );
TQFontMetrics fm = painter->fontMetrics();
int yPos = ( ( itemHeight - fm.height() ) / 2 ) + fm.ascent();
painter->drawText( 3, yPos, item );
//int xPos = fm.width( item );
int xPos = TQMAX(fm.width(item), minNameWidth);
if( !isSelected() )
painter->setPen( listBox()->palette().disabled().text().dark() );
painter->drawText( 10 + xPos, yPos, desc );
}
EditorHighlighter::EditorHighlighter( Editor* e ):
TQSyntaxHighlighter( e )
{
editor = e;
}
int EditorHighlighter::highlightParagraph ( const TQString & text, int )
{
if( !editor->isSyntaxHighlightEnabled() )
{
setFormat( 0, text.length(), editor->colorGroup().text() );
return 0;
}
TQStringList fnames = FunctionManager::instance()->functionList(FunctionManager::All);
fnames.sort(); // Sort list so we can bin search it.
Tokens tokens = Evaluator::scan( text );
for( unsigned i = 0; i < tokens.count(); i++ )
{
Token& token = tokens[i];
TQString text = token.text().lower();
TQFont font = editor->font();
TQColor color = TQt::black;
switch( token.type() )
{
case Token::Number:
color = editor->highlightColor( Editor::Number );
break;
case Token::Identifier:
{
color = editor->highlightColor( Editor::Variable );
13 years ago
#ifndef QT_NO_STL
if( binary_search( fnames.constBegin(), fnames.constEnd(), text) ) {
13 years ago
#else // QT_NO_STL
#warning "Not using STL libraries; performance may be degraded..."
if( fnames.find( text) != fnames.end()) {
#endif // QT_NO_STL
color = editor->highlightColor( Editor::FunctionName );
}
}
break;
case Token::Operator:
break;
default: break;
};
if( token.pos() >= 0 ) {
setFormat( token.pos(), token.text().length(), font, color );
}
}
return 0;
}
Editor::Editor( TQWidget* parent, const char* name ):
TQTextEdit( parent, name )
{
d = new Private;
d->eval = 0;
d->index = 0;
d->autoCompleteEnabled = true;
d->completion = new EditorCompletion( this );
d->completionTimer = new TQTimer( this );
d->autoCalcEnabled = true;
d->syntaxHighlightEnabled = true;
d->highlighter = new EditorHighlighter( this );
d->autoCalcTimer = new TQTimer( this );
d->matchingTimer = new TQTimer( this );
setSizePolicy( TQSizePolicy::Expanding, TQSizePolicy::Fixed );
setWordWrap( NoWrap );
setHScrollBarMode( AlwaysOff );
setVScrollBarMode( AlwaysOff );
setTextFormat( PlainText );
setAutoFormatting( AutoNone );
setTabChangesFocus( true );
setLinkUnderline( false );
connect( d->completion, TQT_SIGNAL( selectedCompletion( const TQString& ) ),
TQT_SLOT( autoComplete( const TQString& ) ) );
connect( this, TQT_SIGNAL( textChanged() ), TQT_SLOT( checkAutoComplete() ) );
connect( d->completionTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( triggerAutoComplete() ) );
connect( this, TQT_SIGNAL( textChanged() ), TQT_SLOT( checkMatching() ) );
connect( d->matchingTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( doMatchingLeft() ) );
connect( d->matchingTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( doMatchingRight() ) );
connect( this, TQT_SIGNAL( textChanged() ), TQT_SLOT( checkAutoCalc() ) );
connect( d->autoCalcTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( autoCalc() ) );
d->autoCalcLabel = new CalcResultLabel( 0, "autocalc", WStyle_StaysOnTop |
WStyle_Customize | WStyle_NoBorder | WStyle_Tool | WX11BypassWM );
d->autoCalcLabel->setFrameStyle( TQFrame::Plain | TQFrame::Box );
d->autoCalcLabel->setPalette( TQToolTip::palette() );
d->autoCalcLabel->hide();
setHighlightColor( Number, TQColor(0,0,127) );
setHighlightColor( FunctionName, TQColor(85,0,0) );
setHighlightColor( Variable, TQColor(0,85,0) );
setHighlightColor( MatchedPar, TQColor(255,255,183) );
}
Editor::~Editor()
{
d->autoCalcLabel->hide();
delete d;
}
TQSize Editor::sizeHint() const
{
constPolish();
TQFontMetrics fm = fontMetrics();
int h = TQMAX(fm.lineSpacing(), 14);
int w = fm.width( 'x' ) * 20;
int m = frameWidth() * 2;
return( style().tqsizeFromContents(TQStyle::CT_LineEdit, this,
TQSize( w + m, h + m + 4 ).
expandedTo(TQApplication::globalStrut())));
}
TQStringList Editor::history() const
{
return d->history;
}
void Editor::setHistory( const TQStringList& h )
{
d->history = h;
d->index = d->history.count();
}
bool Editor::autoCompleteEnabled() const
{
return d->autoCompleteEnabled;
}
void Editor::setAutoCompleteEnabled( bool enable )
{
d->autoCompleteEnabled = enable;
}
bool Editor::autoCalcEnabled() const
{
return d->autoCalcEnabled;
}
void Editor::setAutoCalcEnabled( bool enable )
{
d->autoCalcEnabled = enable;
}
void Editor::setFormat( char format )
{
d->format = format;
}
void Editor::setDecimalDigits( int digits )
{
d->decimalDigits = digits;
}
void Editor::appendHistory( const TQString& text )
{
if( text.isEmpty() ) return;
TQString lastText;
if( d->history.count() )
lastText = d->history[ d->history.count()-1 ];
if( text == lastText ) return;
d->history.append( text );
d->index = d->history.count()-1;
}
void Editor::clearHistory()
{
d->history.clear();
d->index = 0;
}
void Editor::squelchNextAutoCalc()
{
d->autoCalcTimer->stop();
}
void Editor::setText(const TQString &txt)
{
TQTextEdit::setText(txt);
squelchNextAutoCalc();
}
void Editor::checkAutoComplete()
{
if( !d->autoCompleteEnabled ) return;
d->completionTimer->stop();
d->completionTimer->start( 500, true );
}
void Editor::checkMatching()
{
if( !d->syntaxHighlightEnabled ) return;
d->matchingTimer->stop();
d->matchingTimer->start( 200, true );
}
void Editor::checkAutoCalc()
{
// Calc-As-You-Type
if( !d->autoCalcEnabled ) return;
d->autoCalcTimer->stop();
d->autoCalcTimer->start( 1000, true );
d->autoCalcLabel->hide();
}
void Editor::doMatchingLeft()
{
if( !d->syntaxHighlightEnabled ) return;
// tokenize the expression
int para = 0, curPos = 0;
getCursorPosition( &para, &curPos );
// check for right par
TQString subtext = text().left( curPos );
Tokens tokens = Evaluator::scan( subtext );
if( !tokens.valid() ) return;
if( tokens.count()<1 ) return;
Token lastToken = tokens[ tokens.count()-1 ];
// right par ?
if( lastToken.isOperator() )
if( lastToken.asOperator() == Token::RightPar )
if( lastToken.pos() == curPos-1 )
{
// find the matching left par
unsigned par = 1;
int k = 0;
Token matchToken;
int matchPos = -1;
for( k = tokens.count()-2; k >= 0; k-- )
{
if( par < 1 ) break;
Token matchToken = tokens[k];
if( matchToken.isOperator() )
{
if( matchToken.asOperator() == Token::RightPar )
par++;
if( matchToken.asOperator() == Token::LeftPar )
par--;
if( par == 0 ) matchPos = matchToken.pos();
}
}
if( matchPos >= 0 )
{
setSelection( 0, matchPos, 0, matchPos+1, 2 );
setSelection( 0, lastToken.pos(), 0, lastToken.pos()+1, 1 );
setCursorPosition( para, curPos );
}
}
}
void Editor::doMatchingRight()
{
if( !d->syntaxHighlightEnabled ) return;
// tokenize the expression
int para = 0, curPos = 0;
getCursorPosition( &para, &curPos );
// check for left par
TQString subtext = text().right( text().length() - curPos );
Tokens tokens = Evaluator::scan( subtext );
if( !tokens.valid() ) return;
if( tokens.count()<1 ) return;
Token firstToken = tokens[ 0 ];
// left par ?
if( firstToken.isOperator() )
if( firstToken.asOperator() == Token::LeftPar )
if( firstToken.pos() == 0 )
{
// find the matching right par
unsigned par = 1;
unsigned int k = 0;
Token matchToken;
int matchPos = -1;
for( k = 1; k < tokens.count(); k++ )
{
if( par < 1 ) break;
Token matchToken = tokens[k];
if( matchToken.isOperator() )
{
if( matchToken.asOperator() == Token::LeftPar )
par++;
if( matchToken.asOperator() == Token::RightPar )
par--;
if( par == 0 ) matchPos = matchToken.pos();
}
}
if( matchPos >= 0 )
{
setSelection( 0, curPos+matchPos, 0, curPos+matchPos+1, 2 );
setSelection( 0, curPos+firstToken.pos(), 0, curPos+firstToken.pos()+1, 1 );
setCursorPosition( para, curPos );
}
}
}
void Editor::triggerAutoComplete()
{
if( !d->autoCompleteEnabled ) return;
// tokenize the expression (don't worry, this is very fast)
// faster now that it uses flex. ;)
int para = 0, curPos = 0;
getCursorPosition( &para, &curPos );
TQString subtext = text().left( curPos );
Tokens tokens = Evaluator::scan( subtext );
if(!tokens.valid())
{
kdWarning() << "invalid tokens.\n";
return;
}
if(tokens.isEmpty() || subtext.endsWith(" "))
return;
Token lastToken = tokens[ tokens.count()-1 ];
// last token must be an identifier
if( !lastToken.isIdentifier() )
return;
TQString id = lastToken.text();
if( id.isEmpty() )
return;
// find matches in function names
TQStringList fnames = FunctionManager::instance()->functionList(FunctionManager::All);
TQStringList choices;
for( unsigned i=0; i<fnames.count(); i++ )
if( fnames[i].startsWith( id, false ) )
{
TQString str = fnames[i];
::Function* f = FunctionManager::instance()->function( str );
if( f && !f->description.isEmpty() )
str.append( ':' ).append( f->description );
choices.append( str );
}
choices.sort();
// find matches in variables names
TQStringList vchoices;
TQStringList values = ValueManager::instance()->valueNames();
for(TQStringList::ConstIterator it = values.begin(); it != values.end(); ++it)
if( (*it).startsWith( id, false ) )
{
TQString choice = ValueManager::description(*it);
if(choice.isEmpty())
choice = ValueManager::instance()->value(*it).toString();
vchoices.append( TQString("%1:%2").arg( *it, choice ) );
}
vchoices.sort();
choices += vchoices;
// no match, don't bother with completion
if( !choices.count() ) return;
// one match, complete it for the user
if( choices.count()==1 )
{
TQString str = TQStringList::split( ':', choices[0] )[0];
// single perfect match, no need to give choices.
if(str == id.lower())
return;
str = str.remove( 0, id.length() );
int para = 0, curPos = 0;
getCursorPosition( &para, &curPos );
blockSignals( true );
insert( str );
setSelection( 0, curPos, 0, curPos+str.length() );
blockSignals( false );
return;
}
// present the user with completion choices
d->completion->showCompletion( choices );
}
void Editor::autoComplete( const TQString& item )
{
if( !d->autoCompleteEnabled || item.isEmpty() )
return;
int para = 0, curPos = 0;
getCursorPosition( &para, &curPos );
TQString subtext = text().left( curPos );
Tokens tokens = Evaluator::scan( subtext );
if( !tokens.valid() || tokens.count() < 1 )
return;
Token lastToken = tokens[ tokens.count()-1 ];
if( !lastToken.isIdentifier() )
return;
TQStringList str = TQStringList::split( ':', item );
blockSignals( true );
setSelection( 0, lastToken.pos(), 0, lastToken.pos()+lastToken.text().length() );
insert( str[0] );
blockSignals( false );
}
void Editor::autoCalc()
{
if( !d->autoCalcEnabled )
return;
TQString str = Evaluator::autoFix( text() );
if( str.isEmpty() )
return;
// too short? do not bother...
Tokens tokens = Evaluator::scan( str );
if( tokens.count() < 2 )
return;
// If we're using set for a function don't try.
TQRegExp setFn("\\s*set.*\\(.*=");
if( str.find(setFn) != -1 )
return;
// strip off assignment operator, e.g. "x=1+2" becomes "1+2" only
// the reason is that we want only to evaluate (on the fly) the expression,
// not to update (put the result in) the variable
if( tokens.count() > 2 && tokens[0].isIdentifier() &&
tokens[1].asOperator() == Token::Equal )
{
Tokens::const_iterator it = tokens.begin();
++it;
++it; // Skip first two tokens.
// Reconstruct string to evaluate using the tokens.
str = "";
while(it != tokens.end())
{
str += (*it).text();
str += ' ';
++it;
}
}
Abakus::number_t result = parseString(str.latin1());
if( Result::lastResult()->type() == Result::Value )
{
TQString ss = TQString("Result: <b>%2</b>").arg(result.toString());
d->autoCalcLabel->setText( ss );
d->autoCalcLabel->adjustSize();
// reposition nicely
TQPoint pos = mapToGlobal( TQPoint( 0, 0 ) );
pos.setY( pos.y() - d->autoCalcLabel->height() - 1 );
d->autoCalcLabel->move( pos );
d->autoCalcLabel->show();
d->autoCalcLabel->raise();
// do not show it forever
TQTimer::singleShot( 5000, d->autoCalcLabel, TQT_SLOT( hide()) );
}
else
{
// invalid expression
d->autoCalcLabel->hide();
}
}
TQString Editor::formatNumber( const Abakus::number_t &value ) const
{
return value.toString();
}
void Editor::historyBack()
{
if( d->history.isEmpty() )
return;
d->index--;
if( d->index < 0 )
d->index = 0;
setText( d->history[ d->index ] );
setCursorPosition( 0, text().length() );
ensureCursorVisible();
}
void Editor::historyForward()
{
if( d->history.isEmpty() )
return;
d->index++;
if( d->index >= (int) d->history.count() )
d->index = d->history.count() - 1;
setText( d->history[ d->index ] );
setCursorPosition( 0, text().length() );
ensureCursorVisible();
}
void Editor::keyPressEvent( TQKeyEvent* e )
{
if( e->key() == Key_Up )
{
historyBack();
e->accept();
return;
}
if( e->key() == Key_Down )
{
historyForward();
e->accept();
return;
}
if( e->key() == Key_Enter || e->key() == Key_Return )
{
emit returnPressed();
return;
}
if( e->key() == Key_Left ||
e->key() == Key_Right ||
e->key() == Key_Home ||
e->key() == Key_End )
{
checkMatching();
}
TQTextEdit::keyPressEvent( e );
}
void Editor::wheelEvent( TQWheelEvent *e )
{
if( e->delta() > 0 )
historyBack();
else if( e->delta() < 0 )
historyForward();
e->accept();
}
void Editor::setSyntaxHighlight( bool enable )
{
d->syntaxHighlightEnabled = enable;
d->highlighter->rehighlight();
}
bool Editor::isSyntaxHighlightEnabled() const
{
return d->syntaxHighlightEnabled;
}
void Editor::setHighlightColor( ColorType type, TQColor color )
{
d->highlightColors[ type ] = color;
setSelectionAttributes( 1, highlightColor( Editor::MatchedPar ), false );
setSelectionAttributes( 2, highlightColor( Editor::MatchedPar ), false );
d->highlighter->rehighlight();
}
TQColor Editor::highlightColor( ColorType type )
{
return d->highlightColors[ type ];
}
EditorCompletion::EditorCompletion( Editor* editor ): TQObject( editor )
{
d = new Private;
d->editor = editor;
d->completionPopup = new TQVBox( editor->topLevelWidget(), 0, WType_Popup );
d->completionPopup->setFrameStyle( TQFrame::Box | TQFrame::Plain );
d->completionPopup->setLineWidth( 1 );
d->completionPopup->installEventFilter( this );
d->completionPopup->setSizePolicy( TQSizePolicy::Expanding, TQSizePolicy::Minimum);
d->completionListBox = new TQListBox( d->completionPopup );
d->completionPopup->setFocusProxy( d->completionListBox );
d->completionListBox->setFrameStyle( TQFrame::NoFrame );
d->completionListBox->setVariableWidth( true );
d->completionListBox->installEventFilter( this );
}
EditorCompletion::~EditorCompletion()
{
delete d;
}
bool EditorCompletion::eventFilter( TQObject *obj, TQEvent *ev )
{
if ( TQT_BASE_OBJECT(obj) == TQT_BASE_OBJECT(d->completionPopup) || TQT_BASE_OBJECT(obj) == TQT_BASE_OBJECT(d->completionListBox) )
{
if ( ev->type() == TQEvent::KeyPress )
{
TQKeyEvent *ke = (TQKeyEvent*)ev;
if ( ke->key() == Key_Enter || ke->key() == Key_Return )
{
doneCompletion();
return true;
}
else if ( ke->key() == Key_Left || ke->key() == Key_Right ||
ke->key() == Key_Up || ke->key() == Key_Down ||
ke->key() == Key_Home || ke->key() == Key_End ||
ke->key() == Key_Prior || ke->key() == Key_Next )
return false;
d->completionPopup->close();
d->editor->setFocus();
TQApplication::sendEvent( d->editor, ev );
return true;
}
if ( ev->type() == TQEvent::MouseButtonDblClick )
{
doneCompletion();
return true;
}
}
return false;
}
void EditorCompletion::doneCompletion()
{
d->completionPopup->close();
d->editor->setFocus();
emit selectedCompletion( d->completionListBox->currentText() );
}
void EditorCompletion::showCompletion( const TQStringList &choices )
{
static bool shown = false;
if( !choices.count() ) return;
d->completionListBox->clear();
int maxWidth = 0;
for( unsigned i = 0; i < choices.count(); i++ ) {
ChoiceItem *item = new ChoiceItem( d->completionListBox, choices[i] );
int itemMaxWidth = item->nameWidth();
if(itemMaxWidth > maxWidth)
maxWidth = itemMaxWidth;
}
for(unsigned i = 0; i < d->completionListBox->count(); ++i) {
ChoiceItem *item = static_cast<ChoiceItem *>(d->completionListBox->item(i));
item->setMinNameWidth(maxWidth);
}
d->completionListBox->setCurrentItem( 0 );
// size of the pop-up
d->completionPopup->setMaximumHeight( 120 );
d->completionPopup->resize( d->completionListBox->sizeHint() +
TQSize( d->completionListBox->verticalScrollBar()->width() + 4,
d->completionListBox->horizontalScrollBar()->height() + 4 ) );
if(!shown)
{
d->completionPopup->show();
TQTimer::singleShot ( 0, this, TQT_SLOT(moveCompletionPopup()) );
}
else
{
moveCompletionPopup();
d->completionPopup->show();
}
}
void EditorCompletion::moveCompletionPopup()
{
int h = d->completionListBox->height();
int w = d->completionListBox->width();
// position, reference is editor's cursor position in global coord
TQFontMetrics fm( d->editor->font() );
int para = 0, curPos = 0;
d->editor->getCursorPosition( &para, &curPos );
int pixelsOffset = fm.width( d->editor->text(), curPos );
pixelsOffset -= d->editor->contentsX();
TQPoint pos = d->editor->mapToGlobal( TQPoint( pixelsOffset, d->editor->height() ) );
// if popup is partially invisible, move to other position
NETRootInfo info(d->completionPopup->x11Display(),
NET::CurrentDesktop | NET::WorkArea | NET::NumberOfDesktops,
-1, false);
info.activate(); // wtf is this needed for?
NETRect NETarea = info.workArea(info.currentDesktop());
TQRect area(NETarea.pos.x, NETarea.pos.y, NETarea.size.width, NETarea.size.height);
if( pos.y() + h > area.y() + area.height() )
pos.setY( pos.y() - h - d->editor->height() );
if( pos.x() + w > area.x() + area.width() )
pos.setX( area.x() + area.width() - w );
d->completionPopup->move( pos );
d->completionListBox->setFocus();
}
#include "editor.moc"
// vim: set et sw=2 ts=8: