diff --git a/QtSLiM/QtSLiMExtras.cpp b/QtSLiM/QtSLiMExtras.cpp index 426ad0c6..8db81dd7 100644 --- a/QtSLiM/QtSLiMExtras.cpp +++ b/QtSLiM/QtSLiMExtras.cpp @@ -1207,6 +1207,54 @@ void QtSLiMFlashHighlightInTextEdit(QPlainTextEdit *te) QTimer::singleShot(delayMillisec * 3, te, [te]() { te->setPalette(qApp->palette(te)); }); } +// A QLabel that shows shortened text with an ellipsis; see https://stackoverflow.com/a/73316405/2752221 +QtSLiMEllipsisLabel::QtSLiMEllipsisLabel(QWidget *parent) + : QtSLiMEllipsisLabel("", parent) +{ +} + +QtSLiMEllipsisLabel::QtSLiMEllipsisLabel(QString text, QWidget *parent) + : QLabel(parent) +{ + setText(text); +} + +void QtSLiMEllipsisLabel::setText(QString text) +{ + m_text = text; + updateText(); +} + +QSize QtSLiMEllipsisLabel::minimumSizeHint() const +{ + return QSize(0, QLabel::minimumSizeHint().height()); +} + +void QtSLiMEllipsisLabel::resizeEvent(QResizeEvent *p_event) +{ + QLabel::resizeEvent(p_event); + updateText(); +} + +void QtSLiMEllipsisLabel::updateText() +{ + QFontMetrics metrics(font()); + QString elided = metrics.elidedText(m_text, Qt::ElideRight, width()); + QLabel::setText(elided); +} + +void QtSLiMEllipsisLabel::mousePressEvent(QMouseEvent *p_event) +{ + // check the mouse position and only take the click if it is within the displayed label's extent + QPoint curPoint = p_event->pos(); + int clickX = curPoint.x(); + + QFontMetrics metrics(font()); + int labelLength = metrics.size(0, text()).width(); + + if ((clickX >= 0) && (clickX <= labelLength + 1)) + emit pressed(); // triggers QtSLiMWindow::jumpToPopupButtonPressed() +} diff --git a/QtSLiM/QtSLiMExtras.h b/QtSLiM/QtSLiMExtras.h index caad20c8..6ce5992c 100644 --- a/QtSLiM/QtSLiMExtras.h +++ b/QtSLiM/QtSLiMExtras.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -238,6 +239,29 @@ QPixmap QtSLiMDarkenPixmap(QPixmap p_pixmap); void QtSLiMFlashHighlightInTextEdit(QPlainTextEdit *te); +// A QLabel subclass that shows shortened text with an ellipsis; see https://stackoverflow.com/a/73316405/2752221 +class QtSLiMEllipsisLabel : public QLabel +{ + Q_OBJECT + +public: + explicit QtSLiMEllipsisLabel(QWidget *parent = nullptr); + explicit QtSLiMEllipsisLabel(QString text, QWidget *parent = nullptr); + void setText(QString); + virtual QSize minimumSizeHint() const; + +signals: + void pressed(void); + +protected: + void resizeEvent(QResizeEvent *p_event); + void mousePressEvent(QMouseEvent *p_event); + +private: + void updateText(); + QString m_text; +}; + // Incremental sorting // diff --git a/QtSLiM/QtSLiMScriptTextEdit.cpp b/QtSLiM/QtSLiMScriptTextEdit.cpp index 6cfcac11..6493d56a 100644 --- a/QtSLiM/QtSLiMScriptTextEdit.cpp +++ b/QtSLiM/QtSLiMScriptTextEdit.cpp @@ -1044,6 +1044,12 @@ void QtSLiMTextEdit::updateStatusFieldFromSelection(void) { statusBar->clearMessage(); } + + // show the script block's declaration to the right of the Jump button + QtSLiMWindow *windowSLiMController = dynamic_cast(window()); + + if (windowSLiMController) + windowSLiMController->setScriptBlockLabelTextFromSelection(); } } diff --git a/QtSLiM/QtSLiMWindow.cpp b/QtSLiM/QtSLiMWindow.cpp index 91c34240..18e48a52 100644 --- a/QtSLiM/QtSLiMWindow.cpp +++ b/QtSLiM/QtSLiMWindow.cpp @@ -669,6 +669,12 @@ void QtSLiMWindow::initializeUI(void) ui->outputTextEdit->setScriptType(QtSLiMTextEdit::NoScriptType); ui->outputTextEdit->setSyntaxHighlightType(QtSLiMTextEdit::OutputHighlighting); + // set up the script block label, to the right of the Jump menu + QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); + + connect(&prefsNotifier, &QtSLiMPreferencesNotifier::displayFontPrefChanged, this, &QtSLiMWindow::displayFontPrefChanged); + displayFontPrefChanged(); + // set button states ui->toggleDrawerButton->setChecked(false); @@ -737,6 +743,17 @@ void QtSLiMWindow::initializeUI(void) connect(ui->menuWindow, &QMenu::aboutToShow, this, &QtSLiMWindow::updateWindowMenu); } +void QtSLiMWindow::displayFontPrefChanged(void) +{ + // Xcode doesn't use its monospace for this, and it does look a bit out of place in the UI + // So let's try it allowing the font to remain the default system font...? +// QtSLiMPreferencesNotifier &prefs = QtSLiMPreferencesNotifier::instance(); +// QFont displayFont = prefs.displayFontPref(nullptr); + +// displayFont.setPointSize(13); +// ui->scriptBlockLabel->setFont(displayFont); +} + void QtSLiMWindow::applicationPaletteChanged(void) { bool inDarkMode = QtSLiMInDarkMode(); @@ -5555,6 +5572,144 @@ void QtSLiMWindow::jumpToPopupButtonRunMenu(void) jumpToPopupButtonReleased(); } +void QtSLiMWindow::setScriptBlockLabelTextFromSelection(void) +{ + // this does a subset of the parsing logic of QtSLiMWindow::jumpToPopupButtonRunMenu() + // it is used to get the label text for the script block label, to the right of the Jump button + QPlainTextEdit *scriptTE = ui->scriptTextEdit; + QString currentScriptString = scriptTE->toPlainText(); + QByteArray utf8bytes = currentScriptString.toUtf8(); + const char *cstr = utf8bytes.constData(); + + QTextCursor selection_cursor(scriptTE->textCursor()); + int selStart = selection_cursor.selectionStart(); + int selEnd = selection_cursor.selectionEnd(); + + if (cstr) + { + // Figure out whether we have multispecies avatars, and thus want to use the "low brightness symbol" emoji for "ticks all" blocks. + // This emoji provides nicely lined up spacing in the menu, and indicates "ticks all" clearly; seems better than nothing. It would + // be even better, perhaps, to have a spacer of emoji width, to make things line up without having a symbol displayed; unfortunately + // such a spacer does not seem to exist. https://stackoverflow.com/questions/66496671/is-there-a-blank-unicode-character-matching-emoji-width + QString ticksAllAvatar; + + if (community && community->is_explicit_species_ && (community->all_species_.size() > 0)) + { + bool hasAvatars = false; + + for (Species *species : community->all_species_) + if (species->avatar_.length() > 0) + hasAvatars = true; + + if (hasAvatars) + ticksAllAvatar = QString::fromUtf8("\xF0\x9F\x94\x85"); // "low brightness symbol", https://www.compart.com/en/unicode/U+1F505 + } + + SLiMEidosScript script(cstr); + + try { + script.Tokenize(true, false); // make bad tokens as needed, do not keep nonsignificant tokens + script.ParseSLiMFileToAST(true); // make bad nodes as needed (i.e. never raise, and produce a correct tree) + + // Extract SLiMEidosBlocks from the parse tree + const EidosASTNode *root_node = script.AST(); + QString specifierAvatar; + + for (EidosASTNode *script_block_node : root_node->children_) + { + // handle species/ticks specifiers, which are identifier token nodes at the top level of the AST with one child + if ((script_block_node->token_->token_type_ == EidosTokenType::kTokenIdentifier) && (script_block_node->children_.size() == 1)) + { + EidosASTNode *specifierChild = script_block_node->children_[0]; + std::string specifierSpeciesName = specifierChild->token_->token_string_; + Species *specifierSpecies = (community ? community->SpeciesWithName(specifierSpeciesName) : nullptr); + + if (specifierSpecies && specifierSpecies->avatar_.length()) + specifierAvatar = QString::fromStdString(specifierSpecies->avatar_); + else if (!specifierSpecies && (specifierSpeciesName == "all")) + specifierAvatar = ticksAllAvatar; + + continue; + } + + // Create the block and use it to find the string from the start of its declaration to the start of its code + SLiMEidosBlock *new_script_block = new SLiMEidosBlock(script_block_node); + int32_t decl_start = new_script_block->root_node_->token_->token_UTF16_start_; + int32_t code_end = new_script_block->compound_statement_node_->token_->token_UTF16_end_; + + if ((selStart >= decl_start) && (selStart <= code_end) && (selEnd <= code_end + 2)) // +2 allows a selection through the end brace and one more character (typically a newline) + { + int32_t code_start = new_script_block->compound_statement_node_->token_->token_UTF16_start_; + QString decl = currentScriptString.mid(decl_start, code_start - decl_start); + + // Remove everything including and after the first newline + if (decl.indexOf(QChar::LineFeed) != -1) + decl.truncate(decl.indexOf(QChar::LineFeed)); + if (decl.indexOf(0x0C) != -1) // form feed; apparently QChar::FormFeed did not exist in older Qt versions + decl.truncate(decl.indexOf(0x0C)); + if (decl.indexOf(QChar::CarriageReturn) != -1) + decl.truncate(decl.indexOf(QChar::CarriageReturn)); + if (decl.indexOf(QChar::ParagraphSeparator) != -1) + decl.truncate(decl.indexOf(QChar::ParagraphSeparator)); + if (decl.indexOf(QChar::LineSeparator) != -1) + decl.truncate(decl.indexOf(QChar::LineSeparator)); + + // Extract a comment at the end and put it after a em-dash in the string + int simpleCommentStart = decl.indexOf("//"); + int blockCommentStart = decl.indexOf("/*"); + QString comment; + + if ((simpleCommentStart != -1) && ((blockCommentStart == -1) || (simpleCommentStart < blockCommentStart))) + { + // extract a simple comment + comment = decl.right(decl.length() - simpleCommentStart - 2); + decl.truncate(simpleCommentStart); + } + else if ((blockCommentStart != -1) && ((simpleCommentStart == -1) || (blockCommentStart < simpleCommentStart))) + { + // extract a block comment + comment = decl.right(decl.length() - blockCommentStart - 2); + decl.truncate(blockCommentStart); + + int blockCommentEnd = comment.indexOf("*/"); + + if (blockCommentEnd != -1) + comment.truncate(blockCommentEnd); + } + + // Calculate the end of the declaration string; trim off whitespace at the end + decl = decl.trimmed(); + + // Remove trailing whitespace, replace tabs with spaces, etc. + decl = decl.simplified(); + comment = comment.trimmed(); + + if (comment.length() > 0) + decl = decl + " — " + comment; + + // If a species/ticks specifier was previously seen that provides us with an avatar, prepend that + if (specifierAvatar.length()) + { + decl = specifierAvatar + " " + decl; + specifierAvatar.clear(); + } + + delete new_script_block; + + ui->scriptBlockLabel->setText(decl); + } + + delete new_script_block; + } + } + catch (...) + { + } + } + + ui->scriptBlockLabel->setText(QString("")); +} + void QtSLiMWindow::clearOutputClicked(void) { isTransient = false; // Since the user has taken an interest in the window, clear the document's transient status diff --git a/QtSLiM/QtSLiMWindow.h b/QtSLiM/QtSLiMWindow.h index 3126b531..7e3ab80c 100644 --- a/QtSLiM/QtSLiMWindow.h +++ b/QtSLiM/QtSLiMWindow.h @@ -228,6 +228,7 @@ class QtSLiMWindow : public QMainWindow bool changedSinceRecycle(void); void resetSLiMChangeCount(void); void scriptTexteditChanged(void); + void setScriptBlockLabelTextFromSelection(void); bool checkScriptSuppressSuccessResponse(bool suppressSuccessResponse); @@ -284,6 +285,7 @@ public slots: // private slots: + void displayFontPrefChanged(void); void applicationPaletteChanged(void); bool save(void); diff --git a/QtSLiM/QtSLiMWindow.ui b/QtSLiM/QtSLiMWindow.ui index 044608a9..b2f0b298 100644 --- a/QtSLiM/QtSLiMWindow.ui +++ b/QtSLiM/QtSLiMWindow.ui @@ -1009,7 +1009,7 @@ QSlider::handle:horizontal:disabled { - Input Commands: + Input Script: @@ -1050,17 +1050,17 @@ QSlider::handle:horizontal:disabled { - - - Qt::Horizontal + + + + 0 + 0 + - - - 10 - 10 - + + 1 early() - + @@ -1300,7 +1300,7 @@ QSlider::handle:horizontal:disabled { 0 0 914 - 22 + 24 @@ -2080,6 +2080,11 @@ QSlider::handle:horizontal:disabled {
QTabBar.h
1 + + QtSLiMEllipsisLabel + QLabel +
QtSLiMExtras.h
+
diff --git a/QtSLiM/QtSLiMWindow_glue.cpp b/QtSLiM/QtSLiMWindow_glue.cpp index eb6ada95..35557a56 100644 --- a/QtSLiM/QtSLiMWindow_glue.cpp +++ b/QtSLiM/QtSLiMWindow_glue.cpp @@ -110,6 +110,8 @@ void QtSLiMWindow::glueUI(void) connect(ui->browserButton, &QPushButton::released, this, &QtSLiMWindow::showBrowserReleased); connect(ui->jumpToPopupButton, &QPushButton::pressed, this, &QtSLiMWindow::jumpToPopupButtonPressed); connect(ui->jumpToPopupButton, &QPushButton::released, this, &QtSLiMWindow::jumpToPopupButtonReleased); + connect(ui->scriptBlockLabel, &QtSLiMEllipsisLabel::pressed, this, &QtSLiMWindow::jumpToPopupButtonPressed); + //connect(ui->scriptBlockLabel, &QtSLiMEllipsisLabel::released, this, &QtSLiMWindow::jumpToPopupButtonReleased); // seems to be unnecessary connect(ui->clearOutputButton, &QPushButton::pressed, this, &QtSLiMWindow::clearOutputPressed); connect(ui->clearOutputButton, &QPushButton::released, this, &QtSLiMWindow::clearOutputReleased); connect(ui->dumpPopulationButton, &QPushButton::pressed, this, &QtSLiMWindow::dumpPopulationPressed); diff --git a/VERSIONS b/VERSIONS index 97d102d7..34e976d0 100644 --- a/VERSIONS +++ b/VERSIONS @@ -11,6 +11,7 @@ development head (in the master branch): add subpopulationsWithNames() method to Community, to facilitate being able to use user-defined names for subpops fix bugs in some graphs in SLiMgui that could cause incorrect display or a crash in various circumstances send debug output to the main window as well as the debug output tab, to avoid important messages getting lost + add a label next to the Jump button that shows the declaration of the script block containing the selection; clicking it runs the Jump menu version 4.2.2 (Eidos version 3.2.2):