@@ -16,6 +16,50 @@ namespace schrodinger
1616namespace sketcher
1717{
1818
19+ // True when every selected monomer is part of a nucleic acid chain (no
20+ // peptides, no CHEM) AND at least one is an NA base — the gate for the
21+ // "Add Complementary Sequence" action visibility.
22+ static bool na_selection_with_at_least_one_base (
23+ const std::unordered_set<const RDKit::Atom*>& atoms)
24+ {
25+ // Any non-NA monomer type (peptide, CHEM, or an unknown future variant)
26+ // disqualifies the whole selection.
27+ auto is_na_monomer = [](const RDKit::Atom* a) {
28+ switch (get_monomer_type (a)) {
29+ case MonomerType::NA_BASE :
30+ case MonomerType::NA_SUGAR :
31+ case MonomerType::NA_PHOSPHATE :
32+ return true ;
33+ default :
34+ return false ;
35+ }
36+ };
37+ auto is_na_base = [](const RDKit::Atom* a) {
38+ return get_monomer_type (a) == MonomerType::NA_BASE ;
39+ };
40+ // any_of is false for an empty selection, so no explicit empty guard.
41+ return std::ranges::all_of (atoms, is_na_monomer) &&
42+ std::ranges::any_of (atoms, is_na_base);
43+ }
44+
45+ // Filter the menu's selection down to NA bases whose symbol has a
46+ // Watson-Crick complement. Used to drive both the "enabled" state of
47+ // the action and the payload of the emitted signal.
48+ static std::unordered_set<const RDKit::Atom*>
49+ complementable_bases (const std::unordered_set<const RDKit::Atom*>& atoms)
50+ {
51+ std::unordered_set<const RDKit::Atom*> out;
52+ for (const auto * a : atoms) {
53+ if (get_monomer_type (a) != MonomerType::NA_BASE ) {
54+ continue ;
55+ }
56+ if (na_base_has_complement (get_monomer_res_name (a))) {
57+ out.insert (a);
58+ }
59+ }
60+ return out;
61+ }
62+
1963MonomerContextMenu::MonomerContextMenu (QWidget* parent) :
2064 AbstractContextMenu (parent)
2165{
@@ -25,6 +69,7 @@ MonomerContextMenu::MonomerContextMenu(QWidget* parent) :
2569 createProtonateAction ();
2670 createMutateBaseSubMenu ();
2771 createSugarToggleAction ();
72+ createAddComplementaryStrandAction ();
2873 createDeleteAction ();
2974}
3075
@@ -174,6 +219,19 @@ void MonomerContextMenu::createSugarToggleAction()
174219 });
175220}
176221
222+ void MonomerContextMenu::createAddComplementaryStrandAction ()
223+ {
224+ m_add_complement_action =
225+ addAction (" Add Complementary Sequence" , this , [this ]() {
226+ // Reuse the set computed in updateActions() rather than walking
227+ // the selection a third time.
228+ if (m_complement_bases.empty ()) {
229+ return ;
230+ }
231+ emit addComplementaryStrandRequested (m_complement_bases);
232+ });
233+ }
234+
177235void MonomerContextMenu::createDeleteAction ()
178236{
179237 addAction (" Delete" , this , [this ]() { emit deleteRequested (m_atoms); });
@@ -194,6 +252,14 @@ void MonomerContextMenu::updateActions()
194252 m_mutate_base_menu->menuAction ()->setVisible (all_na_base);
195253 m_sugar_toggle_action->setVisible (all_na_sugar);
196254
255+ m_complement_bases = complementable_bases (m_atoms);
256+ const bool na_with_base = na_selection_with_at_least_one_base (m_atoms);
257+ m_add_complement_action->setVisible (na_with_base);
258+ // Enabled iff at least one selected base has a Watson-Crick complement
259+ // symbol; DB-level validation happens model-side.
260+ m_add_complement_action->setEnabled (na_with_base &&
261+ !m_complement_bases.empty ());
262+
197263 if (all_peptide) {
198264 bool any_d_form_toggleable = false ;
199265 for (const auto * a : m_atoms) {
0 commit comments