1 /** Copyright (C) 2006, Ian Paul Larsen.
2  **
3  **  This program is free software; you can redistribute it and/or modify
4  **  it under the terms of the GNU General Public License as published by
5  **  the Free Software Foundation; either version 2 of the License, or
6  **  (at your option) any later version.
7  **
8  **  This program is distributed in the hope that it will be useful,
9  **  but WITHOUT ANY WARRANTY; without even the implied warranty of
10  **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  **  GNU General Public License for more details.
12  **
13  **  You should have received a copy of the GNU General Public License along
14  **  with this program; if not, write to the Free Software Foundation, Inc.,
15  **  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16  **/
17 
18 
19 
20 #include <iostream>
21 #include <QScrollBar>
22 #include <QTextCursor>
23 #include <QTextBlock>
24 #include <QFlags>
25 #include <QPainter>
26 #include <QResizeEvent>
27 #include <QPaintEvent>
28 #include <QtWidgets/QMessageBox>
29 #include <QtWidgets/QStatusBar>
30 #include <QtPrintSupport/QPrinter>
31 #include <QtPrintSupport/QPrintDialog>
32 #include <QtWidgets/QFontDialog>
33 
34 #include "MainWindow.h"
35 #include "BasicEdit.h"
36 #include "LineNumberArea.h"
37 #include "Settings.h"
38 #include "Constants.h"
39 
40 extern int guiState;
41 
BasicEdit(const QString & defaulttitle)42 BasicEdit::BasicEdit(const QString & defaulttitle) {
43 	currentLine = 1;
44 	runState = RUNSTATESTOP;
45     rightClickBlockNumber = -1;
46     breakPoints = new QList<int>;
47     title = defaulttitle;
48     windowtitle = defaulttitle;
49     filename.clear();
50     path.clear();
51     undoButton = false;
52     redoButton = false;
53     copyButton = false;
54     action = new QAction(windowtitle, this);
55     action->setCheckable(true);
56     fileChangedOnDiskFlag = false;
57 
58     setReadOnly(guiState!=GUISTATENORMAL);
59     if(guiState==GUISTATEAPP){
60         startPos=0;
61         lineNumberArea = NULL;
62         setDisabled(true);
63     }else{
64         lineNumberArea = new LineNumberArea(this);
65         this->setInputMethodHints(Qt::ImhNoPredictiveText);
66         startPos = this->textCursor().position();
67         updateLineNumberAreaWidth(0);
68         connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
69         connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
70         connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(cursorMove()));
71         connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
72         connect(this, SIGNAL(modificationChanged(bool)), this, SLOT(updateTitle()));
73         connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(slotUndoAvailable(bool)));
74         connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(slotRedoAvailable(bool)));
75         connect(this, SIGNAL(copyAvailable(bool)), this, SLOT(slotCopyAvailable(bool)));
76         connect(action, SIGNAL(triggered()), this, SLOT(actionWasTriggered()));
77         highlightCurrentLine();
78     }
79 }
80 
81 
~BasicEdit()82 BasicEdit::~BasicEdit() {
83     if (breakPoints) {
84         delete breakPoints;
85         breakPoints = NULL;
86     }
87     if (lineNumberArea) {
88 		delete lineNumberArea;
89 		lineNumberArea = NULL;
90 	}
91 }
92 
setFont(QFont f)93 void BasicEdit::setFont(QFont f) {
94 	// set the font and the tab stop at EDITOR_TAB_WIDTH spaces
95 	QPlainTextEdit::setFont(f);
96 	QFontMetrics metrics(f);
97 	setTabStopWidth(metrics.width(" ")*EDITOR_TAB_WIDTH);
98     updateLineNumberAreaWidth(blockCount());
99 }
100 
101 
102 void
cursorMove()103 BasicEdit::cursorMove() {
104 	QTextCursor t(textCursor());
105 	emit(changeStatusBar(tr("Line: ") + QString::number(t.blockNumber()+1)
106 		+ tr(" Character: ") + QString::number(t.positionInBlock())));
107 }
108 
109 void
seekLine(int newLine)110 BasicEdit::seekLine(int newLine) {
111     // go to a line number and set
112 	// the text cursor
113 	//
114 	// code should be proximal in that it should be closest to look at the curent
115     // position than to go and search the entire program from the top
116     QTextCursor t = textCursor();
117     int line = t.blockNumber()+1;	// current line number for the block
118     // go back or forward to the line from the current position
119     if (line<newLine) {
120         // advance forward
121         while (line < newLine && t.movePosition(QTextCursor::NextBlock)) {
122             line++;
123         }
124     } else if (line>newLine){
125         // go back to find the line
126         while (line > newLine && t.movePosition(QTextCursor::PreviousBlock)) {
127             line--;
128         }
129     }
130 	setTextCursor(t);
131 }
132 
slotWhitespace(bool checked)133 void BasicEdit::slotWhitespace(bool checked) {
134 	// toggle the display of whitespace characters
135 	// http://www.qtcentre.org/threads/27245-Printing-white-spaces-in-QPlainTextEdit-the-QtCreator-way
136 	QTextOption option = document()->defaultTextOption();
137 	if (checked) {
138 		option.setFlags(option.flags() | QTextOption::ShowTabsAndSpaces);
139 	} else {
140 		option.setFlags(option.flags() & ~QTextOption::ShowTabsAndSpaces);
141 	}
142 	option.setFlags(option.flags() | QTextOption::AddSpaceForLineAndParagraphSeparators);
143 	document()->setDefaultTextOption(option);
144 }
145 
146 void
goToLine(int newLine)147 BasicEdit::goToLine(int newLine) {
148     seekLine(newLine);
149     setFocus();
150 }
151 
152 
153 void
keyPressEvent(QKeyEvent * e)154 BasicEdit::keyPressEvent(QKeyEvent *e) {
155 	e->accept();
156     //Autoindent new line as previous one
157 	if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter){
158 		QPlainTextEdit::keyPressEvent(e);
159 		QTextCursor cur = textCursor();
160 		cur.movePosition(QTextCursor::PreviousBlock);
161 		cur.movePosition(QTextCursor::StartOfBlock);
162 		cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
163 		QString str = cur.selectedText();
164 		QRegExp rx("^([\\t ]+)");
165 		if(str.indexOf(rx) >= 0)
166 			textCursor().insertText(rx.cap(1));
167 	}else if(e->key() == Qt::Key_Tab && e->modifiers() == Qt::NoModifier){
168 		if(!indentSelection())
169 			QPlainTextEdit::keyPressEvent(e);
170 	}else if((e->key() == Qt::Key_Tab && e->modifiers() & Qt::ShiftModifier) || e->key() == Qt::Key_Backtab){
171 		unindentSelection();
172 	}else{
173 		QPlainTextEdit::keyPressEvent(e);
174 	}
175 }
176 
177 
saveFile(bool overwrite)178 void BasicEdit::saveFile(bool overwrite) {
179 	// BE SURE TO SET filename PROPERTY FIRST
180 	// or set it to '' to prompt for a new file name
181 	if (filename == "") {
182         emit(setCurrentEditorTab(this)); //activate editor window
183         filename = QFileDialog::getSaveFileName(this, tr("Save file as"), title+".kbs", tr("BASIC-256 File ") + "(*.kbs);;" + tr("Any File ")  + "(*.*)");
184 	}
185 
186 	if (filename != "") {
187 		QRegExp rx("\\.[^\\/]*$");
188 		if (rx.indexIn(filename) == -1) {
189 			filename += ".kbs";
190 		}
191 		QFile f(filename);
192 		bool dooverwrite = true;
193 		if (!overwrite && f.exists()) {
194 			dooverwrite = ( QMessageBox::Yes == QMessageBox::warning(this, tr("Save File"),
195 				tr("The file ") + filename + tr(" already exists.")+ "\n" +tr("Do you want to overwrite?"),
196 				QMessageBox::Yes | QMessageBox::No,
197 				QMessageBox::No));
198 		}
199 		if (dooverwrite) {
200 			f.open(QIODevice::WriteOnly | QIODevice::Truncate);
201 			f.write(this->document()->toPlainText().toUtf8());
202 			f.close();
203 			QFileInfo fi(f);
204             document()->setModified(false);
205             setTitle(fi.fileName());
206 			QDir::setCurrent(fi.absolutePath());
207             emit(addFileToRecentList(filename));
208 		}
209 	}
210 }
211 
saveAllStep(int s)212 void BasicEdit::saveAllStep(int s) {
213     if(document()->isModified()){
214         if(s==1){
215             // Step 1 - save changes
216             if(!filename.isEmpty()) saveFile(true);
217         }else{
218             // Step 2 - save unsaved files (need user interaction)
219             if(filename.isEmpty()) saveFile(true);
220         }
221     }
222 }
223 
saveProgram()224 void BasicEdit::saveProgram() {
225     saveFile(false);
226 }
227 
228 void
saveAsProgram()229 BasicEdit::saveAsProgram() {
230     QString tempfilename = QFileDialog::getSaveFileName(this, tr("Save file as"), ".", tr("BASIC-256 File ")+ "(*.kbs);;" + tr("Any File ")+ "(*.*)");
231     if (tempfilename != "") {
232         filename = tempfilename;
233         saveFile(false);
234     }
235 }
236 
237 
238 
239 
slotPrint()240 void BasicEdit::slotPrint() {
241 #ifdef ANDROID
242     QMessageBox::warning(this, tr("Print"),
243         tr("Printing is not supported in this platform at this time."));
244 #else
245     QTextDocument *document = this->document();
246     QPrinter printer;
247 
248     QPrintDialog *dialog = new QPrintDialog(&printer, this);
249     dialog->setWindowTitle(tr("Print Code"));
250 
251     if (dialog->exec() == QDialog::Accepted) {
252         if ((printer.printerState() != QPrinter::Error) && (printer.printerState() != QPrinter::Aborted)) {
253 
254             QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
255             document->print(&printer);
256             QApplication::restoreOverrideCursor();
257         } else {
258             QMessageBox::warning(this, tr("Print"),
259                 tr("Unable to carry out printing.\nPlease check your printer settings."));
260         }
261 
262     }
263 
264     delete dialog;
265 #endif
266 }
267 
268 
269 
beautifyProgram()270 void BasicEdit::beautifyProgram() {
271 	QString program;
272 	QStringList lines;
273 	int indent = 0;
274 	bool indentThisLine = true;
275 	bool increaseIndent = false;
276 	bool decreaseIndent = false;
277 	bool increaseIndentDouble = false;
278 	bool decreaseIndentDouble = false;
279 	QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
280 	program = this->document()->toPlainText();
281 	lines = program.split(QRegExp("\\n"));
282 	for (int i = 0; i < lines.size(); i++) {
283 		QString line = lines.at(i);
284 		line = line.trimmed();
285         if(line.isEmpty()){
286             // label - empty line no indent
287             indentThisLine = false;
288         } else if (line.contains(QRegExp("^\\S+[:]"))) {
289 			// label - one line no indent
290 			indentThisLine = false;
291 		} else if (line.contains(QRegExp("^(for)|(foreach)\\s", Qt::CaseInsensitive))) {
292 			// for - indent next (block of code)
293 			increaseIndent = true;
294 		} else if (line.contains(QRegExp("^next(\\s)", Qt::CaseInsensitive))) {
295 			// next var - come out of block - reduce indent
296 			decreaseIndent = true;
297 		} else if (line.contains(QRegExp("^next$", Qt::CaseInsensitive))) {
298 			// next - come out of block - reduce indent
299 			decreaseIndent = true;
300 		} else if (line.contains(QRegExp("^if\\s.+\\sthen\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
301 			// if/then (NOTHING FOLLOWING) - indent next (block of code)
302 			increaseIndent = true;
303 		} else if (line.contains(QRegExp("^else\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
304 			// else - come out of block and start new block
305 			decreaseIndent = true;
306 			increaseIndent = true;
307 		} else if (line.contains(QRegExp("^end\\s*if\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
308 			// end if - come out of block - reduce indent
309 			decreaseIndent = true;
310 		} else if (line.contains(QRegExp("^while\\s", Qt::CaseInsensitive))) {
311 			// while - indent next (block of code)
312 			increaseIndent = true;
313 		} else if (line.contains(QRegExp("^end\\s*while\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
314 			// endwhile - come out of block
315 			decreaseIndent = true;
316 		} else if (line.contains(QRegExp("^function\\s", Qt::CaseInsensitive))) {
317 			// function - indent next (block of code)
318 			increaseIndent = true;
319 		} else if (line.contains(QRegExp("^end\\s*function\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
320 			// endfunction - come out of block
321 			decreaseIndent = true;
322 		} else if (line.contains(QRegExp("^subroutine\\s", Qt::CaseInsensitive))) {
323 			// function - indent next (block of code)
324 			increaseIndent = true;
325 		} else if (line.contains(QRegExp("^end\\s*subroutine\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
326 			// endfunction - come out of block
327 			decreaseIndent = true;
328 		} else if (line.contains(QRegExp("^do\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
329 			// do - indent next (block of code)
330 			increaseIndent = true;
331 		} else if (line.contains(QRegExp("^until\\s", Qt::CaseInsensitive))) {
332 			// until - come out of block
333 			decreaseIndent = true;
334 		} else if (line.contains(QRegExp("^try\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
335 			// try indent next (block of code)
336 			increaseIndent = true;
337 		} else if (line.contains(QRegExp("^catch\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
338 			// catch - come out of block and start new block
339 			decreaseIndent = true;
340 			increaseIndent = true;
341 		} else if (line.contains(QRegExp("^end\\s*try\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
342 			// end try - come out of block - reduce indent
343 			decreaseIndent = true;
344 		} else if (line.contains(QRegExp("^begin\\s*case\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
345 			// begin case double indent next (block of code)
346 			increaseIndentDouble = true;
347 		} else if (line.contains(QRegExp("^end\\s*case\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
348 			// end case double reduce
349 			decreaseIndentDouble = true;
350 		} else if (line.contains(QRegExp("^case\\s.+\\s*((#|(rem\\s)).*)?$", Qt::CaseInsensitive))) {
351 			// case expression - indent one line
352 			decreaseIndent = true;
353 			increaseIndent = true;
354 		}
355 		//
356 		if (decreaseIndent) {
357 			indent--;
358 			if (indent<0) indent=0;
359 			decreaseIndent = false;
360 		}
361 		if (decreaseIndentDouble) {
362 			indent-=2;
363 			if (indent<0) indent=0;
364 			decreaseIndentDouble = false;
365 		}
366 		if (indentThisLine) {
367 			line = QString(indent, QChar('\t')) + line;
368 		} else {
369 			indentThisLine = true;
370 		}
371 		if (increaseIndent) {
372 			indent++;
373 			increaseIndent = false;
374 		}
375 		if (increaseIndentDouble) {
376 			indent+=2;
377 			increaseIndentDouble = false;
378 		}
379 		//
380 		lines.replace(i, line);
381 	}
382     //this->setPlainText(lines.join("\n"));
383     QTextCursor cursor = this->textCursor();
384     cursor.select(QTextCursor::Document);
385     cursor.insertText(lines.join("\n"));
386 	QApplication::restoreOverrideCursor();
387 }
388 
findString(QString s,bool reverse,bool casesens,bool words)389 void BasicEdit::findString(QString s, bool reverse, bool casesens, bool words)
390 {
391 	if(s.length()==0) return;
392 	QTextDocument::FindFlags flag;
393 	if (reverse) flag |= QTextDocument::FindBackward;
394 	if (casesens) flag |= QTextDocument::FindCaseSensitively;
395 	if (words) flag |= QTextDocument::FindWholeWords;
396 
397 	QTextCursor cursor = this->textCursor();
398 	// here we save the cursor position and the verticalScrollBar value
399     QTextCursor cursorSaved = cursor;
400     int scroll = verticalScrollBar()->value();
401 
402     if (!find(s, flag))
403 	{
404         //nothing is found | jump to start/end
405         setUpdatesEnabled(false);
406         cursor.movePosition(reverse?QTextCursor::End:QTextCursor::Start);
407         setTextCursor(cursor);
408 
409         if (!find(s, flag))
410         {
411             // word not found : we set the cursor back to its initial position and restore verticalScrollBar value
412 			setTextCursor(cursorSaved);
413             verticalScrollBar()->setValue(scroll);
414             setUpdatesEnabled(true);
415             QMessageBox::information(this, tr("Find"),
416                 tr("String not found."),
417                 QMessageBox::Ok, QMessageBox::Ok);
418         }else{
419             //start from current screen position -> no scroll or scroll only as needed
420             //if next search is in the same view
421             verticalScrollBar()->setValue(scroll);
422             // we can use ensureCursorVisible() but we want to make sure that entire selection is visible
423             cursor = this->textCursor();
424             cursorSaved = cursor;
425             cursor.setPosition(cursor.selectionStart());
426             setTextCursor(cursor);
427             setTextCursor(cursorSaved);
428             setUpdatesEnabled(true);
429         }
430     }
431 }
432 
replaceString(QString from,QString to,bool reverse,bool casesens,bool words,bool doall)433 void BasicEdit::replaceString(QString from, QString to, bool reverse, bool casesens, bool words, bool doall) {
434 	if(from.length()==0) return;
435 
436 	// Replace one time.
437 	if(!doall){
438 		//Replace if text is selected - use the cursor from the last find not the copy
439         QTextCursor cursor = this->textCursor();
440         if (from.compare(cursor.selectedText(),(casesens ? Qt::CaseSensitive : Qt::CaseInsensitive))==0){
441 			cursor.insertText(to);
442 		}
443 
444 		//Make a search
445         findString(from, reverse, casesens, words);
446 
447     //Replace all
448     }else{
449         QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
450         setUpdatesEnabled(false);
451         QTextCursor cursor = this->textCursor();
452         QTextCursor cursorSaved = cursor;
453         int scroll = verticalScrollBar()->value();
454         cursor.beginEditBlock();
455         int n = 0;
456 		cursor.movePosition(QTextCursor::Start);
457 		setTextCursor(cursor);
458         QTextDocument::FindFlags flag;
459         if (casesens) flag |= QTextDocument::FindCaseSensitively;
460         if (words) flag |= QTextDocument::FindWholeWords;
461 		while (find(from, flag)){
462 			if (textCursor().hasSelection()){
463 				textCursor().insertText(to);
464 				n++;
465 			}
466 		}
467 		// set the cursor back to its initial position and restore verticalScrollBar value
468         setTextCursor(cursorSaved);
469         verticalScrollBar()->setValue(scroll);
470         cursor.endEditBlock();
471         setUpdatesEnabled(true);
472         QApplication::restoreOverrideCursor();
473         if(n==0)
474 			QMessageBox::information(this, tr("Replace"),
475 				tr("String not found."),
476 				QMessageBox::Ok, QMessageBox::Ok);
477 		else
478             QMessageBox::information(this, tr("Replace"),
479 				tr("Replace completed.") + "\n" + QString::number(n) + " " + tr("occurrence(s) were replaced."),
480 				QMessageBox::Ok, QMessageBox::Ok);
481 	}
482 }
483 
getCurrentWord()484 QString BasicEdit::getCurrentWord() {
485     // get the cursor and find the current word on this line
486     // anything but letters delimits
487     QString w;
488     QTextCursor t(textCursor());
489     QTextBlock b(t.block());
490     w = b.text();
491     w = w.left(w.indexOf(QRegExp("[^a-zA-Z0-9]"),t.positionInBlock()));
492     w = w.mid(w.lastIndexOf(QRegExp("[^a-zA-Z0-9]"))+1);
493     return w;
494 }
495 
lineNumberAreaWidth()496 int BasicEdit::lineNumberAreaWidth() {
497     int digits = 1;
498     int max = qMax(1, blockCount());
499     while (max >= 10) {
500         max /= 10;
501         ++digits;
502     }
503     int space = 10 + fontMetrics().width(QLatin1Char('9')) * digits;
504     return space;
505 }
506 
updateLineNumberAreaWidth(int)507 void BasicEdit::updateLineNumberAreaWidth(int /* newBlockCount */) {
508     //update setViewportMargins only when lineNumberAreaWidth is changed - save CPU
509     //(this signal is emitted even when user scroll up or scroll down code)
510     int w = lineNumberAreaWidth();
511     if(w!=lastLineNumberAreaWidth){
512         lastLineNumberAreaWidth=w;
513         setViewportMargins(w, 0, 0, 0);
514     }
515 }
516 
updateLineNumberArea(const QRect & rect,int dy)517 void BasicEdit::updateLineNumberArea(const QRect &rect, int dy) {
518     if (dy) {
519         lineNumberArea->scroll(0, dy);
520     } else {
521         lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
522     }
523 }
524 
525 
526 
highlightCurrentLine()527 void BasicEdit::highlightCurrentLine() {
528     if(guiState==GUISTATEAPP) return;
529     QList<QTextEdit::ExtraSelection> extraSelections;
530     QTextEdit::ExtraSelection blockSelection;
531     QTextEdit::ExtraSelection selection;
532     QColor lineColor;
533     QColor blockColor;
534 
535     if(guiState==GUISTATERUN || runState==RUNSTATERUN){
536         // editor is in readOnly mode so line will be red (forbidden)
537         lineColor = QColor(Qt::red).lighter(175);
538         blockColor = QColor(Qt::red).lighter(190);
539     }else if (runState==RUNSTATERUNDEBUG || runState==RUNSTATEDEBUG) {
540         // if we are executing in debug mode
541         lineColor = QColor(Qt::green).lighter(175);
542         blockColor = lineColor;
543     }else{
544         // in edit mode
545         lineColor = QColor(Qt::yellow).lighter(165);
546         blockColor = QColor(Qt::yellow).lighter(190);
547     }
548 
549     blockSelection.format.setBackground(blockColor);
550     blockSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
551     blockSelection.cursor = textCursor();
552     blockSelection.cursor.movePosition(QTextCursor::StartOfBlock,QTextCursor::MoveAnchor);
553     blockSelection.cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::KeepAnchor);
554     extraSelections.append(blockSelection);
555 
556     selection.format.setBackground(lineColor);
557     selection.format.setProperty(QTextFormat::FullWidthSelection, true);
558     selection.cursor = textCursor();
559     selection.cursor.clearSelection();
560     extraSelections.append(selection);
561 
562 
563     //mark brackets if we are editing
564 	if (runState==RUNSTATESTOP && !isReadOnly()) {
565         QTextCursor cur = textCursor();
566         int pos = cur.position();
567         cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
568         cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
569         int posDoc = cur.position();
570         pos = pos - posDoc -1;
571         QString s = cur.selectedText(); //current line as QString
572         int length = s.length();
573         QChar cPrev, cNext;
574 
575 
576         if(length>1){
577             QChar open;
578             QChar close;
579             QChar c;
580             bool lookBefore = false;
581             bool lookAfter = false;
582             int quote=0;
583             int doubleQuote=0;
584             int i = 0;
585             const QColor bracketColor = QColor(Qt::green).lighter(165);
586             QTextEdit::ExtraSelection selectionBracket;
587 
588             if(pos>0){
589                 cPrev = s.at(pos);
590                 if(cPrev==')'){
591                     open = '(';
592                     lookBefore = true;
593                 }else if(cPrev==']'){
594                     open = '[';
595                     lookBefore = true;
596                 }else if(cPrev=='}'){
597                     open = '{';
598                     lookBefore = true;
599                 }
600             }
601 
602             if(pos<length-1){
603                 cNext = s.at(pos+1);
604                 if(cNext=='('){
605                     close = ')';
606                     lookAfter = true;
607                 }else if(cNext=='['){
608                     close = ']';
609                     lookAfter = true;
610                 }else if(cNext=='{'){
611                     close = '}';
612                     lookAfter = true;
613                 }
614             }
615 
616             if(lookBefore){
617                 //check for open bracket
618                 QList<int> list;
619                 for(i=0;i<pos;i++){
620                     c=s.at(i);
621                     if(c=='\"' && quote==0){
622                         doubleQuote^=1;
623                     }else if(c=='\'' && doubleQuote==0){
624                         quote^=1;
625                     }else if(quote==0 && doubleQuote==0){
626                         if(c==open){
627                             list.append(i);
628                         }else if(c==cPrev){
629                             if(!list.isEmpty())
630                                 list.removeLast();
631                         }
632                     }
633                 }
634                 if(quote==0 && doubleQuote==0 && !list.isEmpty()){
635                     //mark current bracket and the corresponding bracket
636                     selectionBracket.format.setBackground(bracketColor);
637                     selectionBracket.cursor = textCursor();
638                     selectionBracket.cursor.setPosition(pos+posDoc+1);
639                     selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
640                     extraSelections.append(selectionBracket);
641                     selectionBracket.cursor.setPosition(list.last()+posDoc+1);
642                     selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
643                     extraSelections.append(selectionBracket);
644                 }
645             }else if(lookAfter){
646                 //else check only for quotes if lookAfter
647                 for(i=0;i<pos;i++){
648                     c=s.at(i);
649                     if(c=='\"' && quote==0){
650                         doubleQuote^=1;
651                     }else if(c=='\'' && doubleQuote==0){
652                         quote^=1;
653                     }
654                 }
655             }
656 
657             if(lookAfter && quote==0 && doubleQuote==0){
658                 //check for bracket if current bracket is not quoted
659                 int count = 1;
660                 for(i=pos+2;i<length;i++){
661                     c=s.at(i);
662                     if(c=='\"' && quote==0){
663                         doubleQuote^=1;
664                     }else if(c=='\'' && doubleQuote==0){
665                         quote^=1;
666                     }else if(quote==0 && doubleQuote==0){
667                         if(c==cNext){
668                             count++;
669                         }else if(c==close){
670                             count--;
671                             if(count==0){
672                                 //mark current bracket and the corresponding bracket
673                                 selectionBracket.format.setBackground(bracketColor);
674                                 selectionBracket.cursor = textCursor();
675                                 selectionBracket.cursor.setPosition(pos+posDoc+1);
676                                 selectionBracket.cursor.movePosition(QTextCursor::NextCharacter,QTextCursor::KeepAnchor);
677                                 extraSelections.append(selectionBracket);
678                                 selectionBracket.cursor.setPosition(i+posDoc+1);
679                                 selectionBracket.cursor.movePosition(QTextCursor::PreviousCharacter,QTextCursor::KeepAnchor);
680                                 extraSelections.append(selectionBracket);
681                                 break;
682                             }
683                         }
684                     }
685                 }
686             }
687         }
688     }
689     setExtraSelections(extraSelections);
690 }
691 
692 
resizeEvent(QResizeEvent * e)693 void BasicEdit::resizeEvent(QResizeEvent *e) {
694     QPlainTextEdit::resizeEvent(e);
695     if(lineNumberArea){
696         QRect cr = contentsRect();
697         lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
698     }
699 }
700 
701 
lineNumberAreaPaintEvent(QPaintEvent * event)702 void BasicEdit::lineNumberAreaPaintEvent(QPaintEvent *event) {
703     //We need lastBlockNumber to adjust Qt bug (see below)
704     QTextCursor t(textCursor());
705     int currentBlockNumber = t.blockNumber();
706     t.movePosition(QTextCursor::End);
707     int lastBlockNumber = t.block().blockNumber();
708 
709     QPainter painter(lineNumberArea);
710     painter.fillRect(event->rect(), Qt::lightGray);
711 
712     QTextBlock block = firstVisibleBlock();
713     int blockNumber = block.blockNumber();
714     int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
715     int bottom = top + (int) blockBoundingRect(block).height();
716     int y = -1;
717 
718     if(lineNumberArea->underMouse())
719         y = lineNumberArea->mapFromGlobal(QCursor::pos()).y();
720 
721     while (block.isValid() && top <= event->rect().bottom()) {
722         if (block.isVisible() && bottom > event->rect().top()) {
723             QString number = QString::number(blockNumber + 1);
724 
725             //Draw lighter background for current paragraph
726             //For this to work (and to correctly change the color of current paragraph number) highlightCurrentLine()
727             //must set a specialSelection for entire current block. If not, QPaintEvent will update only
728             //a small portion of lineNumberAreaPaint (just current line, not entire paragraph), acording with Automatic Clipping.
729             //Also there is a bug with last block. See http://stackoverflow.com/questions/37890906/qt-linenumberarea-example-blockboundingrectlastblock
730             //or http://www.forum.crossplatform.ru/index.php?showtopic=3963
731             if (blockNumber==currentBlockNumber)
732                 painter.fillRect(0,top,lineNumberArea->width(),bottom-top-(blockNumber==lastBlockNumber?3:0), QColor(Qt::lightGray).lighter(110));
733             else if(top <= y && bottom > y && y>=0 && runState != RUNSTATERUN)
734                 painter.fillRect(0,top,lineNumberArea->width(),bottom-top-(blockNumber==lastBlockNumber?3:0), QColor(Qt::lightGray).lighter(104));
735 
736             // Draw breakpoints
737             if (block.userState()==STATEBREAKPOINT) {
738                     painter.setBrush(Qt::red);
739                     painter.setPen(Qt::red);
740                     int w = lineNumberArea->width();
741                     int bh = blockBoundingRect(block).height();
742                     int fh = fontMetrics().height();
743                     painter.drawEllipse((w-(fh-6))/2, top+(bh-(fh-6))/2, fh-6, fh-6);
744             }
745             // draw text
746             if (blockNumber==currentBlockNumber) {
747                 painter.setPen(Qt::blue);
748             } else {
749                 painter.setPen(Qt::black);
750             }
751             painter.drawText(0, top, lineNumberArea->width()-5, fontMetrics().height(), Qt::AlignRight, number);
752         }
753 
754         block = block.next();
755         top = bottom;
756         bottom = top + (int) blockBoundingRect(block).height();
757         ++blockNumber;
758     }
759 }
760 
761 
lineNumberAreaMouseWheelEvent(QWheelEvent * event)762 void BasicEdit::lineNumberAreaMouseWheelEvent(QWheelEvent *event) {
763     QPlainTextEdit::wheelEvent(event);
764 }
765 
766 
767 //Breakpoints are stick to each paragraph by using setUserState to avoid messing up while delete/edit/drop/paste large sections of text.
768 //breakPoints list is also needed by interpretor. Before running in debug mode breakPoints list is update by calling updateBreakPointsList().
769 //Because setting/removing breakpoints is enabled in debug mode, we set/unset breakpoints in breakPoints list too.
lineNumberAreaMouseClickEvent(QMouseEvent * event)770 void BasicEdit::lineNumberAreaMouseClickEvent(QMouseEvent *event) {
771     if (runState == RUNSTATERUN)
772         return;
773     // based on mouse click - set the breakpoint in the map/block and highlight the line
774 	int line;
775 	QTextBlock block = firstVisibleBlock();
776     int bottom = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); //bottom from previous block
777     line = block.blockNumber();
778 	// line 0 ... (n-1) of what was clicked
779 	while(block.isValid()) {
780 		bottom += blockBoundingRect(block).height();
781 		if (event->y() < bottom) {
782             if(event->button() == Qt::LeftButton){
783                 //keep breakPoints list update for debug running mode
784                 if(block.userState()==STATEBREAKPOINT){
785                     block.setUserState(STATECLEAR);
786                     if (breakPoints->contains(line))
787                         breakPoints->removeAt(breakPoints->indexOf(line,0));
788                 }else{
789                     block.setUserState(STATEBREAKPOINT);
790                     breakPoints->append(line);
791                 }
792                 lineNumberArea->repaint();
793             }else if(event->button() == Qt::RightButton){
794                 rightClickBlockNumber = line;
795                 QMenu contextMenu(this);
796                 if(block.userState()==STATEBREAKPOINT)
797                     contextMenu.addAction ( tr("Remove breakpoint from line") + " " + QString::number(line+1), this , SLOT (toggleBreakPoint()) );
798                 else
799                     contextMenu.addAction ( tr("Set breakpoint at line") + " " + QString::number(line+1), this , SLOT (toggleBreakPoint()) );
800                 QAction *act = contextMenu.addAction ( tr("Clear all breakpoints") , this , SLOT (clearBreakPoints()) );
801                 act->setEnabled(isBreakPoint());
802                 contextMenu.exec (event->globalPos());
803             }
804             return;
805         }
806 		block = block.next();
807 		line++;
808 	}
809     QMenu contextMenu(this);
810     contextMenu.addAction ( tr("Clear all breakpoints") , this , SLOT (clearBreakPoints()) );
811     contextMenu.exec (event->globalPos());
812 }
813 
toggleBreakPoint()814 void BasicEdit::toggleBreakPoint(){
815     if(rightClickBlockNumber<0)
816         return;
817     QTextBlock block = firstVisibleBlock();
818     int n = block.blockNumber();
819     if(n<rightClickBlockNumber){
820         for(;n<rightClickBlockNumber;n++){
821             block = block.next();
822         }
823     }else if(n>rightClickBlockNumber){
824         for(;n>rightClickBlockNumber;n--){
825             block = block.previous();
826         }
827     }
828     if(block.isValid()){
829         if(block.blockNumber()==rightClickBlockNumber){
830             //keep breakPoints list update for debug running mode
831             if(block.userState()==STATEBREAKPOINT){
832                 block.setUserState(STATECLEAR);
833                 if (breakPoints->contains(rightClickBlockNumber))
834                     breakPoints->removeAt(breakPoints->indexOf(rightClickBlockNumber,0));
835             }else{
836                 block.setUserState(STATEBREAKPOINT);
837                 breakPoints->append(rightClickBlockNumber);
838             }
839             lineNumberArea->repaint();
840         }
841     }
842     rightClickBlockNumber=-1;
843 }
844 
clearBreakPoints()845 void BasicEdit::clearBreakPoints() {
846     // remove all breakpoints
847     QTextBlock b(document()->firstBlock());
848     while (b.isValid()){
849         b.setUserState(STATECLEAR);
850         b = b.next();
851     }
852     breakPoints->clear();
853     lineNumberArea->repaint();
854 }
855 
isBreakPoint()856 bool BasicEdit::isBreakPoint() {
857     // check if there are breakpoints to be cleared (usefull to update menu)
858     QTextBlock b(document()->firstBlock());
859     while (b.isValid()){
860         if(b.userState()==STATEBREAKPOINT)
861             return true;
862         b = b.next();
863     }
864     return false;
865 }
866 
updateBreakPointsList()867 void BasicEdit::updateBreakPointsList() {
868     breakPoints->clear();
869     QTextBlock b(document()->firstBlock());
870     while (b.isValid()){
871         if(b.userState()==STATEBREAKPOINT)
872             breakPoints->append(b.blockNumber());
873         b = b.next();
874     }
875 }
876 
877 
indentSelection()878 int BasicEdit::indentSelection() {
879 	QTextCursor cur = textCursor();
880 	if(!cur.hasSelection())
881 			return false;
882 	int a = cur.anchor();
883 	int p = cur.position();
884 	int start = (a<=p?a:p);
885 	int end = (a>p?a:p);
886 
887 	cur.beginEditBlock();
888 	cur.setPosition(end);
889 	int eblock = cur.block().blockNumber();
890 	cur.setPosition(start);
891 	int sblock = cur.block().blockNumber();
892 
893 	for(int i = sblock; i <= eblock; i++)
894 	{
895 		cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
896 		cur.insertText("\t");
897 		cur.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
898 	}
899 	cur.endEditBlock();
900 return true;
901 }
902 
903 
unindentSelection()904 void BasicEdit::unindentSelection() {
905 	QTextCursor cur = textCursor();
906 	int a = cur.anchor();
907 	int p = cur.position();
908 	int start = (a<=p?a:p);
909 	int end = (a>p?a:p);
910 
911 	cur.beginEditBlock();
912 	cur.setPosition(end);
913 	int eblock = cur.block().blockNumber();
914 	cur.setPosition(start);
915 	int sblock = cur.block().blockNumber();
916 	QString s;
917 
918 	for(int i = sblock; i <= eblock; i++)
919 	{
920 		cur.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
921 		cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
922 		s = cur.selectedText();
923 		if(!s.isEmpty()){
924 			if(s.startsWith("    ") || s.startsWith("   \t")){
925 				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
926 				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4);
927 				cur.removeSelectedText();
928 			}else if(s.startsWith("   ") || s.startsWith("  \t")){
929 				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
930 				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 3);
931 				cur.removeSelectedText();
932 			}else if(s.startsWith("  ") || s.startsWith(" \t")){
933 				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
934 				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 2);
935 				cur.removeSelectedText();
936 			}else if(s.startsWith(" ") || s.startsWith("\t")){
937 				cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
938 				cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1);
939 				cur.removeSelectedText();
940 			}
941 		}
942 		cur.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
943 	}
944 	cur.endEditBlock();
945 }
946 
setTitle(QString newTitle)947 void BasicEdit::setTitle(QString newTitle){
948     title = newTitle;
949     updateTitle();
950 }
951 
updateTitle()952 void BasicEdit::updateTitle(){
953     windowtitle = (document()->isModified()?"*":"") + title;
954     action->setText(windowtitle);
955     emit(updateWindowTitle(this));
956 }
957 
dragEnterEvent(QDragEnterEvent * event)958 void BasicEdit::dragEnterEvent(QDragEnterEvent *event){
959     if (event->mimeData()->hasFormat("text/plain")){
960         event->acceptProposedAction();
961         QPlainTextEdit::dragEnterEvent(event);
962     }else{
963         event->ignore();
964     }
965 }
966 
dropEvent(QDropEvent * event)967 void BasicEdit::dropEvent(QDropEvent *event){
968     if (event->mimeData()->hasFormat("text/plain")){
969         event->acceptProposedAction();
970         QPlainTextEdit::dropEvent(event);
971     }else{
972         event->ignore();
973     }
974 }
975 
setEditorRunState(int state)976 void BasicEdit::setEditorRunState(int state){
977     if(runState == state) return;
978     runState = state;
979     if(guiState!=GUISTATEAPP){
980         if(state==RUNSTATESTOP&&guiState==GUISTATENORMAL){
981             setReadOnly(false);
982             setTextInteractionFlags(Qt::TextEditorInteraction);
983             if(fileChangedOnDiskFlag) handleFileChangedOnDisk();
984         } else if(state==RUNSTATERUN||guiState!=GUISTATENORMAL){
985             setReadOnly(true);
986         }else{
987             //Just like readonly but user can not change current line and highlight another one (need in debug mode)
988             setTextInteractionFlags(Qt::NoTextInteraction);
989         }
990         //setDisabled(!(state==RUNSTATESTOP&&guiState==GUISTATENORMAL));
991         highlightCurrentLine();
992     }
993 }
994 
slotUndoAvailable(bool val)995 void BasicEdit::slotUndoAvailable(bool val){
996     undoButton = val;
997     emit(updateEditorButtons());
998 }
999 
slotRedoAvailable(bool val)1000 void BasicEdit::slotRedoAvailable(bool val){
1001     redoButton = val;
1002     emit(updateEditorButtons());
1003 }
slotCopyAvailable(bool val)1004 void BasicEdit::slotCopyAvailable(bool val){
1005     copyButton = val;
1006     emit(updateEditorButtons());
1007 }
1008 
actionWasTriggered()1009 void BasicEdit::actionWasTriggered(){
1010     emit(setCurrentEditorTab(this));
1011 }
1012 
fileChangedOnDiskSlot(QString fn)1013 void BasicEdit::fileChangedOnDiskSlot(QString fn){
1014     if(fn==filename){
1015         if(runState==RUNSTATESTOP){
1016             if(!fileChangedOnDiskFlag){
1017                 //avoid fileChangedOnDiskSlot to be fired multiple times while program waits user confirmation
1018                 fileChangedOnDiskFlag=true;
1019                 handleFileChangedOnDisk();
1020             }
1021         }else{
1022             fileChangedOnDiskFlag=true;
1023         }
1024     }
1025 }
1026 
handleFileChangedOnDisk()1027 void BasicEdit::handleFileChangedOnDisk(){
1028     document()->setModified(true);
1029     updateTitle();
1030     emit(setCurrentEditorTab(this));
1031     QFileInfo check_file(filename);
1032     if(!check_file.exists()){
1033         QMessageBox::information(this, tr("File has been removed."),
1034         tr("It seems that file %1 was removed from disk. Don't forget to save your work.").arg(filename));
1035     }else{
1036         if( QMessageBox::Yes == QMessageBox::warning(this, tr("File changed"),
1037         tr("The file %1 has changed outside BASIC256 editor. Do you want to reload it?").arg(filename),
1038         QMessageBox::Yes | QMessageBox::No, QMessageBox::No)){
1039             //reload changed file from disk but keep undo history to allow reversing this action
1040             QFile f(filename);
1041             if(f.open(QIODevice::ReadOnly)){
1042                 QByteArray ba = f.readAll();
1043                 f.close();
1044                 QTextCursor cursor = this->textCursor();
1045                 cursor.beginEditBlock();
1046                 cursor.select(QTextCursor::Document);
1047                 cursor.insertText(QString::fromUtf8(ba.data()));
1048                 cursor.movePosition(QTextCursor::Start);
1049                 setTextCursor(cursor);
1050                 cursor.endEditBlock();
1051                 document()->setModified(false);
1052                 updateTitle();
1053             }else{
1054                 QMessageBox::critical(this, tr("Load File"), tr("Unable to open program file") + " \"" + filename + "\".\n" + tr("File permissions problem or file open by another process."));
1055             }
1056         }
1057     }
1058     fileChangedOnDiskFlag=false;
1059 }
1060