wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ElementPalette.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QDir>
7#include <QIcon>
8#include <QLayout>
9#include <QLineEdit>
10#include <QMimeData>
11#include <QRegularExpression>
12#include <QScrollArea>
13
18#include "App/Scene/Scene.h"
19#include "App/UI/MainWindowUI.h"
20
22 : QObject(parent)
23 , m_ui(ui)
24{
25 m_ui->lineEditSearch->setAccessibleName(tr("Search elements"));
26 m_ui->lineEditSearch->setWhatsThis(tr("Type to filter the palette by element name. Press "
27 "Enter to add the first match to the circuit."));
28 m_ui->tabElements->setAccessibleName(tr("Element palette"));
29 m_ui->tabElements->setWhatsThis(tr("Elements grouped by category. Drag one onto the canvas, "
30 "or double-click to add it to the active circuit."));
31
32 connect(m_ui->lineEditSearch, &QLineEdit::textChanged, this, &ElementPalette::onSearchTextChanged);
33 connect(m_ui->lineEditSearch, &QLineEdit::returnPressed, this, &ElementPalette::onSearchReturnPressed);
34}
35
36bool ElementPalette::nameMatchesSearch(const QString &name, const QString &query)
37{
38 // Escape the raw query so regex metacharacters ('(', '[', '\\', '+', ...) are matched
39 // literally; an unescaped query can form an invalid pattern whose match() never succeeds,
40 // silently returning zero results even when items match.
41 const QRegularExpression regex(".*" + QRegularExpression::escape(query) + ".*",
42 QRegularExpression::CaseInsensitiveOption);
43 return regex.match(name).hasMatch();
44}
45
47{
48 setupTabIcons();
49
50 const int ioTabIndex = tabIndex("io");
51 if (ioTabIndex != -1) {
52 m_ui->tabElements->setCurrentIndex(ioTabIndex);
53 } else {
54 m_ui->tabElements->setCurrentIndex(0);
55 }
56
57 populateMenu(m_ui->verticalSpacer_InOut, {"InputVcc", "InputGnd", "InputButton", "InputSwitch", "InputRotary", "Clock", "Led", "Display7", "Display14", "Display16", "Buzzer", "AudioBox"}, m_ui->scrollAreaWidgetContents_InOut->layout());
58 populateMenu(m_ui->verticalSpacer_Gates, {"And", "Or", "Not", "Nand", "Nor", "Xor", "Xnor", "Node"}, m_ui->scrollAreaWidgetContents_Gates->layout());
59 populateMenu(m_ui->verticalSpacer_Combinational,{"TruthTable", "Mux", "Demux"}, m_ui->scrollAreaWidgetContents_Combinational->layout());
60 populateMenu(m_ui->verticalSpacer_Memory, {"DLatch", "SRLatch", "DFlipFlop", "JKFlipFlop", "TFlipFlop"}, m_ui->scrollAreaWidgetContents_Memory->layout());
61 populateMenu(m_ui->verticalSpacer_Misc, {"Text", "Line"}, m_ui->scrollAreaWidgetContents_Misc->layout());
62}
63
64void ElementPalette::updateICList(const QFileInfo &currentFile)
65{
66 // Remove the expanding spacer before inserting items so new labels don't push it out of place.
67 m_ui->scrollAreaWidgetContents_IC->layout()->removeItem(m_ui->verticalSpacer_IC);
68
69 // Clear all existing IC labels from both the IC panel and the search panel.
70 const auto items = m_ui->scrollAreaWidgetContents_IC->findChildren<ElementLabel *>();
71 for (auto *item : items) {
72 item->deleteLater();
73 }
74
75 const auto items2 = m_ui->scrollAreaWidgetContents_Search->findChildren<ElementLabel *>();
76 for (auto *item : items2) {
77 if (item->elementType() == ElementType::IC) {
78 item->deleteLater();
79 }
80 }
81
82 if (currentFile.exists()) {
83 QDir directory(currentFile.absoluteDir());
84 // Enumerate all .panda files in the same directory — they are candidate ICs.
85 QStringList files = directory.entryList({"*.panda", "*.PANDA"}, QDir::Files);
86 // Exclude the project file itself and hidden/autosave files.
87 files.removeAll(currentFile.fileName());
88 for (qsizetype i = files.size() - 1; i >= 0; --i) {
89 if (files.at(i).at(0) == '.') {
90 files.removeAt(i); // LCOV_EXCL_LINE — unreachable: QDir::entryList() with a plain QDir::Files filter (no QDir::Hidden) excludes dot-prefixed entries by construction (verified empirically against this Qt build), so this loop can never find one to remove.
91 }
92 }
93
94 for (const QString &file : std::as_const(files)) {
95 const QPixmap pixmap(":/Components/Logic/ic-panda.svg");
96
97 // "label_ic" lets onSearchTextChanged()'s third pass match by full file name
98 // (including the .panda extension), which the second pass's name()-based match
99 // (the upper-cased base name only) can't do on its own.
100 auto *item = new ElementLabel(pixmap, ElementType::IC, file, m_ui->scrollAreaWidgetContents_IC);
101 item->setObjectName("label_ic");
102 connectDoubleClickAdd(item);
103 m_ui->scrollAreaWidgetContents_IC->layout()->addWidget(item);
104
105 auto *item2 = new ElementLabel(pixmap, ElementType::IC, file, m_ui->scrollAreaWidgetContents_Search);
106 item2->setObjectName("label_ic");
107 connectDoubleClickAdd(item2);
108 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(item2);
109 }
110 }
111
112 m_ui->scrollAreaWidgetContents_IC->layout()->addItem(m_ui->verticalSpacer_IC);
113}
114
116{
117 // Remove existing embedded IC labels
118 const auto existingLabels = m_ui->scrollAreaWidgetContents_EmbeddedIC->findChildren<ElementLabel *>("label_embedded_ic");
119 for (auto *item : existingLabels) {
120 item->deleteLater();
121 }
122
123 const auto searchLabels = m_ui->scrollAreaWidgetContents_Search->findChildren<ElementLabel *>("label_embedded_ic");
124 for (auto *item : searchLabels) {
125 item->deleteLater();
126 }
127
128 if (!scene) {
129 return;
130 }
131
132 // Collect all blob names from the registry
133 QStringList seenBlobNames;
134 if (scene->icRegistry()) {
135 seenBlobNames = scene->icRegistry()->blobMap().keys();
136 }
137
138 // Remove spacer, add labels, restore spacer
139 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->removeItem(m_ui->verticalSpacer_EmbeddedIC);
140
141 const QPixmap pixmap(":/Components/Logic/ic-panda-embedded.svg");
142
143 for (const QString &bn : std::as_const(seenBlobNames)) {
144 auto *item = new ElementLabel(pixmap, ElementType::IC, bn, m_ui->scrollAreaWidgetContents_EmbeddedIC, true);
145 item->setObjectName("label_embedded_ic");
146 connectDoubleClickAdd(item);
147 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->addWidget(item);
148
149 auto *item2 = new ElementLabel(pixmap, ElementType::IC, bn, m_ui->scrollAreaWidgetContents_Search, true);
150 item2->setObjectName("label_embedded_ic");
151 connectDoubleClickAdd(item2);
152 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(item2);
153 }
154
155 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->addItem(m_ui->verticalSpacer_EmbeddedIC);
156}
157
159{
160 const auto items = m_ui->tabElements->findChildren<ElementLabel *>();
161 for (auto *item : items) {
162 item->updateName();
163 }
164}
165
167{
168 const int memoryTabIndex = tabIndex("memory");
169 if (memoryTabIndex != -1) {
170 m_ui->tabElements->setTabIcon(memoryTabIndex, QIcon(DFlipFlop::pixmapPath()));
171 }
172
173 const auto labels = m_ui->memory->findChildren<ElementLabel *>();
174 for (auto *label : labels) {
175 label->updateTheme();
176 }
177}
178
179void ElementPalette::onSearchTextChanged(const QString &text)
180{
181 m_ui->scrollAreaWidgetContents_Search->layout()->removeItem(m_ui->verticalSpacer_Search);
182
183 const int searchTabIndex = tabIndex("search");
184
185 if (text.isEmpty()) {
186 // Restore the normal tab bar and return to the tab the user was on before typing.
187 m_ui->tabElements->tabBar()->show();
188 m_ui->tabElements->setCurrentIndex(m_lastTabIndex);
189 if (searchTabIndex != -1) {
190 m_ui->tabElements->setTabEnabled(searchTabIndex, false);
191 }
192 m_lastTabIndex = -1;
193 } else {
194 // On first keystroke, remember which tab was active so we can restore it.
195 if (m_lastTabIndex == -1) {
196 m_lastTabIndex = m_ui->tabElements->currentIndex();
197 }
198
199 // Hide the tab bar so only unified search results are visible.
200 m_ui->tabElements->tabBar()->hide();
201 if (searchTabIndex != -1) {
202 m_ui->tabElements->setCurrentIndex(searchTabIndex);
203 m_ui->tabElements->setTabEnabled(searchTabIndex, true);
204 }
205
206 // Walk the search tab's ElementLabel children once and reuse the result for
207 // all three passes below, instead of calling findChildren() three times.
208 const auto allItems = m_ui->scrollArea_Search->findChildren<ElementLabel *>();
209
210 // First pass: match by object name (e.g. "label_and") — prioritises exact type hits.
211 // The query is regex-escaped (as in nameMatchesSearch) so metacharacters can't form an
212 // invalid pattern; the ^label_ anchor keeps the query matching the type part rather than
213 // the shared "label_" prefix.
214 const QRegularExpression regex1(QString("^label_.*%1.*").arg(QRegularExpression::escape(text)), QRegularExpression::CaseInsensitiveOption);
215 QList<ElementLabel *> searchResults;
216 for (auto *item : allItems) {
217 if (regex1.match(item->objectName()).hasMatch()) {
218 searchResults.append(item);
219 }
220 }
221
222 // Second pass: match by human-readable translated element name.
223 for (auto *item : allItems) {
224 if (nameMatchesSearch(item->name(), text) && !searchResults.contains(item)) {
225 searchResults.append(item);
226 }
227 }
228
229 // Third pass: also search IC file names (all share object name "label_ic").
230 for (auto *item : allItems) {
231 if (item->objectName() == QLatin1String("label_ic") && nameMatchesSearch(item->icFileName(), text)) {
232 searchResults.append(item);
233 }
234 }
235
236 for (auto *item : allItems) {
237 item->setHidden(true);
238 }
239 for (auto *item : std::as_const(searchResults)) {
240 item->setVisible(true);
241 }
242 }
243
244 m_ui->scrollAreaWidgetContents_Search->layout()->addItem(m_ui->verticalSpacer_Search);
245}
246
247void ElementPalette::onSearchReturnPressed()
248{
249 const auto allLabels = m_ui->scrollArea_Search->findChildren<ElementLabel *>();
250
251 // Emit the first visible result so MainWindow can add it to the active scene.
252 for (auto *label : std::as_const(allLabels)) {
253 if (label->isVisible()) {
254 emit addElementRequested(label->mimeData());
255 m_ui->lineEditSearch->clear();
256 return;
257 }
258 }
259}
260
261void ElementPalette::populateMenu(QSpacerItem *spacer, const QStringList &names, QLayout *layout)
262{
263 layout->removeItem(spacer);
264
265 // Each element type gets two labels: one in its category panel and a
266 // duplicate in the search panel so search can find everything in one place.
267 QWidget *const container = layout->parentWidget();
268 for (const auto &name : names) {
269 const auto type = ElementFactory::textToType(name);
270 const auto pixmap = ElementFactory::pixmap(type);
271 // "label_<name>" lets onSearchTextChanged()'s first pass match the internal,
272 // locale-independent type keyword (e.g. "and"), rather than only the translated
273 // display name that the second pass matches.
274 const QString objectName = "label_" + name.toLower();
275
276 auto *categoryLabel = new ElementLabel(pixmap, type, name, container);
277 categoryLabel->setObjectName(objectName);
278 connectDoubleClickAdd(categoryLabel);
279 layout->addWidget(categoryLabel);
280
281 auto *searchLabel = new ElementLabel(pixmap, type, name, m_ui->scrollAreaWidgetContents_Search);
282 searchLabel->setObjectName(objectName);
283 connectDoubleClickAdd(searchLabel);
284 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(searchLabel);
285 }
286
287 layout->addItem(spacer);
288}
289
290void ElementPalette::connectDoubleClickAdd(ElementLabel *label)
291{
292 // Signal-to-signal: a double-clicked label re-emits through addElementRequested,
293 // the same route the search-box Return key already uses to add an element.
295}
296
297void ElementPalette::setupTabIcons()
298{
299 // Lookup by object name so reordering tabs in the UI doesn't break icon assignment.
300 const int ioTabIndex = tabIndex("io");
301 const int gatesTabIndex = tabIndex("gates");
302 const int combinationalTabIndex = tabIndex("combinational");
303 const int memoryTabIndex = tabIndex("memory");
304 const int icTabIndex = tabIndex("ic");
305 const int miscTabIndex = tabIndex("misc");
306 const int searchTabIndex = tabIndex("search");
307
308 if (ioTabIndex != -1) m_ui->tabElements->setTabIcon(ioTabIndex, QIcon(":/Components/Input/buttonOff.svg"));
309 if (gatesTabIndex != -1) m_ui->tabElements->setTabIcon(gatesTabIndex, QIcon(":/Components/Logic/xor.svg"));
310 if (combinationalTabIndex != -1) m_ui->tabElements->setTabIcon(combinationalTabIndex, QIcon(":/Components/Logic/truthtable-rotated.svg"));
311 if (memoryTabIndex != -1) m_ui->tabElements->setTabIcon(memoryTabIndex, QIcon(DFlipFlop::pixmapPath()));
312 if (icTabIndex != -1) m_ui->tabElements->setTabIcon(icTabIndex, QIcon(":/Components/Logic/ic-panda.svg"));
313 if (miscTabIndex != -1) m_ui->tabElements->setTabIcon(miscTabIndex, QIcon(":/Components/Misc/text.png"));
314 // The search tab is virtual; enabled only when the user types in the search box.
315 if (searchTabIndex != -1) m_ui->tabElements->setTabEnabled(searchTabIndex, false);
316}
317
318int ElementPalette::tabIndex(const QString &objectName) const
319{
320 for (int i = 0; i < m_ui->tabElements->count(); ++i) {
321 QWidget *tabWidget = m_ui->tabElements->widget(i);
322 if (tabWidget && tabWidget->objectName() == objectName) {
323 return i;
324 }
325 }
326 return -1;
327}
Graphic element for the D flip-flop.
Singleton factory for all circuit element types.
Draggable element icon+label widget used in the element palette.
ElementPalette: manages the left-panel element palette and search UI.
Abstract base class for all graphical circuit elements.
MainWindowUi: hand-written UI class for the MainWindow.
Main circuit editing scene with undo/redo and user interaction.
static QString pixmapPath()
Definition DFlipFlop.h:32
static QPixmap pixmap(const ElementType type)
Returns the pixmap icon for the given element type.
static ElementType textToType(const QString &text)
Converts an element type name string to its ElementType enum value.
Icon and name widget shown in the left-panel element palette.
void addToSceneRequested(QMimeData *mimeData)
ElementPalette(MainWindowUi *ui, QObject *parent=nullptr)
Constructs the palette controller.
void updateICList(const QFileInfo &currentFile)
Refreshes the IC tab to reflect .panda files next to currentFile.
void updateTheme()
Updates the memory tab icon and all memory-tab ElementLabel themes.
void populate()
Populates all category tabs with ElementLabel widgets and sets tab icons.
void retranslateLabels()
Calls updateName() on every ElementLabel in the palette (on language change).
static bool nameMatchesSearch(const QString &name, const QString &query)
void updateEmbeddedICList(Scene *scene)
Refreshes the embedded-IC section of the palette from the scene's blob registry.
void addElementRequested(QMimeData *mimeData)
Emitted when the user presses Enter in the search box.
const QMap< QString, QByteArray > & blobMap() const
Returns a const reference to the full blob map (name → .panda bytes).
Definition ICRegistry.h:63
QSpacerItem * verticalSpacer_Search
QWidget * scrollAreaWidgetContents_Search
QTabWidget * tabElements
Main circuit editing scene.
Definition Scene.h:56
ICRegistry * icRegistry()
Returns the IC definition registry for this scene.
Definition Scene.h:365