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);
91 }
92 }
93
94 for (const QString &file : std::as_const(files)) {
95 const QPixmap pixmap(":/Components/Logic/ic-panda.svg");
96
97 auto *item = new ElementLabel(pixmap, ElementType::IC, file, m_ui->scrollAreaWidgetContents_IC);
98 connectDoubleClickAdd(item);
99 m_ui->scrollAreaWidgetContents_IC->layout()->addWidget(item);
100
101 auto *item2 = new ElementLabel(pixmap, ElementType::IC, file, m_ui->scrollAreaWidgetContents_Search);
102 connectDoubleClickAdd(item2);
103 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(item2);
104 }
105 }
106
107 m_ui->scrollAreaWidgetContents_IC->layout()->addItem(m_ui->verticalSpacer_IC);
108}
109
111{
112 // Remove existing embedded IC labels
113 const auto existingLabels = m_ui->scrollAreaWidgetContents_EmbeddedIC->findChildren<ElementLabel *>("label_embedded_ic");
114 for (auto *item : existingLabels) {
115 item->deleteLater();
116 }
117
118 const auto searchLabels = m_ui->scrollAreaWidgetContents_Search->findChildren<ElementLabel *>("label_embedded_ic");
119 for (auto *item : searchLabels) {
120 item->deleteLater();
121 }
122
123 if (!scene) {
124 return;
125 }
126
127 // Collect all blob names from the registry
128 QStringList seenBlobNames;
129 if (scene->icRegistry()) {
130 seenBlobNames = scene->icRegistry()->blobMap().keys();
131 }
132
133 // Remove spacer, add labels, restore spacer
134 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->removeItem(m_ui->verticalSpacer_EmbeddedIC);
135
136 const QPixmap pixmap(":/Components/Logic/ic-panda-embedded.svg");
137
138 for (const QString &bn : std::as_const(seenBlobNames)) {
139 auto *item = new ElementLabel(pixmap, ElementType::IC, bn, m_ui->scrollAreaWidgetContents_EmbeddedIC, true);
140 item->setObjectName("label_embedded_ic");
141 connectDoubleClickAdd(item);
142 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->addWidget(item);
143
144 auto *item2 = new ElementLabel(pixmap, ElementType::IC, bn, m_ui->scrollAreaWidgetContents_Search, true);
145 item2->setObjectName("label_embedded_ic");
146 connectDoubleClickAdd(item2);
147 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(item2);
148 }
149
150 m_ui->scrollAreaWidgetContents_EmbeddedIC->layout()->addItem(m_ui->verticalSpacer_EmbeddedIC);
151}
152
154{
155 const auto items = m_ui->tabElements->findChildren<ElementLabel *>();
156 for (auto *item : items) {
157 item->updateName();
158 }
159}
160
162{
163 const int memoryTabIndex = tabIndex("memory");
164 if (memoryTabIndex != -1) {
165 m_ui->tabElements->setTabIcon(memoryTabIndex, QIcon(DFlipFlop::pixmapPath()));
166 }
167
168 const auto labels = m_ui->memory->findChildren<ElementLabel *>();
169 for (auto *label : labels) {
170 label->updateTheme();
171 }
172}
173
174void ElementPalette::onSearchTextChanged(const QString &text)
175{
176 m_ui->scrollAreaWidgetContents_Search->layout()->removeItem(m_ui->verticalSpacer_Search);
177
178 const int searchTabIndex = tabIndex("search");
179
180 if (text.isEmpty()) {
181 // Restore the normal tab bar and return to the tab the user was on before typing.
182 m_ui->tabElements->tabBar()->show();
183 m_ui->tabElements->setCurrentIndex(m_lastTabIndex);
184 if (searchTabIndex != -1) {
185 m_ui->tabElements->setTabEnabled(searchTabIndex, false);
186 }
187 m_lastTabIndex = -1;
188 } else {
189 // On first keystroke, remember which tab was active so we can restore it.
190 if (m_lastTabIndex == -1) {
191 m_lastTabIndex = m_ui->tabElements->currentIndex();
192 }
193
194 // Hide the tab bar so only unified search results are visible.
195 m_ui->tabElements->tabBar()->hide();
196 if (searchTabIndex != -1) {
197 m_ui->tabElements->setCurrentIndex(searchTabIndex);
198 m_ui->tabElements->setTabEnabled(searchTabIndex, true);
199 }
200
201 // Walk the search tab's ElementLabel children once and reuse the result for
202 // all three passes below, instead of calling findChildren() three times.
203 const auto allItems = m_ui->scrollArea_Search->findChildren<ElementLabel *>();
204
205 // First pass: match by object name (e.g. "label_and") — prioritises exact type hits.
206 // The query is regex-escaped (as in nameMatchesSearch) so metacharacters can't form an
207 // invalid pattern; the ^label_ anchor keeps the query matching the type part rather than
208 // the shared "label_" prefix.
209 const QRegularExpression regex1(QString("^label_.*%1.*").arg(QRegularExpression::escape(text)), QRegularExpression::CaseInsensitiveOption);
210 QList<ElementLabel *> searchResults;
211 for (auto *item : allItems) {
212 if (regex1.match(item->objectName()).hasMatch()) {
213 searchResults.append(item);
214 }
215 }
216
217 // Second pass: match by human-readable translated element name.
218 for (auto *item : allItems) {
219 if (nameMatchesSearch(item->name(), text) && !searchResults.contains(item)) {
220 searchResults.append(item);
221 }
222 }
223
224 // Third pass: also search IC file names (all share object name "label_ic").
225 for (auto *item : allItems) {
226 if (item->objectName() == QLatin1String("label_ic") && nameMatchesSearch(item->icFileName(), text)) {
227 searchResults.append(item);
228 }
229 }
230
231 for (auto *item : allItems) {
232 item->setHidden(true);
233 }
234 for (auto *item : std::as_const(searchResults)) {
235 item->setVisible(true);
236 }
237 }
238
239 m_ui->scrollAreaWidgetContents_Search->layout()->addItem(m_ui->verticalSpacer_Search);
240}
241
242void ElementPalette::onSearchReturnPressed()
243{
244 const auto allLabels = m_ui->scrollArea_Search->findChildren<ElementLabel *>();
245
246 // Emit the first visible result so MainWindow can add it to the active scene.
247 for (auto *label : std::as_const(allLabels)) {
248 if (label->isVisible()) {
249 emit addElementRequested(label->mimeData());
250 m_ui->lineEditSearch->clear();
251 return;
252 }
253 }
254}
255
256void ElementPalette::populateMenu(QSpacerItem *spacer, const QStringList &names, QLayout *layout)
257{
258 layout->removeItem(spacer);
259
260 // Each element type gets two labels: one in its category panel and a
261 // duplicate in the search panel so search can find everything in one place.
262 QWidget *const container = layout->parentWidget();
263 for (const auto &name : names) {
264 const auto type = ElementFactory::textToType(name);
265 const auto pixmap = ElementFactory::pixmap(type);
266
267 auto *categoryLabel = new ElementLabel(pixmap, type, name, container);
268 connectDoubleClickAdd(categoryLabel);
269 layout->addWidget(categoryLabel);
270
271 auto *searchLabel = new ElementLabel(pixmap, type, name, m_ui->scrollAreaWidgetContents_Search);
272 connectDoubleClickAdd(searchLabel);
273 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(searchLabel);
274 }
275
276 layout->addItem(spacer);
277}
278
279void ElementPalette::connectDoubleClickAdd(ElementLabel *label)
280{
281 // Signal-to-signal: a double-clicked label re-emits through addElementRequested,
282 // the same route the search-box Return key already uses to add an element.
284}
285
286void ElementPalette::setupTabIcons()
287{
288 // Lookup by object name so reordering tabs in the UI doesn't break icon assignment.
289 const int ioTabIndex = tabIndex("io");
290 const int gatesTabIndex = tabIndex("gates");
291 const int combinationalTabIndex = tabIndex("combinational");
292 const int memoryTabIndex = tabIndex("memory");
293 const int icTabIndex = tabIndex("ic");
294 const int miscTabIndex = tabIndex("misc");
295 const int searchTabIndex = tabIndex("search");
296
297 if (ioTabIndex != -1) m_ui->tabElements->setTabIcon(ioTabIndex, QIcon(":/Components/Input/buttonOff.svg"));
298 if (gatesTabIndex != -1) m_ui->tabElements->setTabIcon(gatesTabIndex, QIcon(":/Components/Logic/xor.svg"));
299 if (combinationalTabIndex != -1) m_ui->tabElements->setTabIcon(combinationalTabIndex, QIcon(":/Components/Logic/truthtable-rotated.svg"));
300 if (memoryTabIndex != -1) m_ui->tabElements->setTabIcon(memoryTabIndex, QIcon(DFlipFlop::pixmapPath()));
301 if (icTabIndex != -1) m_ui->tabElements->setTabIcon(icTabIndex, QIcon(":/Components/Logic/ic-panda.svg"));
302 if (miscTabIndex != -1) m_ui->tabElements->setTabIcon(miscTabIndex, QIcon(":/Components/Misc/text.png"));
303 // The search tab is virtual; enabled only when the user types in the search box.
304 if (searchTabIndex != -1) m_ui->tabElements->setTabEnabled(searchTabIndex, false);
305}
306
307int ElementPalette::tabIndex(const QString &objectName) const
308{
309 for (int i = 0; i < m_ui->tabElements->count(); ++i) {
310 QWidget *tabWidget = m_ui->tabElements->widget(i);
311 if (tabWidget && tabWidget->objectName() == objectName) {
312 return i;
313 }
314 }
315 return -1;
316}
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:359