#include #include #include #include #include #include #include REGISTER_DOCK_WIDGET(ScreenshotsWidget, "Screenshots", true) ImageFrame::ImageFrame(QWidget* parent) : QWidget(parent), label(new QLabel), scrollArea(new QScrollArea(this)), ctrlPressed(false), scaleFactor(1.0) { setFocusPolicy(Qt::StrongFocus); label->setBackgroundRole(QPalette::Base); label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); label->setScaledContents(true); scrollArea->setBackgroundRole(QPalette::Dark); scrollArea->setWidget(label); scrollArea->viewport()->installEventFilter(this); auto layout = new QVBoxLayout(this); layout->addWidget(scrollArea); setLayout(layout); } void ImageFrame::setPixmap(const QPixmap& pix) { originalPixmap = pix; label->setPixmap(originalPixmap); scaleFactor = 1.0; resizeImage(); } void ImageFrame::resizeImage() const { if (!originalPixmap.isNull()) { label->resize(scaleFactor * originalPixmap.size()); } } void ImageFrame::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); resizeImage(); } QPixmap ImageFrame::pixmap() const { return originalPixmap; } void ImageFrame::keyPressEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Control) ctrlPressed = true; QWidget::keyPressEvent(e); } void ImageFrame::keyReleaseEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Control) ctrlPressed = false; QWidget::keyReleaseEvent(e); } bool ImageFrame::eventFilter(QObject* obj, QEvent* e) { if (obj == scrollArea->viewport() && e->type() == QEvent::Wheel) { auto we = static_cast(e); if (ctrlPressed) { const double step = (we->angleDelta().y() > 0) ? 1.1 : 0.9; scaleFactor *= step; scaleFactor = std::clamp(scaleFactor, 0.3, 4.0); resizeImage(); return true; } } return QWidget::eventFilter(obj, e); } void ImageFrame::clear() { originalPixmap = QPixmap(); label->setPixmap(QPixmap()); scaleFactor = 1.0; resizeImage(); } ScreenshotsWidget::ScreenshotsWidget(AdaptixWidget* w) : DockTab("Screenshots", w->GetProfile()->GetProject(), ":/icons/picture"), adaptixWidget(w) { this->createUI(); connect(tableView, &QTableView::customContextMenuRequested, this, &ScreenshotsWidget::handleScreenshotsMenu); connect(tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const QItemSelection &selected, const QItemSelection &deselected){ Q_UNUSED(selected) Q_UNUSED(deselected) if (!inputFilter->hasFocus()) tableView->setFocus(); }); connect(tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &ScreenshotsWidget::onTableItemSelection); connect(hideButton, &ClickableLabel::clicked, this, &ScreenshotsWidget::toggleSearchPanel); connect(inputFilter, &QLineEdit::textChanged, this, &ScreenshotsWidget::onFilterUpdate); connect(inputFilter, &QLineEdit::returnPressed, this, [this]() { proxyModel->setTextFilter(inputFilter->text()); }); connect(splitter, &QSplitter::splitterMoved, imageFrame, &ImageFrame::resizeImage); shortcutSearch = new QShortcut(QKeySequence("Ctrl+F"), this); shortcutSearch->setContext(Qt::WidgetWithChildrenShortcut); connect(shortcutSearch, &QShortcut::activated, this, &ScreenshotsWidget::toggleSearchPanel); auto shortcutEsc = new QShortcut(QKeySequence(Qt::Key_Escape), inputFilter); shortcutEsc->setContext(Qt::WidgetShortcut); connect(shortcutEsc, &QShortcut::activated, this, [this]() { searchWidget->setVisible(false); }); this->dockWidget->setWidget(this); } ScreenshotsWidget::~ScreenshotsWidget() = default; void ScreenshotsWidget::SetUpdatesEnabled(const bool enabled) { if (!enabled) { bufferingEnabled = true; } else { bufferingEnabled = false; flushPendingScreens(); } if (proxyModel) proxyModel->setDynamicSortFilter(enabled); if (tableView) tableView->setSortingEnabled(enabled); tableView->setUpdatesEnabled(enabled); } void ScreenshotsWidget::flushPendingScreens() { if (pendingScreens.isEmpty()) return; QList filtered; { QWriteLocker locker(&adaptixWidget->ScreenshotsLock); int count = 0; for (const auto& screen : pendingScreens) { if (adaptixWidget->Screenshots.contains(screen.ScreenId)) continue; adaptixWidget->Screenshots[screen.ScreenId] = screen; filtered.append(screen); } } if (!filtered.isEmpty()) screensModel->addBatch(filtered); pendingScreens.clear(); } void ScreenshotsWidget::createUI() { auto horizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); searchWidget = new QWidget(this); searchWidget->setVisible(false); searchWidget->setMaximumHeight(30); inputFilter = new QLineEdit(searchWidget); inputFilter->setPlaceholderText("filter: (admin | root) & ^(test)"); inputFilter->setMaximumWidth(300); autoSearchCheck = new QCheckBox("auto", searchWidget); autoSearchCheck->setChecked(true); autoSearchCheck->setToolTip("Auto search on text change. If unchecked, press Enter to search."); hideButton = new ClickableLabel(" x "); hideButton->setCursor(Qt::PointingHandCursor); searchLayout = new QHBoxLayout(searchWidget); searchLayout->setContentsMargins(0, 5, 0, 0); searchLayout->setSpacing(4); searchLayout->addWidget(inputFilter); searchLayout->addWidget(autoSearchCheck); searchLayout->addSpacing(8); searchLayout->addWidget(hideButton); searchLayout->addSpacerItem(horizontalSpacer); screensModel = new ScreensTableModel(this); proxyModel = new ScreensFilterProxyModel(this); proxyModel->setSourceModel(screensModel); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); tableView = new QTableView(this); tableView->setModel(proxyModel); tableView->setHorizontalHeader(new BoldHeaderView(Qt::Horizontal, tableView)); tableView->setContextMenuPolicy(Qt::CustomContextMenu); tableView->setAutoFillBackground(false); tableView->setShowGrid(false); tableView->setSortingEnabled(true); tableView->setWordWrap(true); tableView->setCornerButtonEnabled(false); tableView->setSelectionBehavior(QAbstractItemView::SelectRows); tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); tableView->setFocusPolicy(Qt::NoFocus); tableView->setAlternatingRowColors(true); tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); tableView->horizontalHeader()->setCascadingSectionResizes(true); tableView->horizontalHeader()->setHighlightSections(false); tableView->verticalHeader()->setVisible(false); tableView->sortByColumn(SCR_Date, Qt::AscendingOrder); tableView->horizontalHeader()->setSectionResizeMode(SCR_Note, QHeaderView::Stretch); tableView->setItemDelegate(new PaddingDelegate(tableView)); tableView->hideColumn(SCR_ScreenId); imageFrame = new ImageFrame(this); splitter = new QSplitter(this); splitter->setOrientation(Qt::Horizontal); splitter->addWidget(tableView); splitter->addWidget(imageFrame); splitter->setSizes(QList() << 80 << 200); mainGridLayout = new QGridLayout(this); mainGridLayout->setContentsMargins(0, 0, 0, 0); mainGridLayout->addWidget(searchWidget, 0, 0, 1, 1); mainGridLayout->addWidget(splitter, 1, 0, 1, 1); } void ScreenshotsWidget::Clear() const { { QWriteLocker locker(&adaptixWidget->ScreenshotsLock); adaptixWidget->Screenshots.clear(); } QSignalBlocker blocker(tableView->selectionModel()); screensModel->clear(); imageFrame->clear(); } void ScreenshotsWidget::AddScreenshotItem(const ScreenData &newScreen) { if (bufferingEnabled) { pendingScreens.append(newScreen); return; } QWriteLocker locker(&adaptixWidget->ScreenshotsLock); if (adaptixWidget->Screenshots.contains(newScreen.ScreenId)) return; adaptixWidget->Screenshots[newScreen.ScreenId] = newScreen; locker.unlock(); screensModel->add(newScreen); } void ScreenshotsWidget::EditScreenshotItem(const QString &screenId, const QString ¬e) { { QWriteLocker locker(&adaptixWidget->ScreenshotsLock); if (!adaptixWidget->Screenshots.contains(screenId)) return; adaptixWidget->Screenshots[screenId].Note = note; } screensModel->update(screenId, note); } void ScreenshotsWidget::RemoveScreenshotItem(const QString &screenId) { { QWriteLocker locker(&adaptixWidget->ScreenshotsLock); if (!adaptixWidget->Screenshots.contains(screenId)) return; adaptixWidget->Screenshots.remove(screenId); } screensModel->remove(screenId); if (screensModel->rowCount(QModelIndex()) == 0) imageFrame->clear(); } QString ScreenshotsWidget::getSelectedScreenId() const { QModelIndexList selected = tableView->selectionModel()->selectedRows(); if (selected.isEmpty()) return {}; QModelIndex sourceIndex = proxyModel->mapToSource(selected.first()); return screensModel->getScreenIdAt(sourceIndex.row()); } const ScreenData* ScreenshotsWidget::getSelectedScreen() const { QString screenId = getSelectedScreenId(); if (screenId.isEmpty()) return nullptr; return screensModel->getById(screenId); } QStringList ScreenshotsWidget::getSelectedScreenIds() const { QModelIndexList selected = tableView->selectionModel()->selectedRows(); QStringList ids; for (const QModelIndex& idx : selected) { QModelIndex sourceIndex = proxyModel->mapToSource(idx); QString id = screensModel->getScreenIdAt(sourceIndex.row()); if (!id.isEmpty()) ids.append(id); } return ids; } /// SLOTS void ScreenshotsWidget::toggleSearchPanel() const { if (this->searchWidget->isVisible()) { this->searchWidget->setVisible(false); proxyModel->setSearchVisible(false); } else { this->searchWidget->setVisible(true); proxyModel->setSearchVisible(true); inputFilter->setFocus(); } } void ScreenshotsWidget::onFilterUpdate() const { if (autoSearchCheck->isChecked()) { proxyModel->setTextFilter(inputFilter->text()); } inputFilter->setFocus(); } void ScreenshotsWidget::handleScreenshotsMenu(const QPoint &pos) { QModelIndex index = tableView->indexAt(pos); if (!index.isValid()) return; auto ctxMenu = QMenu(); ctxMenu.addAction("Set note", this, &ScreenshotsWidget::actionNote); ctxMenu.addAction("Download", this, &ScreenshotsWidget::actionDownload); ctxMenu.addAction("Delete", this, &ScreenshotsWidget::actionDelete); ctxMenu.exec(tableView->viewport()->mapToGlobal(pos)); } void ScreenshotsWidget::actionNote() { QStringList listId = getSelectedScreenIds(); if (listId.empty()) return; QString note = ""; if (listId.size() == 1) { const ScreenData* screen = getSelectedScreen(); if (screen) note = screen->Note; } bool inputOk; QString newNote = QInputDialog::getText(nullptr, "Set note", "New note", QLineEdit::Normal, note, &inputOk); if (inputOk) { HttpReqScreenSetNoteAsync(listId, newNote, *(adaptixWidget->GetProfile()), [](bool success, const QString& message, const QJsonObject&) { if (!success) MessageError(message.isEmpty() ? "Response timeout" : message); }); } } void ScreenshotsWidget::actionDownload() { const ScreenData* screen = getSelectedScreen(); if (!screen) return; ScreenData screenData = *screen; QString baseDir = QStringLiteral("screenshot.png"); if (adaptixWidget && adaptixWidget->GetProfile()) baseDir = QDir(adaptixWidget->GetProfile()->GetProjectDir()).filePath(QStringLiteral("screenshot.png")); NonBlockingDialogs::getSaveFileName(this, "Save File", baseDir, "All Files (*.*)", [this, screenData](const QString& filePath) { if (filePath.isEmpty()) return; QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { MessageError("Failed to open file for writing"); return; } file.write(screenData.Content); file.close(); QInputDialog inputDialog; inputDialog.setWindowTitle("Sync file"); inputDialog.setLabelText("File saved to:"); inputDialog.setTextEchoMode(QLineEdit::Normal); inputDialog.setTextValue(filePath); inputDialog.adjustSize(); inputDialog.move(QGuiApplication::primaryScreen()->geometry().center() - inputDialog.geometry().center()); inputDialog.exec(); }); } void ScreenshotsWidget::actionDelete() { QStringList listId = getSelectedScreenIds(); if (listId.empty()) return; HttpReqScreenRemoveAsync(listId, *(adaptixWidget->GetProfile()), [](bool success, const QString& message, const QJsonObject&) { if (!success) MessageError(message.isEmpty() ? "Response timeout" : message); }); } void ScreenshotsWidget::onTableItemSelection(const QModelIndex ¤t, const QModelIndex &previous) { Q_UNUSED(previous); if (!current.isValid()) return; QModelIndex sourceIndex = proxyModel->mapToSource(current); QString screenId = screensModel->getScreenIdAt(sourceIndex.row()); if (screenId.isEmpty()) return; const ScreenData* screenData = screensModel->getById(screenId); if (!screenData) return; auto image = QPixmap(); if (image.loadFromData(screenData->Content)) imageFrame->setPixmap(image); }