feat: type hints green [bracketed] notation, workspace cleanup, unique naming

- Type inference hints now show value-first with bracketed type in comment
  green: "0x7ff718570000 [ptr64]", "6, 16 [int32_t×2]"
- Raise hint threshold to strong-only (score >= 75%)
- Remove Bool inference, widen Int16 range to ±16384
- Workspace: remove dead WorkspaceProxy, fix null deref, debounce search,
  cache icons, add pinning support
- Unique naming: UnnamedClass0/UnnamedEnum1 with global counter
- Footer buttons: +10h +100h +1000h replacing +1024
- MCP: project lifecycle API, snapshot provider fix
This commit is contained in:
IChooseYou
2026-03-09 10:39:22 -06:00
parent a21e5a07a8
commit 483f87cfbd
20 changed files with 310 additions and 2069 deletions

View File

@@ -604,6 +604,25 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createWorkspaceDock();
createScannerDock();
// Hidden sentinel dock — never visible, only used to force Qt to create a
// QTabBar when the first document dock is added (Qt only creates tab bars
// via tabifyDockWidget). Immediately hidden after tabification so it takes
// zero layout space. An event filter on the QTabBar keeps it visible.
{
m_sentinelDock = new QDockWidget(this);
m_sentinelDock->setObjectName(QStringLiteral("_sentinel"));
m_sentinelDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
auto* sw = new QWidget(m_sentinelDock);
sw->setFixedSize(0, 0);
m_sentinelDock->setWidget(sw);
auto* stb = new QWidget(m_sentinelDock);
stb->setFixedHeight(0);
m_sentinelDock->setTitleBarWidget(stb);
addDockWidget(Qt::TopDockWidgetArea, m_sentinelDock);
m_sentinelDock->hide(); // hidden = zero layout space
}
createMenus();
createStatusBar();
@@ -1644,6 +1663,16 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
else
addDockWidget(Qt::TopDockWidgetArea, dock);
// Bootstrap: tabify the hidden sentinel with the first doc dock so Qt
// creates a QTabBar. Then hide sentinel (zero layout space). The event
// filter in eventFilter() keeps the tab bar visible even at count==1.
if (m_sentinelDock && m_docDocks.isEmpty()) {
m_sentinelDock->show();
tabifyDockWidget(dock, m_sentinelDock);
m_sentinelDock->hide();
dock->raise();
}
m_docDocks.append(dock);
m_tabs[dock] = { doc, ctrl, splitter, {}, 0 };
m_activeDocDock = dock;
@@ -1698,7 +1727,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
rebuildAllDocs();
rebuildWorkspaceModel();
if (m_tabs.isEmpty())
if (m_tabs.isEmpty() && !m_closingAll)
project_new();
});
@@ -1780,6 +1809,10 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
updateWindowTitle();
});
});
// Notify MCP clients of tree changes
connect(doc, &RcxDocument::documentChanged, this, [this]() {
if (m_mcp) m_mcp->notifyTreeChanged();
});
connect(&doc->undoStack, &QUndoStack::indexChanged,
this, [this, dockGuard](int) {
if (!dockGuard) return;
@@ -1875,6 +1908,9 @@ void MainWindow::setupDockTabBars() {
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
}
// Force tab bar visible (event filter keeps it alive, belt-and-suspenders)
tabBar->show();
// Install tab buttons for any tab that doesn't have them yet
for (int i = 0; i < tabBar->count(); ++i) {
auto* existing = qobject_cast<DockTabButtons*>(
@@ -2010,6 +2046,25 @@ void MainWindow::setupDockTabBars() {
}
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
// Keep dock tab bars visible even when Qt wants to hide them (count==1).
// Qt's QMainWindowLayout calls setVisible(false) on the QTabBar when only
// one dock remains in a tab group. We catch the resulting Hide event and
// immediately re-show the tab bar, provided at least one doc dock is docked.
if (event->type() == QEvent::Hide && !m_tabBarShowGuard) {
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
if (tabBar->parent() == this && tabBar->count() >= 1) {
bool hasDockedDoc = false;
for (auto* d : m_docDocks)
if (!d->isFloating() && d->isVisible()) { hasDockedDoc = true; break; }
if (hasDockedDoc) {
m_tabBarShowGuard = true;
tabBar->show();
m_tabBarShowGuard = false;
return true;
}
}
}
}
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::MiddleButton) {
@@ -3107,8 +3162,10 @@ void MainWindow::importReclassXml() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
setAppStatus(QStringLiteral("Imported %1 classes from %2")
.arg(classCount).arg(QFileInfo(filePath).fileName()));
@@ -3156,8 +3213,10 @@ void MainWindow::importFromSource() {
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3210,8 +3269,10 @@ void MainWindow::importPdb() {
auto* doc = new rcx::RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
createTab(doc);
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3408,8 +3469,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
}
auto* doc = new RcxDocument(this);
doc->tree = std::move(tree);
closeAllDocDocks();
auto* dock = createTab(doc);
QDockWidget* dock;
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
dock = createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3433,9 +3497,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
}
// Close all existing tabs so the project replaces the current state
closeAllDocDocks();
auto* dock = createTab(doc);
QDockWidget* dock;
{ ClosingGuard guard(m_closingAll);
closeAllDocDocks();
dock = createTab(doc);
}
rebuildWorkspaceModel();
if (!m_docDocks.isEmpty()) {
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
@@ -3750,6 +3816,16 @@ void MainWindow::createWorkspaceDock() {
actConvert = menu.addAction("Convert to Class");
}
// Pin/Unpin
bool allPinned = true;
for (const auto& item : items)
if (!m_pinnedIds.contains(item.structId)) { allPinned = false; break; }
auto* actPin = menu.addAction(
QIcon(QStringLiteral(":/vsicons/pin.svg")),
allPinned ? QStringLiteral("Unpin") : QStringLiteral("Pin"));
menu.addSeparator();
// Delete: works for single or multi
QString delLabel = items.size() == 1
? QStringLiteral("Delete")
@@ -3941,6 +4017,17 @@ void MainWindow::createWorkspaceDock() {
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
rcx::cmd::ChangeClassKeyword{item.structId, item.keyword, newKw}));
rebuildWorkspaceModel();
} else if (chosen && chosen == actPin) {
for (const auto& item : items) {
if (allPinned)
m_pinnedIds.remove(item.structId);
else
m_pinnedIds.insert(item.structId);
}
// Full rebuild to reorder pinned items to top
m_workspaceModel->removeRows(0, m_workspaceModel->rowCount());
rebuildWorkspaceModelNow();
}
});
@@ -4244,9 +4331,9 @@ void MainWindow::rebuildWorkspaceModelNow() {
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
}
rcx::syncProjectExplorer(m_workspaceModel, tabs);
rcx::syncProjectExplorer(m_workspaceModel, tabs, m_pinnedIds);
// Mark items that are currently viewed in a tab
// Mark items that are currently viewed in a tab + pinned state
QSet<uint64_t> viewedIds;
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
viewedIds.insert(it->ctrl->viewRootId());
@@ -4255,6 +4342,7 @@ void MainWindow::rebuildWorkspaceModelNow() {
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
item->setData(m_pinnedIds.contains(id), Qt::UserRole + 4);
}
if (m_dockTitleLabel) {