#include <QWidget>
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
#include <QFileDialog>
#include <QCloseEvent>
#include <QTextDocumentFragment>
#include <QSyntaxHighlighter>
#include "Utilities.h"
#include "MdiChild.h"
#include "DocConvert.h"
#include "ui_ExtensibleObjectModelUI.h"
#include <unistd.h>
extern void RemoveFile(const char* fileName);
class MarkdownHighlighter : public QSyntaxHighlighter
{
public:
enum Construct
{
Heading, // # text #
NewPage, // --<<>>--
HorizontalRule, // --
Italics, // *text*
Bold, // **text**
Quoted, // `text`
BlockQuote, // >text
Code, // ``` \n text \n ```
Link, // [text] / [[text]]
ListItem, // \n -|1.|* text
LastConstruct = ListItem
};
MarkdownHighlighter(QTextDocument *document)
: QSyntaxHighlighter(document)
{
setFormatFor(Bold, 5, true); // **text**
setFormatFor(Heading, 2, true); // # text #
setFormatFor(HorizontalRule, 1, false); // --
setFormatFor(NewPage, 3, false); // --<<>>--
setFormatFor(BlockQuote, 0, false, true); // >text
setFormatFor(Quoted, 0, false, true); // `text`
setFormatFor(Italics, 6, false, true); // *text*
setFormatFor(Code, 2, false); // ``` \n text \n ```
setFormatFor(Link, 10, false); // [text] / [[text]]
setFormatFor(ListItem, 9, false); // \n -|1.|* text
setCurrentBlockState(NormalState);
}
void setFormatFor(Construct construct, int palIdx, bool bold = false, bool italic = false)
{
// TODO: Make it user customizable
static const uint32_t palette[] =
{
0xa082bd, 0xc4c8cc, 0x79b541, 0xd5a400, 0xec7600, 0xff850a,
0xd5cb7d, 0xd39745, 0x4e5a5f, 0x678cb1, 0x5899c0, 0x5ab9be,
0x1a252a, 0x24333a, 0xe1c70d, 0xffffff, 0x3c5975, 0x374043
};
uint32_t color = palette[palIdx];
int r = (color & 0xff0000) >> 16;
int g = (color & 0x00ff00) >> 8;
int b = (color & 0x0000ff) >> 0;
QTextCharFormat format;
format.setForeground(QColor(r, g, b));
if (bold)
format.setFontWeight(QFont::Bold);
format.setFontItalic(italic);
m_formats[construct] = format;
rehighlight();
}
QTextCharFormat formatFor(Construct construct) const
{
return m_formats[construct];
}
protected:
enum State
{
NormalState = -1,
InCode
};
bool ApplyRegExFormat(const QString &text, Construct construct, QString regEx)
{
bool found = false;
QRegularExpression expression(regEx);
QRegularExpressionMatchIterator i = expression.globalMatch(text);
while (i.hasNext())
{
QRegularExpressionMatch match = i.next();
setFormat(match.capturedStart(), match.capturedLength(), m_formats[construct]);
found = true;
}
return found;
}
void highlightBlock(const QString &text)
{
int state = previousBlockState();
ApplyRegExFormat(text, BlockQuote, "^>.*$"); // >text
ApplyRegExFormat(text, Link, "\\[.*\\]"); // [text] / [[text]]
ApplyRegExFormat(text, ListItem, "^\\s*[-|\\*]\\s*"); // \n -|*
ApplyRegExFormat(text, ListItem, "^\\s*\\d+\\.\\s*"); // \n 1.
ApplyRegExFormat(text, Heading, "^\\s*[\\#]+ .*"); // # text #
ApplyRegExFormat(text, HorizontalRule, "^\\s*---*"); // --
ApplyRegExFormat(text, NewPage, "^\\s*-*-<<>>--*"); // --<<>>--
ApplyRegExFormat(text, Italics, "\\*[^\\*]*\\*"); // *text*
ApplyRegExFormat(text, Bold, "\\*\\*[^\\*]*\\*\\*"); // **text**
ApplyRegExFormat(text, Quoted, "\\`.*\\`"); // `text`
// Search for in-code sections
QRegularExpression expression("```");
QRegularExpressionMatchIterator i = expression.globalMatch(text);
int lastIndex = 0;
while (i.hasNext())
{
QRegularExpressionMatch match = i.next();
if (state == InCode)
{
state = NormalState;
setFormat(lastIndex, (match.capturedStart() + 3) - lastIndex, m_formats[Code]);
}
else
{
state = InCode;
lastIndex = match.capturedStart();
}
}
if (state == InCode)
{
setFormat(lastIndex, text.size() - lastIndex, m_formats[Code]);
}
setCurrentBlockState(state);
}
private:
QTextCharFormat m_formats[LastConstruct + 1];
};
/*!
* \brief MdiChild::MdiChild
*
* Implementation Status: ?
*
*/
MdiChild::MdiChild()
{
#if NON_TABBED_CHILD
editor = this;
preview = nullptr;
#else
editor = new QTextEdit;
preview = new QTextEdit;
addTab(editor, "Plain");
addTab(preview, "Formatted");
#endif
highlighter = new MarkdownHighlighter(editor->document());
setAttribute(Qt::WA_DeleteOnClose);
isUntitled = true;
connect(editor->document(), SIGNAL(contentsChanged()), this, SIGNAL(documentContentsChanged()));
// TODO: make user setting
editor->setStyleSheet("QTextEdit { font: 12pt \"Courier\"; }");
editor->setAcceptRichText(false);
}
/*!
* \brief MdiChild::~MdiChild
*
* Implementation Status: ?
*
*/
MdiChild::~MdiChild()
{
if (!tmpFile.isEmpty())
RemoveFile(tmpFile.toLatin1().data());
closing();
}
/*!
* \brief MdiChild::newFile
*
* Implementation Status: ?
*
*/
void MdiChild::newFile()
{
static int sequenceNumber = 1;
isUntitled = true;
curFile = tr("document%1.txt").arg(sequenceNumber++);
setWindowTitle(curFile + "[*]");
connect(editor->document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified()));
documentWasModified();
newTempName();
}
void MdiChild::newTempName()
{
if (!tmpFile.isEmpty())
RemoveFile(tmpFile.toLatin1().data());
// TODO: this is platform specific with the path etc Also race condition
// may need to change the concept to deal with outputting to a file handle
// instead of a file name
char tmpBuffer[] = "/var/tmp/tmpfile-XXXXXX.pdf";
int fd = mkstemps(tmpBuffer, 4);
::close(fd);
tmpFile = tmpBuffer;
//tmpnam("/var/tmp/tmpfile");
//tmpFile += ".pdf";
}
/*!
* \brief MdiChild::loadFile
* \param fileName
* \return
*
* Implementation Status: ?
*
*/
bool MdiChild::loadFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(this, tr("MDI"),
tr("Cannot read file %1:\n%2.").arg(fileName).arg(file.errorString()));
return false;
}
QTextStream in(&file);
QApplication::setOverrideCursor(Qt::WaitCursor);
editor->setPlainText(in.readAll());
QApplication::restoreOverrideCursor();
setCurrentFile(fileName);
connect(editor->document(), SIGNAL(contentsChanged()), this, SLOT(documentWasModified()));
documentWasModified();
newTempName();
return true;
}
/*!
* \brief MdiChild::save
* \return
*
* Implementation Status: ?
*
*/
bool MdiChild::save()
{
return (isUntitled) ? saveAs() : saveFile(curFile);
}
/*!
* \brief MdiChild::saveAs prompts for the file name to save the file as
* \return
*
* Implementation Status: ?
*
*/
bool MdiChild::saveAs()
{
QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), curFile);
if (fileName.isEmpty())
return false;
return saveFile(fileName);
}
/*!
* \brief MdiChild::saveFile
* \param fileName
* \return
*
* Implementation Status: ?
*
*/
bool MdiChild::saveFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text)) {
QMessageBox::warning(this, tr("MDI"),
tr("Cannot write file %1:\n%2.").arg(fileName).arg(file.errorString()));
return false;
}
QTextStream out(&file);
QApplication::setOverrideCursor(Qt::WaitCursor);
out << editor->toPlainText();
QApplication::restoreOverrideCursor();
setCurrentFile(fileName);
return true;
}
/*!
* \brief MdiChild::userFriendlyCurrentFile
* \return
*
* Implementation Status: ?
*
*/
QString MdiChild::userFriendlyCurrentFile()
{
return strippedName(curFile);
}
/*!
* \brief MdiChild::closeEvent
* \param event
*
* Implementation Status: ?
*
*/
void MdiChild::closeEvent(QCloseEvent *event)
{
if (maybeSave()) {
event->accept();
} else {
event->ignore();
}
}
/*!
* \brief MdiChild::documentWasModified
*
* Implementation Status: ?
*
*/
void MdiChild::documentWasModified()
{
setWindowModified(editor->document()->isModified());
// Convert to rich-text
char* data;
size_t size;
QByteArray text = editor->toPlainText().toLatin1();
DocConvert converter;
converter.SetSourceData(text.data(), text.size());
converter.Convert();
converter.GetHTMLData(&data, &size);
if (preview)
preview->setText(QString::fromLatin1(data, size));
}
/*!
* \brief MdiChild::maybeSave
* \return
*
* Implementation Status: ?
*
*/
bool MdiChild::maybeSave()
{
if (editor->document()->isModified()) {
QMessageBox::StandardButton ret;
ret = QMessageBox::warning(this, tr("MDI"),
tr("'%1' has been modified.\n"
"Do you want to save your changes?")
.arg(userFriendlyCurrentFile()),
QMessageBox::Save | QMessageBox::Discard
| QMessageBox::Cancel);
if (ret == QMessageBox::Save)
return save();
else if (ret == QMessageBox::Cancel)
return false;
}
return true;
}
/*!
* \brief MdiChild::setCurrentFile
* \param fileName
*
* Implementation Status: ?
*
*/
void MdiChild::setCurrentFile(const QString &fileName)
{
curFile = QFileInfo(fileName).canonicalFilePath();
isUntitled = false;
editor->document()->setModified(false);
setWindowModified(false);
setWindowTitle(userFriendlyCurrentFile() + "[*]");
}
/*!
* \brief MdiChild::strippedName
* \param fullFileName
* \return
*
* Implementation Status: ?
*
*/
QString MdiChild::strippedName(const QString &fullFileName)
{
return QFileInfo(fullFileName).fileName();
}
/*!
* \brief MdiChild::markdownSelection
* \param preText
* \param postText
*
* Implementation Status: ?
*
*/
void MdiChild::markdownSelection(const char* preText, const char* postText)
{
QTextCursor cur = editor->textCursor();
cur.beginEditBlock();
QString selectedText = cur.selection().toPlainText();
if (selectedText.isEmpty())
{
cur.select(QTextCursor::WordUnderCursor);
selectedText = cur.selection().toPlainText();
}
cur.removeSelectedText();
cur.insertText(preText + selectedText + postText);
cur.endEditBlock();
}
/*!
* \brief MdiChild::insertText
* \param text
*
* Implementation Status: ?
* TODO: Appears to replace current selection. Is that expected? Perhaps should only add text.
*
*/
void MdiChild::insertText(const char* text)
{
QTextCursor cur = editor->textCursor();
cur.beginEditBlock();
cur.insertText(text);
cur.endEditBlock();
}
/*!
* \brief MdiChild::bold
*
* Implementation Status: 50%
* TODO: modal bold? If no selection, click it, type, then click it again to turn off bold mode
*
*/
void MdiChild::bold()
{
markdownSelection("**", "**");
}
/*!
* \brief MdiChild::italic
*
* Implementation Status: ?
*
*/
void MdiChild::italic()
{
markdownSelection("*", "*");
}
/*!
* \brief MdiChild::quote
*
* Implementation Status: ?
*
*/
void MdiChild::quote()
{
markdownSelection("`", "`");
}
/*!
* \brief MdiChild::code
*
* Implementation Status: ?
*
*/
void MdiChild::code()
{
//markdownSelection("*", "*");
// Need to add indentation of text with 4 spaces each line
}
/*!
* \brief MdiChild::heading1
*
* Implementation Status: ?
*
*/
void MdiChild::heading1()
{
markdownSelection("# ", " #");
}
/*!
* \brief MdiChild::heading2
*
* Implementation Status: ?
*
*/
void MdiChild::heading2()
{
markdownSelection("## ", " ##");
}
/*!
* \brief MdiChild::heading3
*
* Implementation Status: ?
*
*/
void MdiChild::heading3()
{
markdownSelection("### ", " ###");
}
/*!
* \brief MdiChild::heading4
*
* Implementation Status: ?
*
*/
void MdiChild::heading4()
{
markdownSelection("#### ", " ####");
}
/*!
* \brief MdiChild::hyperlink
*
* Implementation Status: ?
*
*/
void MdiChild::hyperlink()
{
}
/*!
* \brief MdiChild::image
*
* Implementation Status: ?
*
*/
void MdiChild::image()
{
}
/*!
* \brief MdiChild::unorderedList
*
* Implementation Status: ?
*
*/
void MdiChild::unorderedList()
{
}
/*!
* \brief MdiChild::orderedList
*
* Implementation Status: ?
*
*/
void MdiChild::orderedList()
{
}
/*!
* \brief MdiChild::horizontalRule
*
* Implementation Status: 0%
*
*/
void MdiChild::horizontalRule()
{
insertText("----------");
}
/*!
* \brief MdiChild::timestamp
*
* Implementation Status: 0%
* TODO: Appears to replace current selection. Is that expected?
*
*/
void MdiChild::timestamp()
{
insertText("[TIMESTAMP]");
}
/*!
* \brief MdiChild::pageNumber
*
* Implementation Status: 0%
*
*/
void MdiChild::pageNumber()
{
insertText("[PAGENUM]");
}
/*!
* \brief MdiChild::pageCount
*
* Implementation Status: 0%
*
*/
void MdiChild::pageCount()
{
insertText("[PAGECOUNT]");
}
/*!
* \brief MdiChild::del
*
* Implementation Status: 100%
*
*/
void MdiChild::del()
{
QTextCursor cur = editor->textCursor();
cur.beginEditBlock();
cur.removeSelectedText();
cur.endEditBlock();
}
/*!
* \brief MdiChild::undo
*
* Implementation Status: 100%
*
*/
void MdiChild::undo()
{
editor->undo();
}
/*!
* \brief MdiChild::redo
*
* Implementation Status: 100%
*
*/
void MdiChild::redo()
{
editor->redo();
}
/*!
* \brief MdiChild::find
*
* Implementation Status: 0%
*
*/
void MdiChild::find()
{
}
/*!
* \brief MdiChild::findNext
*
* Implementation Status: 0%
*
*/
void MdiChild::findNext()
{
}
/*!
* \brief MdiChild::findPrevious
*
* Implementation Status: 0%
*
*/
void MdiChild::findPrevious()
{
}
/*!
* \brief MdiChild::goToLine
*
* Implementation Status: 0%
*
*/
void MdiChild::goToLine()
{
}