1- #include " containers.h"
1+ #include " containers.h"
2+
3+ QVariant WarpSourcesModel::data (const QModelIndex &index, int role) const
4+ {
5+ if (!index.isValid ())
6+ return {};
7+ if (index.row () < 0 || index.row () >= rowCount ())
8+ return {};
9+
10+ const auto &r = m_rows[static_cast <size_t >(index.row ())];
11+
12+ // Build a small two-dot status icon (left: writable, right: uncommitted)
13+ auto statusIcon = [](bool writable, bool uncommitted) -> QIcon {
14+ static QIcon cache[2 ][2 ]; // [writable][uncommitted]
15+ QIcon &cached = cache[writable ? 1 : 0 ][uncommitted ? 1 : 0 ];
16+ if (!cached.isNull ())
17+ return cached;
18+
19+ const int w = 16 , h = 12 , radius = 4 ;
20+ QPixmap pm (w, h);
21+ pm.fill (Qt::transparent);
22+ QPainter p (&pm);
23+ p.setRenderHint (QPainter::Antialiasing, true );
24+
25+ // Colors
26+ QColor writableOn (76 , 175 , 80 ); // green
27+ QColor writableOff (158 , 158 , 158 ); // grey
28+ QColor uncommittedOn (255 , 193 , 7 ); // amber
29+ QColor uncommittedOff (158 , 158 , 158 ); // grey
30+
31+ // Left dot: writable
32+ p.setBrush (writable ? writableOn : writableOff);
33+ p.setPen (Qt::NoPen);
34+ p.drawEllipse (QPoint (4 , h / 2 ), radius, radius);
35+
36+ // Right dot: uncommitted
37+ p.setBrush (uncommitted ? uncommittedOn : uncommittedOff);
38+ p.drawEllipse (QPoint (w - 6 , h / 2 ), radius, radius);
39+
40+ p.end ();
41+ cached = QIcon (pm);
42+ return cached;
43+ };
44+
45+ if (role == Qt::DecorationRole && index.column () == PathCol)
46+ {
47+ return statusIcon (r.writable , r.uncommitted );
48+ }
49+
50+ if (role == Qt::ToolTipRole && index.column () == PathCol)
51+ {
52+ QStringList parts;
53+ parts << (r.writable ? " Writable" : " Read-only" );
54+ parts << (r.uncommitted ? " Uncommitted changes" : " No uncommitted changes" );
55+ return parts.join (" • " );
56+ }
57+
58+ if (role == Qt::DisplayRole)
59+ {
60+ switch (index.column ())
61+ {
62+ case GuidCol: return r.guid ;
63+ case PathCol: return r.path ;
64+ case WritableCol: return r.writable ? " Yes" : " No" ;
65+ case UncommittedCol: return r.uncommitted ? " Yes" : " No" ;
66+ default : return {};
67+ }
68+ }
69+
70+ if (role == Qt::CheckStateRole)
71+ {
72+ // Optional: expose as checkboxes if someone ever shows these columns
73+ switch (index.column ())
74+ {
75+ case WritableCol: return r.writable ? Qt::Checked : Qt::Unchecked;
76+ case UncommittedCol: return r.uncommitted ? Qt::Checked : Qt::Unchecked;
77+ default : break ;
78+ }
79+ }
80+
81+ return {};
82+ }
83+
84+ WarpContainerWidget::WarpContainerWidget (Warp::Ref<Warp::Container> container, QWidget *parent) : QWidget(parent)
85+ {
86+ m_container = std::move (container);
87+ auto *layout = new QVBoxLayout (this );
88+ layout->setContentsMargins (0 , 0 , 0 , 0 );
89+ m_tabs = new QTabWidget (this );
90+ layout->addWidget (m_tabs);
91+
92+ // Sources tab
93+ m_sourcesPage = new QWidget (this );
94+ auto *sourcesLayout = new QVBoxLayout (m_sourcesPage);
95+ m_sourcesView = new QTableView (m_sourcesPage);
96+ m_sourcesModel = new WarpSourcesModel (m_sourcesPage);
97+ m_sourcesModel->setContainer (m_container);
98+ m_sourcesView->setModel (m_sourcesModel);
99+ m_sourcesView->horizontalHeader ()->setStretchLastSection (true );
100+ m_sourcesView->setSelectionBehavior (QAbstractItemView::SelectRows);
101+ m_sourcesView->setSelectionMode (QAbstractItemView::SingleSelection);
102+
103+ // Make the table look like a simple list that shows only the source path
104+ m_sourcesView->setShowGrid (false );
105+ m_sourcesView->verticalHeader ()->setVisible (false );
106+ m_sourcesView->horizontalHeader ()->setVisible (false );
107+ m_sourcesView->setAlternatingRowColors (false );
108+ m_sourcesView->setEditTriggers (QAbstractItemView::NoEditTriggers);
109+ m_sourcesView->setHorizontalScrollBarPolicy (Qt::ScrollBarAlwaysOff);
110+ m_sourcesView->setWordWrap (false );
111+ m_sourcesView->setIconSize (QSize (16 , 12 ));
112+ // Ensure long paths truncate from the left: "...tail/of/the/path"
113+ m_sourcesView->setTextElideMode (Qt::ElideLeft);
114+ // Hide GUID column, keep only the Path column visible
115+ m_sourcesView->setColumnHidden (WarpSourcesModel::GuidCol, true );
116+ // Also hide boolean columns; their state is shown as an icon next to the path
117+ m_sourcesView->setColumnHidden (WarpSourcesModel::WritableCol, true );
118+ m_sourcesView->setColumnHidden (WarpSourcesModel::UncommittedCol, true );
119+ // Ensure the remaining (Path) column fills the width
120+ m_sourcesView->horizontalHeader ()->setSectionResizeMode (WarpSourcesModel::PathCol, QHeaderView::Stretch);
121+
122+ // Per-item context menu
123+ m_sourcesView->setContextMenuPolicy (Qt::CustomContextMenu);
124+ connect (m_sourcesView, &QWidget::customContextMenuRequested, this , [this ](const QPoint &pos) {
125+ QMenu menu (m_sourcesView);
126+ const QModelIndex index = m_sourcesView->indexAt (pos);
127+
128+ if (!index.isValid ())
129+ {
130+ QAction *actAdd = menu.addAction (tr (" Add Source" ));
131+ QAction *chosen = menu.exec (m_sourcesView->viewport ()->mapToGlobal (pos));
132+ if (!chosen)
133+ return ;
134+ if (chosen == actAdd)
135+ {
136+ std::string sourceName;
137+ if (!BinaryNinja::GetTextLineInput (sourceName, " Source name:" , " Add Source" ))
138+ return ;
139+ if (const auto sourceId = m_container->AddSource (sourceName); !sourceId.has_value ())
140+ {
141+ BinaryNinja::LogAlertF (" Failed to add source: {}" , sourceName);
142+ return ;
143+ }
144+ m_sourcesModel->reload ();
145+ }
146+ } else
147+ {
148+ m_sourcesView->setCurrentIndex (index.sibling (index.row (), WarpSourcesModel::PathCol));
149+
150+ const int row = index.row ();
151+ const QModelIndex pathIdx = m_sourcesModel->index (row, WarpSourcesModel::PathCol);
152+ const QModelIndex guidIdx = m_sourcesModel->index (row, WarpSourcesModel::GuidCol);
153+ const QString path = m_sourcesModel->data (pathIdx, Qt::DisplayRole).toString ();
154+ const QFileInfo fi (path);
155+
156+ const QString guid = m_sourcesModel->data (guidIdx, Qt::DisplayRole).toString ();
157+
158+ QAction *actReveal = menu.addAction (tr (" Reveal in File Browser" ));
159+ actReveal->setEnabled (fi.exists ());
160+ QAction *actCopyPath = menu.addAction (tr (" Copy Path" ));
161+ QAction *actCopyGuid = menu.addAction (tr (" Copy GUID" ));
162+
163+ QAction *chosen = menu.exec (m_sourcesView->viewport ()->mapToGlobal (pos));
164+ if (!chosen)
165+ return ;
166+ if (chosen == actCopyPath)
167+ QGuiApplication::clipboard ()->setText (path);
168+ else if (chosen == actCopyGuid)
169+ QGuiApplication::clipboard ()->setText (guid);
170+ else if (chosen == actReveal)
171+ QDesktopServices::openUrl (QUrl::fromLocalFile (fi.absoluteFilePath ()));
172+ }
173+ });
174+
175+
176+ sourcesLayout->addWidget (m_sourcesView);
177+ m_tabs->addTab (m_sourcesPage, tr (" Sources" ));
178+
179+ // Search tab
180+ m_searchTab = new WarpSearchWidget (m_container, this );
181+ m_tabs->addTab (m_searchTab, tr (" Search" ));
182+
183+ // Periodic refresh timer for the Sources view
184+ m_refreshTimer = new QTimer (this );
185+ m_refreshTimer->setInterval (5000 );
186+ connect (m_refreshTimer, &QTimer::timeout, this , [this ]() {
187+ // Only refresh if the widget and the Sources page are actually visible
188+ if (!this ->isVisible () || !m_sourcesPage || !m_sourcesPage->isVisible ())
189+ return ;
190+
191+ // Preserve selection by GUID across reloads
192+ QString currentGuid;
193+ if (const QModelIndex currentIdx = m_sourcesView->currentIndex (); currentIdx.isValid ())
194+ {
195+ const int row = currentIdx.row ();
196+ const QModelIndex guidIdx = m_sourcesModel->index (row, WarpSourcesModel::GuidCol);
197+ currentGuid = m_sourcesModel->data (guidIdx, Qt::DisplayRole).toString ();
198+ }
199+
200+ m_sourcesModel->reload ();
201+
202+ if (!currentGuid.isEmpty ())
203+ {
204+ for (int r = 0 ; r < m_sourcesModel->rowCount (); ++r)
205+ {
206+ const QModelIndex gIdx = m_sourcesModel->index (r, WarpSourcesModel::GuidCol);
207+ if (m_sourcesModel->data (gIdx , Qt::DisplayRole).toString () == currentGuid)
208+ {
209+ m_sourcesView->setCurrentIndex (m_sourcesModel->index (r, WarpSourcesModel::PathCol));
210+ break ;
211+ }
212+ }
213+ }
214+ });
215+ m_refreshTimer->start ();
216+
217+ // Optional: force a refresh when switching back to the Sources tab
218+ connect (m_tabs, &QTabWidget::currentChanged, this , [this ](const int idx) {
219+ QWidget *w = m_tabs->widget (idx);
220+ if (w == m_sourcesPage)
221+ m_sourcesModel->reload ();
222+ });
223+ }
224+
225+ WarpContainersPane::WarpContainersPane (QWidget *parent) : QWidget(parent)
226+ {
227+ auto *splitter = new QSplitter (Qt::Vertical, this );
228+ splitter->setContentsMargins (0 , 0 , 0 , 0 );
229+ auto *mainLayout = new QVBoxLayout (this );
230+ mainLayout->setContentsMargins (0 , 0 , 0 , 0 );
231+ mainLayout->setSpacing (0 );
232+ mainLayout->addWidget (splitter);
233+ auto newPalette = palette ();
234+ newPalette.setColor (QPalette::Window, getThemeColor (SidebarWidgetBackgroundColor));
235+ setAutoFillBackground (true );
236+ setPalette (newPalette);
237+
238+ // List on top
239+ m_list = new QListWidget (splitter);
240+ m_list->setSelectionMode (QAbstractItemView::SingleSelection);
241+ m_list->setHorizontalScrollBarPolicy (Qt::ScrollBarAlwaysOff);
242+ m_list->setSizeAdjustPolicy (QAbstractScrollArea::AdjustToContents);
243+ m_list->setUniformItemSizes (true );
244+
245+ // Make names larger and show end of long strings (elide at the start)
246+ {
247+ QFont f = m_list->font ();
248+ f.setPointSizeF (f.pointSizeF () + 2.0 ); // bump size
249+ m_list->setFont (f);
250+ m_list->setTextElideMode (Qt::ElideLeft);
251+ }
252+
253+ // Container view (tabs) below
254+ m_stack = new QStackedWidget (splitter);
255+ m_stack->setContentsMargins (0 , 0 , 0 , 0 );
256+
257+ splitter->setStretchFactor (0 , 0 ); // list: minimal growth
258+ splitter->setStretchFactor (1 , 1 ); // stack: takes remaining space
259+ splitter->setCollapsible (0 , false );
260+ splitter->setCollapsible (1 , false );
261+
262+ populate ();
263+
264+ connect (m_list, &QListWidget::currentRowChanged, this , [this ](int row) {
265+ if (row >= 0 && row < m_stack->count ())
266+ m_stack->setCurrentIndex (row);
267+ });
268+
269+ // Select the first container if available
270+ if (m_list->count () > 0 )
271+ {
272+ m_list->setCurrentRow (0 );
273+ }
274+ }
0 commit comments