/* This file was part of the SpeedCrunch project Copyright (C) 2004,2005 Ariya Hidayat And is now part of abakus. Copyright (c) 2005 Michael Pyne 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include // netwm.h includes X11 headers which conflict with qevent #include #include #include // XXX: QT 4: Replace this with qBinaryFind(). using std::binary_search; class CalcResultLabel : public TQLabel { public: CalcResultLabel(TQWidget *tqparent, const char *name, int WFlags) : TQLabel(tqparent, 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 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()->tqpalette().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->tqcolorGroup().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 ); if( binary_search( fnames.constBegin(), fnames.constEnd(), text) ) { 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* tqparent, const char* name ): TQTextEdit( tqparent, 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 ); tqsetSizePolicy( 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::tqsizeHint() const { constPolish(); TQFontMetrics fm = fontMetrics(); int h = TQMAX(fm.lineSpacing(), 14); int w = fm.width( 'x' ) * 20; int m = frameWidth() * 2; return( tqstyle().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( ¶, &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( ¶, &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( ¶, &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; ifunction( 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).tqstartsWith( id, false ) ) { TQString choice = ValueManager::description(*it); if(choice.isEmpty()) choice = ValueManager::instance()->value(*it).toString(); vchoices.append( TQString("%1:%2").tqarg( *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( ¶, &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( ¶, &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: %2").tqarg(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->tqtopLevelWidget(), 0, WType_Popup ); d->completionPopup->setFrameStyle( TQFrame::Box | TQFrame::Plain ); d->completionPopup->setLineWidth( 1 ); d->completionPopup->installEventFilter( this ); d->completionPopup->tqsetSizePolicy( 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(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->tqsizeHint() + 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( ¶, &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: