(displayWindow_->layout());
QtSLiMClearLayout(topLayout, /* deleteWidgets */ true);
if (chromosomeCount > 0)
{
// Add a chromosome view for each chromosome in the model, with a spacer next to it to give it the right length
std::vector labels;
bool firstRow = true;
for (Chromosome *chromosome : chromosomes)
{
QHBoxLayout *rowLayout = new QHBoxLayout;
rowLayout->setContentsMargins(margin, firstRow ? margin : spacing, margin, 0);
rowLayout->setSpacing(0);
topLayout->addLayout(rowLayout);
QtSLiMChromosomeWidget *chromosomeWidget = new QtSLiMChromosomeWidget(nullptr);
chromosomeWidget->setController(this);
chromosomeWidget->setFocalChromosome(chromosome);
chromosomeWidget->setDisplayedRange(QtSLiMRange(0, 0)); // display entirety
// multi-chromosome displays do not show ticks, because it would be too crowded; single-chromosome displays do
if (!singleChromosomeDisplay)
chromosomeWidget->setShowsTicks(false);
slim_position_t length = chromosome->last_position_ + 1;
double fractionOfMax = length / (double)chromosomeMaxLength;
int chromosomeStretch = (int)(round(fractionOfMax * 255)); // Qt requires a max value of 255
QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Expanding);
sizePolicy1.setHorizontalStretch(useScaledWidths_ ? chromosomeStretch : 0);
sizePolicy1.setVerticalStretch(0);
chromosomeWidget->setSizePolicy(sizePolicy1);
QLabel *chromosomeLabel = new QLabel();
chromosomeLabel->setText(QString::fromStdString(chromosome->symbol_));
chromosomeLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QSizePolicy sizePolicy2(QSizePolicy::Fixed, QSizePolicy::Expanding);
chromosomeLabel->setSizePolicy(sizePolicy2);
rowLayout->addWidget(chromosomeLabel);
rowLayout->addSpacing(margin);
rowLayout->addWidget(chromosomeWidget);
if (useScaledWidths_)
rowLayout->addStretch(255 - chromosomeStretch); // the remaining width after chromosomeStretch
labels.push_back(chromosomeLabel);
firstRow = false;
}
// adjust all the labels to have the same width
int maxWidth = 0;
for (QLabel *label : labels)
maxWidth = std::max(maxWidth, label->sizeHint().width());
for (QLabel *label : labels)
label->setMinimumWidth(maxWidth);
}
else
{
// no chromosomes; make a single row with a message label and nothing else
QHBoxLayout *rowLayout = new QHBoxLayout;
rowLayout->setContentsMargins(margin, margin, margin, 0);
rowLayout->setSpacing(0);
topLayout->addLayout(rowLayout);
QLabel *chromosomeLabel = new QLabel();
if (singleChromosomeDisplay)
chromosomeLabel->setText(QString("no chromosome with symbol '%1' found").arg(QString::fromStdString(chromosomeSymbol_)));
else
chromosomeLabel->setText("no chromosomes found for display");
chromosomeLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
chromosomeLabel->setEnabled(false);
QFont labelFont(chromosomeLabel->font());
labelFont.setBold(true);
chromosomeLabel->setFont(labelFont);
QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Expanding);
chromosomeLabel->setSizePolicy(sizePolicy2);
rowLayout->addWidget(chromosomeLabel);
}
// Add a horizontal layout at the bottom, for the action button
QHBoxLayout *buttonLayout = nullptr;
{
buttonLayout = new QHBoxLayout;
buttonLayout->setContentsMargins(margin, margin, margin, margin);
buttonLayout->setSpacing(5);
topLayout->addLayout(buttonLayout);
// set up the species badge; note that unlike QtSLiMGraphView, we set it up here immediately,
// since we are guaranteed to already have a valid species object, and then we don't update it
focalSpeciesAvatar_ = focalSpecies->avatar_;
if (focalSpeciesAvatar_.length() && (focalSpecies->community_.all_species_.size() > 1))
{
QLabel *speciesLabel = new QLabel();
speciesLabel->setText(QString::fromStdString(focalSpeciesAvatar_));
buttonLayout->addWidget(speciesLabel);
}
QSpacerItem *rightSpacer = new QSpacerItem(16, 5, QSizePolicy::Expanding, QSizePolicy::Minimum);
buttonLayout->addItem(rightSpacer);
// this code is based on the creation of executeScriptButton in ui_QtSLiMEidosConsole.h
QtSLiMPushButton *actionButton = new QtSLiMPushButton(displayWindow_);
actionButton->setObjectName(QString::fromUtf8("actionButton"));
actionButton->setMinimumSize(QSize(20, 20));
actionButton->setMaximumSize(QSize(20, 20));
actionButton->setFocusPolicy(Qt::NoFocus);
QIcon icon4;
icon4.addFile(QtSLiMImagePath("action", false), QSize(), QIcon::Normal, QIcon::Off);
icon4.addFile(QtSLiMImagePath("action", true), QSize(), QIcon::Normal, QIcon::On);
actionButton->setIcon(icon4);
actionButton->setIconSize(QSize(20, 20));
actionButton->qtslimSetBaseName("action");
actionButton->setCheckable(true);
actionButton->setFlat(true);
#if QT_CONFIG(tooltip)
actionButton->setToolTip("configure chromosome display
");
#endif // QT_CONFIG(tooltip)
buttonLayout->addWidget(actionButton);
connect(actionButton, &QPushButton::pressed, this, [actionButton, this]() { actionButton->qtslimSetHighlight(true); actionButtonRunMenu(actionButton); });
connect(actionButton, &QPushButton::released, this, [actionButton]() { actionButton->qtslimSetHighlight(false); });
// note that this action button has no enable/disable code anywhere, since it is happy to respond at all times
}
}
void QtSLiMChromosomeWidgetController::runChromosomeContextMenuAtPoint(QPoint p_globalPoint)
{
if (!slimWindow_)
return;
Community *community = slimWindow_->community;
if (!invalidSimulation() && community && community->simulation_valid_)
{
QMenu contextMenu("chromosome_menu", slimWindow_); // slimWindow_ is the parent in the sense that the menu is freed if slimWindow_ is freed
QAction *scaledWidths = nullptr;
QAction *unscaledWidths = nullptr;
Species *focalSpecies = focalDisplaySpecies();
if (displayWindow_ && focalSpecies && (focalSpecies->Chromosomes().size() > 1))
{
// only in multichromosome models, offer to scale the widths of the displayed chromosomes
// according to their length or not, as the user prefers
scaledWidths = contextMenu.addAction("Use Scaled Widths");
scaledWidths->setCheckable(true);
scaledWidths->setChecked(useScaledWidths_);
unscaledWidths = contextMenu.addAction("Use Full Widths");
unscaledWidths->setCheckable(true);
unscaledWidths->setChecked(!useScaledWidths_);
contextMenu.addSeparator();
}
QAction *displayMutations = contextMenu.addAction("Display Mutations");
displayMutations->setCheckable(true);
displayMutations->setChecked(shouldDrawMutations_);
QAction *displaySubstitutions = contextMenu.addAction("Display Substitutions");
displaySubstitutions->setCheckable(true);
displaySubstitutions->setChecked(shouldDrawFixedSubstitutions_);
QAction *displayGenomicElements = contextMenu.addAction("Display Genomic Elements");
displayGenomicElements->setCheckable(true);
displayGenomicElements->setChecked(shouldDrawGenomicElements_);
QAction *displayRateMaps = contextMenu.addAction("Display Rate Maps");
displayRateMaps->setCheckable(true);
displayRateMaps->setChecked(shouldDrawRateMaps_);
contextMenu.addSeparator();
QAction *displayFrequencies = contextMenu.addAction("Display Frequencies");
displayFrequencies->setCheckable(true);
displayFrequencies->setChecked(!displayHaplotypes_);
QAction *displayHaplotypes = contextMenu.addAction("Display Haplotypes");
displayHaplotypes->setCheckable(true);
displayHaplotypes->setChecked(displayHaplotypes_);
QActionGroup *displayGroup = new QActionGroup(this); // On Linux this provides a radio-button-group appearance
displayGroup->addAction(displayFrequencies);
displayGroup->addAction(displayHaplotypes);
QAction *displayAllMutations = nullptr;
QAction *selectNonneutralMutations = nullptr;
// mutation type checkmark items
{
const std::map &muttypes = community->AllMutationTypes();
if (muttypes.size() > 0)
{
contextMenu.addSeparator();
displayAllMutations = contextMenu.addAction("Display All Mutations");
displayAllMutations->setCheckable(true);
displayAllMutations->setChecked(displayMuttypes_.size() == 0);
// Make a sorted list of all mutation types we know – those that exist, and those that used to exist that we are displaying
std::vector all_muttypes;
for (auto muttype_iter : muttypes)
{
MutationType *muttype = muttype_iter.second;
slim_objectid_t muttype_id = muttype->mutation_type_id_;
all_muttypes.emplace_back(muttype_id);
}
all_muttypes.insert(all_muttypes.end(), displayMuttypes_.begin(), displayMuttypes_.end());
// Avoid building a huge menu, which will hang the app
if (all_muttypes.size() <= 500)
{
std::sort(all_muttypes.begin(), all_muttypes.end());
all_muttypes.resize(static_cast(std::distance(all_muttypes.begin(), std::unique(all_muttypes.begin(), all_muttypes.end()))));
// Then add menu items for each of those muttypes
for (slim_objectid_t muttype_id : all_muttypes)
{
QString menuItemTitle = QString("Display m%1").arg(muttype_id);
MutationType *muttype = community->MutationTypeWithID(muttype_id); // try to look up the mutation type; can fail if it doesn't exists now
if (muttype && (community->all_species_.size() > 1))
menuItemTitle.append(" ").append(QString::fromStdString(muttype->species_.avatar_));
QAction *mutationAction = contextMenu.addAction(menuItemTitle);
mutationAction->setData(muttype_id);
mutationAction->setCheckable(true);
if (std::find(displayMuttypes_.begin(), displayMuttypes_.end(), muttype_id) != displayMuttypes_.end())
mutationAction->setChecked(true);
}
}
contextMenu.addSeparator();
selectNonneutralMutations = contextMenu.addAction("Select Non-Neutral MutationTypes");
}
}
// Run the context menu synchronously
QAction *action = contextMenu.exec(p_globalPoint);
// Act upon the chosen action; we just do it right here instead of dealing with slots
if (action)
{
if (action == scaledWidths)
{
if (!useScaledWidths_)
{
useScaledWidths_ = true;
buildChromosomeDisplay(/* resetWindowSize */ false);
}
}
else if (action == unscaledWidths)
{
if (useScaledWidths_)
{
useScaledWidths_ = false;
buildChromosomeDisplay(/* resetWindowSize */ false);
}
}
else if (action == displayMutations)
shouldDrawMutations_ = !shouldDrawMutations_;
else if (action == displaySubstitutions)
shouldDrawFixedSubstitutions_ = !shouldDrawFixedSubstitutions_;
else if (action == displayGenomicElements)
shouldDrawGenomicElements_ = !shouldDrawGenomicElements_;
else if (action == displayRateMaps)
shouldDrawRateMaps_ = !shouldDrawRateMaps_;
else if (action == displayFrequencies)
displayHaplotypes_ = false;
else if (action == displayHaplotypes)
displayHaplotypes_ = true;
else
{
const std::map &muttypes = community->AllMutationTypes();
if (action == displayAllMutations)
displayMuttypes_.clear();
else if (action == selectNonneutralMutations)
{
// - (IBAction)filterNonNeutral:(id)sender
displayMuttypes_.clear();
for (auto muttype_iter : muttypes)
{
MutationType *muttype = muttype_iter.second;
slim_objectid_t muttype_id = muttype->mutation_type_id_;
if ((muttype->dfe_type_ != DFEType::kFixed) || (muttype->dfe_parameters_[0] != 0.0))
displayMuttypes_.emplace_back(muttype_id);
}
}
else
{
// - (IBAction)filterMutations:(id)sender
slim_objectid_t muttype_id = action->data().toInt();
auto present_iter = std::find(displayMuttypes_.begin(), displayMuttypes_.end(), muttype_id);
if (present_iter == displayMuttypes_.end())
{
// this mut-type is not being displayed, so add it to our display list
displayMuttypes_.emplace_back(muttype_id);
}
else
{
// this mut-type is being displayed, so remove it from our display list
displayMuttypes_.erase(present_iter);
}
}
}
emit needsRedisplay();
}
}
}
void QtSLiMChromosomeWidgetController::actionButtonRunMenu(QtSLiMPushButton *p_actionButton)
{
QPoint mousePos = QCursor::pos();
runChromosomeContextMenuAtPoint(mousePos);
// This is not called by Qt, for some reason (nested tracking loops?), so we call it explicitly
p_actionButton->qtslimSetHighlight(false);
}
QtSLiMChromosomeWidget::QtSLiMChromosomeWidget(QWidget *p_parent, QtSLiMChromosomeWidgetController *controller, Species *displaySpecies, Qt::WindowFlags f)
#ifndef SLIM_NO_OPENGL
: QOpenGLWidget(p_parent, f)
#else
: QWidget(p_parent, f)
#endif
{
controller_ = controller;
setFocalDisplaySpecies(displaySpecies);
// We support both OpenGL and non-OpenGL display, because some platforms seem
// to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462)
QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance();
connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); });
}
QtSLiMChromosomeWidget::~QtSLiMChromosomeWidget()
{
setDependentChromosomeView(nullptr);
controller_ = nullptr;
}
void QtSLiMChromosomeWidget::setController(QtSLiMChromosomeWidgetController *controller)
{
if (controller != controller_)
{
if (controller_)
disconnect(controller_, &QtSLiMChromosomeWidgetController::needsRedisplay, this, nullptr);
controller_ = controller;
connect(controller, &QtSLiMChromosomeWidgetController::needsRedisplay, this, &QtSLiMChromosomeWidget::updateAfterTick);
}
}
void QtSLiMChromosomeWidget::setFocalDisplaySpecies(Species *displaySpecies)
{
// We can have no focal species (when coming out of the nib, in particular); in that case we display empty state
if (displaySpecies && (displaySpecies->name_ != focalSpeciesName_))
{
// we've switched species, so we should remember the new one
focalSpeciesName_ = displaySpecies->name_;
// ... and reset to showing an overview of all the chromosomes
focalChromosomeSymbol_ = "";
update();
updateDependentView();
}
else
{
// if displaySpecies is nullptr or unchanged, we just stick with our last remembered species
}
}
Species *QtSLiMChromosomeWidget::focalDisplaySpecies(void)
{
// We look up our focal species object by name every time, since keeping a pointer to it would be unsafe
// Before initialize() is done species have not been created, so we return nullptr in that case
if (focalSpeciesName_.length() == 0)
return nullptr;
if (controller_ && controller_->community() && (controller_->community()->Tick() >= 1))
return controller_->community()->SpeciesWithName(focalSpeciesName_);
return nullptr;
}
void QtSLiMChromosomeWidget::setFocalChromosome(Chromosome *chromosome)
{
if (chromosome)
{
if (chromosome->Symbol() != focalChromosomeSymbol_)
{
// we've switched chromosomes, so remember the new one
focalChromosomeSymbol_ = chromosome->Symbol();
// ... and reset to the default selection
setSelectedRange(QtSLiMRange(0, 0));
// ... and if our new chromosome belongs to a different species, remember that
if (chromosome->species_.name_ != focalSpeciesName_)
focalSpeciesName_ = chromosome->species_.name_;
update();
updateDependentView();
}
}
else
{
if (focalChromosomeSymbol_.length())
{
// we had a focal chromosome symbol, so reset to the overall view
focalChromosomeSymbol_ = "";
update();
updateDependentView();
}
}
}
Chromosome *QtSLiMChromosomeWidget::focalChromosome(void)
{
Species *focalSpecies = focalDisplaySpecies();
if (focalSpecies)
{
if (focalChromosomeSymbol_.length())
{
Chromosome *chromosome = focalSpecies->ChromosomeFromSymbol(focalChromosomeSymbol_);
if (isOverview_ && !chromosome)
{
// The focal chromosome apparently no longer exists, but we want to keep
// trying to focus on it if it comes back (e.g., after a recycle), so we
// do not reset or forget the focal chromosome symbol here; we only reset
// the symbol to "" in setFocalDisplaySpecies() and setFocalChromosome().
// However, if the focal species has chromosomes (and they don't match),
// then apparently we're waiting for something that won't happen; give up
// and switch.
if (focalSpecies->Chromosomes().size() > 1)
{
focalChromosomeSymbol_ = "";
return nullptr;
}
else if (focalSpecies->Chromosomes().size() == 1)
{
chromosome = focalSpecies->Chromosomes()[0];
focalChromosomeSymbol_ = chromosome->Symbol();
}
}
return chromosome;
}
else if (focalSpecies->Chromosomes().size() == 1)
{
// The species has just one chromosome, so there is no visual difference
// between that chromosome being selected vs. not selected. However, we
// want to return that chromosome to the caller, so if it is not selected,
// we fix that here and return it.
Chromosome *chromosome = focalSpecies->Chromosomes()[0];
focalChromosomeSymbol_ = chromosome->Symbol();
return chromosome;
}
}
return nullptr;
}
void QtSLiMChromosomeWidget::setDependentChromosomeView(QtSLiMChromosomeWidget *p_dependent_widget)
{
if (dependentChromosomeView_ != p_dependent_widget)
{
dependentChromosomeView_ = p_dependent_widget;
isOverview_ = (dependentChromosomeView_ ? true : false);
showsTicks_ = !isOverview_;
updateDependentView();
}
}
void QtSLiMChromosomeWidget::updateDependentView(void)
{
if (dependentChromosomeView_)
{
Chromosome *chromosome = focalChromosome();
dependentChromosomeView_->setFocalChromosome(chromosome);
if (chromosome)
dependentChromosomeView_->setDisplayedRange(getSelectedRange(chromosome));
else
dependentChromosomeView_->setDisplayedRange(QtSLiMRange(0, 0)); // display entirely
dependentChromosomeView_->stateChanged();
}
}
void QtSLiMChromosomeWidget::stateChanged(void)
{
update();
}
void QtSLiMChromosomeWidget::updateAfterTick(void)
{
// overview chromosomes don't need to update all the time, since their display doesn't change
if (!isOverview_)
stateChanged();
}
#ifndef SLIM_NO_OPENGL
void QtSLiMChromosomeWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
}
void QtSLiMChromosomeWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
// Update the projection
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
}
#endif
QRect QtSLiMChromosomeWidget::rectEncompassingBaseToBase(slim_position_t startBase, slim_position_t endBase, QRect interiorRect, QtSLiMRange displayedRange)
{
double startFraction = (startBase - static_cast(displayedRange.location)) / static_cast(displayedRange.length);
double leftEdgeDouble = interiorRect.left() + startFraction * interiorRect.width();
double endFraction = (endBase + 1 - static_cast(displayedRange.location)) / static_cast(displayedRange.length);
double rightEdgeDouble = interiorRect.left() + endFraction * interiorRect.width();
int leftEdge, rightEdge;
if (rightEdgeDouble - leftEdgeDouble > 1.0)
{
// If the range spans a width of more than one pixel, then use the maximal pixel range
leftEdge = static_cast(floor(leftEdgeDouble));
rightEdge = static_cast(ceil(rightEdgeDouble));
}
else
{
// If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the left-right positions span a pixel boundary
leftEdge = static_cast(floor(leftEdgeDouble));
rightEdge = leftEdge + 1;
}
return QRect(leftEdge, interiorRect.top(), rightEdge - leftEdge, interiorRect.height());
}
slim_position_t QtSLiMChromosomeWidget::baseForPosition(double position, QRect interiorRect, QtSLiMRange displayedRange)
{
double fraction = (position - interiorRect.left()) / interiorRect.width();
slim_position_t base = static_cast(floor(fraction * (displayedRange.length + 1) + displayedRange.location));
return base;
}
QRect QtSLiMChromosomeWidget::getContentRect(void)
{
QRect bounds = rect();
// The height gets adjusted because our "content rect" does not include the space for selection knobs below
// (for the overview) or for tick marks and labels (for the zoomed view). Note that SLiMguiLegacy has a two-
// pixel margin on the left and right of the chromosome view, to avoid clipping the selection knobs, but that
// is a bit harder to do in Qt since the UI layout is trickier, so we just let the knobs clip; it's fine.
int bottomMargin = (isOverview_ ? (selectionKnobSize+1) : heightForTicks);
if (!isOverview_ && !showsTicks_)
bottomMargin = 0;
return QRect(bounds.left(), bounds.top(), bounds.width(), bounds.height() - bottomMargin);
}
QtSLiMRange QtSLiMChromosomeWidget::getSelectedRange(Chromosome *chromosome)
{
if (hasSelection_ && chromosome && (chromosome == focalChromosome()))
{
return QtSLiMRange(selectionFirstBase_, selectionLastBase_ - selectionFirstBase_ + 1); // number of bases encompassed; a selection from x to x encompasses 1 base
}
else if (chromosome)
{
slim_position_t chromosomeLastPosition = chromosome->last_position_;
return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed
}
else
{
return QtSLiMRange(0, 0);
}
}
void QtSLiMChromosomeWidget::setSelectedRange(QtSLiMRange p_selectionRange)
{
if (isOverview_ && (p_selectionRange.length >= 1))
{
selectionFirstBase_ = static_cast(p_selectionRange.location);
selectionLastBase_ = static_cast(p_selectionRange.location + p_selectionRange.length) - 1;
hasSelection_ = true;
// Save the selection for restoring across recycles, etc.
savedSelectionFirstBase_ = selectionFirstBase_;
savedSelectionLastBase_ = selectionLastBase_;
savedHasSelection_ = hasSelection_;
}
else if (hasSelection_)
{
hasSelection_ = false;
// Save the selection for restoring across recycles, etc.
savedHasSelection_ = hasSelection_;
}
else
{
// Save the selection for restoring across recycles, etc.
savedHasSelection_ = false;
return;
}
// Our selection changed, so update and post a change notification
update();
if (isOverview_ && dependentChromosomeView_)
updateDependentView();
}
void QtSLiMChromosomeWidget::restoreLastSelection(void)
{
if (isOverview_ && savedHasSelection_)
{
selectionFirstBase_ = savedSelectionFirstBase_;
selectionLastBase_ = savedSelectionLastBase_;
hasSelection_ = savedHasSelection_;
}
else if (hasSelection_)
{
hasSelection_ = false;
}
// Our selection changed, so update and post a change notification
update();
// We want to always post the notification, to make sure updating happens correctly;
// this ensures that correct ticks marks get drawn after a recycle, etc.
if (isOverview_ && dependentChromosomeView_)
updateDependentView();
}
QtSLiMRange QtSLiMChromosomeWidget::getDisplayedRange(Chromosome *chromosome)
{
if (isOverview_)
{
// the overview always displays the whole length
slim_position_t chromosomeLastPosition = chromosome->last_position_;
return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed
}
else if (!chromosome || (displayedRange_.length == 0))
{
// the detail view displays the entire length unless a specific displayed range is set
slim_position_t chromosomeLastPosition = chromosome->last_position_;
return QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed
}
else
{
return displayedRange_;
}
}
void QtSLiMChromosomeWidget::setDisplayedRange(QtSLiMRange p_displayedRange)
{
displayedRange_ = p_displayedRange;
update();
}
void QtSLiMChromosomeWidget::setShowsTicks(bool p_showTicks)
{
if (p_showTicks != showsTicks_)
{
showsTicks_ = p_showTicks;
update();
}
}
#ifndef SLIM_NO_OPENGL
void QtSLiMChromosomeWidget::paintGL()
#else
void QtSLiMChromosomeWidget::paintEvent(QPaintEvent * /* p_paint_event */)
#endif
{
QPainter painter(this);
bool inDarkMode = QtSLiMInDarkMode();
painter.eraseRect(rect()); // erase to background color, which is not guaranteed
//painter.fillRect(rect(), Qt::red);
painter.setPen(Qt::black); // make sure we have our default color of black, since Qt apparently does not guarantee that
Species *displaySpecies = focalDisplaySpecies();
bool ready = (isEnabled() && controller_ && !controller_->invalidSimulation() && (displaySpecies != nullptr));
QRect contentRect = getContentRect();
QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1));
// if the simulation is at tick 0, it is not ready
if (ready)
if (controller_->community()->Tick() == 0)
ready = false;
if (ready)
{
if (isOverview_)
{
drawOverview(displaySpecies, painter);
}
else
{
Chromosome *chromosome = focalChromosome();
if (!chromosome)
{
// display all chromosomes simultaneously
drawFullGenome(displaySpecies, painter);
}
else
{
// display one chromosome in the regular way
QtSLiMRange displayedRange = getDisplayedRange(chromosome);
// draw ticks at bottom of content rect
if (showsTicks_)
drawTicksInContentRect(contentRect, displaySpecies, displayedRange, painter);
// do the core drawing, with or without OpenGL according to user preference
#ifndef SLIM_NO_OPENGL
if (QtSLiMPreferencesNotifier::instance().useOpenGLPref())
{
painter.beginNativePainting();
glDrawRect(contentRect, displaySpecies, chromosome);
painter.endNativePainting();
}
else
#endif
{
qtDrawRect(contentRect, displaySpecies, chromosome, painter);
}
// frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up
QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter);
}
}
}
else
{
// erase the content area itself
painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0));
// frame
QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter);
}
}
void QtSLiMChromosomeWidget::drawOverview(Species *displaySpecies, QPainter &painter)
{
// the overview draws all of the chromosomes showing genomic elements; always with Qt, not GL
Chromosome *focalChrom = focalChromosome();
QRect contentRect = getContentRect();
bool inDarkMode = QtSLiMInDarkMode();
if (!displaySpecies->HasGenetics())
{
QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1));
painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0));
QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter);
return;
}
const std::vector &chromosomes = displaySpecies->Chromosomes();
int chromosomeCount = (int)chromosomes.size();
int64_t availableWidth = contentRect.width() - (chromosomeCount * 2) - ((chromosomeCount - 1) * spaceBetweenChromosomes);
int64_t totalLength = 0;
// after a delay, we show chromosome numbers unless we're tracking or have a single-chromosome model
bool showChromosomeNumbers = (showChromosomeNumbers_ && !isTracking_ && (chromosomeCount > 1));
for (Chromosome *chrom : chromosomes)
{
slim_position_t chromLength = (chrom->last_position_ + 1);
totalLength += chromLength;
}
if (showChromosomeNumbers)
{
painter.save();
static QFont *tickFont = nullptr;
if (!tickFont)
{
tickFont = new QFont();
#ifdef __linux__
tickFont->setPointSize(8);
#else
tickFont->setPointSize(10);
#endif
}
painter.setFont(*tickFont);
}
int64_t remainingLength = totalLength;
int leftPosition = contentRect.left();
for (Chromosome *chrom : chromosomes)
{
double scale = (double)availableWidth / remainingLength;
slim_position_t chromLength = (chrom->last_position_ + 1);
int width = (int)round(chromLength * scale);
int paddedWidth = 2 + width;
QRect chromContentRect(leftPosition, contentRect.top(), paddedWidth, contentRect.height());
QRect chromInteriorRect = chromContentRect.marginsRemoved(QMargins(1, 1, 1, 1));
QtSLiMRange displayedRange = getDisplayedRange(chrom);
if (showChromosomeNumbers)
{
painter.fillRect(chromInteriorRect, Qt::white);
const std::string &symbol = chrom->Symbol();
QString symbolLabel = QString::fromStdString(symbol);
QRect labelBoundingRect = painter.boundingRect(QRect(), Qt::TextDontClip | Qt::TextSingleLine, symbolLabel);
double labelWidth = labelBoundingRect.width();
// display the chromosome symbol only if there is space for it
if (labelWidth < chromInteriorRect.width())
{
int symbolLabelX = static_cast(round(chromContentRect.center().x())) + 1;
int symbolLabelY = static_cast(round(chromContentRect.center().y())) + 7;
int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | Qt::AlignHCenter);
painter.drawText(QRect(symbolLabelX, symbolLabelY, 0, 0), textFlags, symbolLabel);
}
}
else
{
painter.fillRect(chromInteriorRect, Qt::black);
qtDrawGenomicElements(chromInteriorRect, chrom, displayedRange, painter);
}
if (chrom == focalChrom)
{
if (hasSelection_)
{
// overlay the selection last, since it bridges over the frame; when showing chromosome numbers
// in light mode, drawing the interior shadow looks better because of the white background
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter);
overlaySelection(chromInteriorRect, displayedRange, /* drawInteriorShadow */ showChromosomeNumbers && !inDarkMode, painter);
}
else if (chromosomes.size() > 1)
{
// the selected chromosome gets a heavier frame, if we have more than one chromosome
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 0.4, 1.0), painter);
}
else
{
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter);
}
}
else
{
if ((chromosomes.size() > 1) && focalChrom)
{
// with more than one chromosome, chromosomes other than the focal chromosome get washed out
painter.fillRect(chromInteriorRect, QtSLiMColorWithWhite(inDarkMode ? 0.0 : 1.0, inDarkMode ? 0.50 : 0.60));
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.1 : 0.8, 1.0), painter);
}
else
{
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter);
}
}
leftPosition += (paddedWidth + spaceBetweenChromosomes);
availableWidth -= width;
remainingLength -= chromLength;
}
if (showChromosomeNumbers)
{
painter.restore();
}
}
void QtSLiMChromosomeWidget::drawFullGenome(Species *displaySpecies, QPainter &painter)
{
// this is similar to drawOverview(), but shows the detail view and can use GL
// we end up here in no-genetics models, because there is no selected chromosome (no chromosomes at all)
QRect contentRect = getContentRect();
bool inDarkMode = QtSLiMInDarkMode();
const std::vector &chromosomes = displaySpecies->Chromosomes();
int chromosomeCount = (int)chromosomes.size();
int64_t availableWidth = contentRect.width() - (chromosomeCount * 2) - ((chromosomeCount - 1) * spaceBetweenChromosomes);
int64_t totalLength = 0;
if (!displaySpecies->HasGenetics())
{
QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1));
painter.fillRect(interiorRect, QtSLiMColorWithWhite(inDarkMode ? 0.118 : 0.9, 1.0));
QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.77, 1.0), painter);
// draw the "no genetics" label at the bottom; we now do this instead of drawTicksInContentRect()
static QFont *tickFont = nullptr;
if (!tickFont)
{
tickFont = new QFont();
#ifdef __linux__
tickFont->setPointSize(7);
#else
tickFont->setPointSize(9);
#endif
}
painter.setFont(*tickFont);
QString tickLabel("no genetics");
int tickLabelX = static_cast(floor(contentRect.left() + contentRect.width() / 2.0));
int tickLabelY = contentRect.bottom() + (2 + 13);
int textFlags = (Qt::TextDontClip | Qt::TextSingleLine | Qt::AlignBottom | Qt::AlignHCenter);
painter.drawText(QRect(tickLabelX, tickLabelY, 0, 0), textFlags, tickLabel);
return;
}
for (Chromosome *chrom : chromosomes)
{
slim_position_t chromLength = (chrom->last_position_ + 1);
totalLength += chromLength;
}
int64_t remainingLength = totalLength;
int leftPosition = contentRect.left();
for (Chromosome *chrom : chromosomes)
{
// display one chromosome in the regular way
double scale = (double)availableWidth / remainingLength;
slim_position_t chromLength = (chrom->last_position_ + 1);
int width = (int)round(chromLength * scale);
int paddedWidth = 2 + width;
QRect chromContentRect(leftPosition, contentRect.top(), paddedWidth, contentRect.height());
QRect chromInteriorRect = chromContentRect.marginsRemoved(QMargins(1, 1, 1, 1));
slim_position_t chromosomeLastPosition = chrom->last_position_;
QtSLiMRange displayedRange = QtSLiMRange(0, chromosomeLastPosition + 1); // chromosomeLastPosition + 1 bases are encompassed
painter.fillRect(chromInteriorRect, Qt::black);
// draw ticks at bottom of content rect
if (showsTicks_)
drawTicksInContentRect(chromContentRect, displaySpecies, displayedRange, painter);
// do the core drawing, with or without OpenGL according to user preference
#ifndef SLIM_NO_OPENGL
if (QtSLiMPreferencesNotifier::instance().useOpenGLPref())
{
painter.beginNativePainting();
glDrawRect(chromContentRect, displaySpecies, chrom);
painter.endNativePainting();
}
else
#endif
{
qtDrawRect(chromContentRect, displaySpecies, chrom, painter);
}
// frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up
QtSLiMFrameRect(chromContentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter);
leftPosition += (paddedWidth + spaceBetweenChromosomes);
availableWidth -= width;
remainingLength -= chromLength;
}
}
void QtSLiMChromosomeWidget::drawTicksInContentRect(QRect contentRect, __attribute__((__unused__)) Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter)
{
bool inDarkMode = QtSLiMInDarkMode();
QRect interiorRect = contentRect.marginsRemoved(QMargins(1, 1, 1, 1));
int64_t lastTickIndex = numberOfTicksPlusOne;
painter.save();
painter.setPen(inDarkMode ? Qt::white : Qt::black);
// Display fewer ticks when we are displaying a very small number of positions
lastTickIndex = std::min(lastTickIndex, (displayedRange.length + 1) / 3);
double tickIndexDivisor = ((lastTickIndex == 0) ? 1.0 : static_cast(lastTickIndex)); // avoid a divide by zero when we are displaying a single site
static QFont *tickFont = nullptr;
if (!tickFont)
{
tickFont = new QFont();
#ifdef __linux__
tickFont->setPointSize(7);
#else
tickFont->setPointSize(9);
#endif
}
painter.setFont(*tickFont);
QFontMetricsF fontMetrics(*tickFont);
if (displayedRange.length == 0)
{
// The "no genetics" case is now handled in drawFullGenome()
painter.restore();
return;
}
// We use scientific notation in two situations: (1) for numbers larger than 1e8, for readability, with four digits
// after the decimal point, and (2) when the largest tick label and 0 won't fit together, for space-efficiency,
// with two digits after the decimal point
bool useScientificNotation = false; // the rightmost tick determines this since it has the largest tickBase
int scientificNotationDigits = 4;
slim_position_t largestTickBase = static_cast