Newer
Older
Import / applications / MakePDF / MdiChild.cpp
#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()
{
}