File indexing completed on 2024-04-28 03:58:04

0001 /*
0002     SPDX-FileCopyrightText: 2023 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "screenshotdialog.h"
0006 
0007 #include "katedocument.h"
0008 #include "kateglobal.h"
0009 #include "katelinelayout.h"
0010 #include "katerenderer.h"
0011 #include "kateview.h"
0012 
0013 #include <QActionGroup>
0014 #include <QApplication>
0015 #include <QBitmap>
0016 #include <QCheckBox>
0017 #include <QClipboard>
0018 #include <QColorDialog>
0019 #include <QDebug>
0020 #include <QFileDialog>
0021 #include <QGraphicsDropShadowEffect>
0022 #include <QImageWriter>
0023 #include <QLabel>
0024 #include <QMenu>
0025 #include <QMessageBox>
0026 #include <QPainter>
0027 #include <QPainterPath>
0028 #include <QPushButton>
0029 #include <QScrollArea>
0030 #include <QScrollBar>
0031 #include <QTimer>
0032 #include <QToolButton>
0033 #include <QVBoxLayout>
0034 
0035 #include <KConfigGroup>
0036 #include <KLocalizedString>
0037 #include <KSyntaxHighlighting/Theme>
0038 
0039 using namespace KTextEditor;
0040 
0041 class BaseWidget : public QWidget
0042 {
0043 public:
0044     explicit BaseWidget(QWidget *parent = nullptr)
0045         : QWidget(parent)
0046         , m_screenshot(new QLabel(this))
0047     {
0048         setAutoFillBackground(true);
0049         setContentsMargins({});
0050         auto layout = new QHBoxLayout(this);
0051         setColor(Qt::yellow);
0052 
0053         layout->addStretch();
0054         layout->addWidget(m_screenshot);
0055         layout->addStretch();
0056 
0057         m_renableEffects.setInterval(500);
0058         m_renableEffects.setSingleShot(true);
0059         m_renableEffects.callOnTimeout(this, &BaseWidget::enableDropShadow);
0060     }
0061 
0062     void setColor(QColor c)
0063     {
0064         auto p = palette();
0065         p.setColor(QPalette::Base, c);
0066         p.setColor(QPalette::Window, c);
0067         setPalette(p);
0068     }
0069 
0070     void setPixmap(const QPixmap &p)
0071     {
0072         temporarilyDisableDropShadow();
0073 
0074         m_screenshot->setPixmap(p);
0075         m_screenshotSize = p.size();
0076     }
0077 
0078     QPixmap grabPixmap()
0079     {
0080         const int h = m_screenshotSize.height();
0081         const int y = std::max(((height() - h) / 2), 0);
0082         const int x = m_screenshot->geometry().x();
0083         QRect r(x, y, m_screenshotSize.width(), m_screenshotSize.height());
0084         r.adjust(-6, -6, 6, 6);
0085         return grab(r);
0086     }
0087 
0088     void temporarilyDisableDropShadow()
0089     {
0090         // Disable drop shadow because on large pixmaps
0091         // it is too slow
0092         m_screenshot->setGraphicsEffect(nullptr);
0093         m_renableEffects.start();
0094     }
0095 
0096 private:
0097     void enableDropShadow()
0098     {
0099         QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(m_screenshot);
0100         e->setColor(Qt::black);
0101         e->setOffset(2.);
0102         e->setBlurRadius(15.);
0103         m_screenshot->setGraphicsEffect(e);
0104     }
0105 
0106     QLabel *const m_screenshot;
0107     QSize m_screenshotSize;
0108     QTimer m_renableEffects;
0109 
0110     friend class ScrollArea;
0111 };
0112 
0113 class ScrollArea : public QScrollArea
0114 {
0115 public:
0116     explicit ScrollArea(BaseWidget *contents, QWidget *parent = nullptr)
0117         : QScrollArea(parent)
0118         , m_base(contents)
0119     {
0120     }
0121 
0122 private:
0123     void scrollContentsBy(int dx, int dy) override
0124     {
0125         m_base->temporarilyDisableDropShadow();
0126         QScrollArea::scrollContentsBy(dx, dy);
0127     }
0128 
0129 private:
0130     BaseWidget *const m_base;
0131 };
0132 
0133 ScreenshotDialog::ScreenshotDialog(KTextEditor::Range selRange, KTextEditor::ViewPrivate *parent)
0134     : QDialog(parent)
0135     , m_base(new BaseWidget(this))
0136     , m_selRange(selRange)
0137     , m_scrollArea(new ScrollArea(m_base, this))
0138     , m_saveButton(new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save")))
0139     , m_copyButton(new QPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy")))
0140     , m_changeBGColor(new QPushButton(QIcon::fromTheme(QStringLiteral("color-fill")), i18n("Background Color...")))
0141     , m_lineNumButton(new QToolButton(this))
0142     , m_extraDecorations(new QCheckBox(i18n("Show Extra Decorations"), this))
0143     , m_windowDecorations(new QCheckBox(i18n("Show Window Decorations"), this))
0144     , m_lineNumMenu(new QMenu(this))
0145     , m_resizeTimer(new QTimer(this))
0146 {
0147     setModal(true);
0148     setWindowTitle(i18n("Screenshot..."));
0149 
0150     m_scrollArea->setWidget(m_base);
0151     m_scrollArea->setWidgetResizable(true);
0152     m_scrollArea->setAutoFillBackground(true);
0153     m_scrollArea->setAttribute(Qt::WA_Hover, false);
0154     m_scrollArea->setFrameStyle(QFrame::NoFrame);
0155 
0156     auto baseLayout = new QVBoxLayout(this);
0157     baseLayout->setContentsMargins(0, 0, 0, 4);
0158     baseLayout->addWidget(m_scrollArea);
0159 
0160     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0161     const int color = cg.readEntry("BackgroundColor", EditorPrivate::self()->theme().textColor(KSyntaxHighlighting::Theme::Normal));
0162     const auto c = QColor::fromRgba(color);
0163     m_base->setColor(c);
0164     m_scrollArea->setPalette(m_base->palette());
0165 
0166     auto bottomBar = new QHBoxLayout();
0167     baseLayout->addLayout(bottomBar);
0168     bottomBar->setContentsMargins(0, 0, 4, 0);
0169     bottomBar->addStretch();
0170     bottomBar->addWidget(m_windowDecorations);
0171     bottomBar->addWidget(m_extraDecorations);
0172     bottomBar->addWidget(m_lineNumButton);
0173     bottomBar->addWidget(m_changeBGColor);
0174     bottomBar->addWidget(m_saveButton);
0175     bottomBar->addWidget(m_copyButton);
0176     connect(m_saveButton, &QPushButton::clicked, this, &ScreenshotDialog::onSaveClicked);
0177     connect(m_copyButton, &QPushButton::clicked, this, &ScreenshotDialog::onCopyClicked);
0178     connect(m_changeBGColor, &QPushButton::clicked, this, [this] {
0179         QColorDialog dlg(this);
0180         int e = dlg.exec();
0181         if (e == QDialog::Accepted) {
0182             QColor c = dlg.selectedColor();
0183             m_base->setColor(c);
0184             m_scrollArea->setPalette(m_base->palette());
0185 
0186             KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0187             cg.writeEntry("BackgroundColor", c.rgba());
0188         }
0189     });
0190 
0191     connect(m_extraDecorations, &QCheckBox::toggled, this, [this] {
0192         renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
0193         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0194         cg.writeEntry<bool>("ShowExtraDecorations", m_extraDecorations->isChecked());
0195     });
0196     m_extraDecorations->setChecked(cg.readEntry<bool>("ShowExtraDecorations", true));
0197 
0198     connect(m_windowDecorations, &QCheckBox::toggled, this, [this] {
0199         renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
0200         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0201         cg.writeEntry<bool>("ShowWindowDecorations", m_windowDecorations->isChecked());
0202     });
0203     m_windowDecorations->setChecked(cg.readEntry<bool>("ShowWindowDecorations", true));
0204 
0205     {
0206         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0207         int i = cg.readEntry("LineNumbers", (int)ShowAbsoluteLineNums);
0208 
0209         auto gp = new QActionGroup(m_lineNumMenu);
0210         auto addMenuAction = [this, gp](const QString &text, int data) {
0211             auto a = new QAction(text, m_lineNumMenu);
0212             a->setCheckable(true);
0213             a->setActionGroup(gp);
0214             m_lineNumMenu->addAction(a);
0215             connect(a, &QAction::triggered, this, [this, data] {
0216                 onLineNumChangedClicked(data);
0217             });
0218             return a;
0219         };
0220         addMenuAction(i18n("Don't Show Line Numbers"), DontShowLineNums)->setChecked(i == DontShowLineNums);
0221         addMenuAction(i18n("Show Line Numbers From 1"), ShowAbsoluteLineNums)->setChecked(i == ShowAbsoluteLineNums);
0222         addMenuAction(i18n("Show Actual Line Numbers"), ShowActualLineNums)->setChecked(i == ShowActualLineNums);
0223 
0224         m_showLineNumbers = i != DontShowLineNums;
0225         m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
0226     }
0227 
0228     m_lineNumButton->setText(i18n("Line Numbers"));
0229     m_lineNumButton->setPopupMode(QToolButton::InstantPopup);
0230     m_lineNumButton->setMenu(m_lineNumMenu);
0231 
0232     m_resizeTimer->setSingleShot(true);
0233     m_resizeTimer->setInterval(500);
0234     m_resizeTimer->callOnTimeout(this, [this] {
0235         renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
0236         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0237         cg.writeEntry("Geometry", saveGeometry());
0238     });
0239 
0240     const QByteArray geometry = cg.readEntry("Geometry", QByteArray());
0241     if (!geometry.isEmpty()) {
0242         restoreGeometry(geometry);
0243     }
0244 }
0245 
0246 ScreenshotDialog::~ScreenshotDialog()
0247 {
0248     m_resizeTimer->stop();
0249 }
0250 
0251 void ScreenshotDialog::renderScreenshot(KateRenderer *r)
0252 {
0253     if (m_selRange.isEmpty()) {
0254         return;
0255     }
0256 
0257     constexpr int leftMargin = 16;
0258     constexpr int rightMargin = 16;
0259     constexpr int topMargin = 8;
0260     constexpr int bottomMargin = 8;
0261     constexpr int lnNoAreaSpacing = 8;
0262 
0263     KateRenderer renderer(r->doc(), r->folding(), r->view());
0264     renderer.setPrinterFriendly(!m_extraDecorations->isChecked());
0265 
0266     int startLine = m_selRange.start().line();
0267     int endLine = m_selRange.end().line();
0268 
0269     int width = std::min(1024, std::max(400, this->width() - (m_scrollArea->horizontalScrollBar()->height())));
0270 
0271     // If the font is fixed width, try to find the best width
0272     const bool fixedWidth = QFontInfo(renderer.currentFont()).fixedPitch();
0273     if (fixedWidth) {
0274         int maxLineWidth = 0;
0275         auto doc = renderer.view()->doc();
0276         int w = renderer.currentFontMetrics().averageCharWidth();
0277         for (int line = startLine; line <= endLine; ++line) {
0278             maxLineWidth = std::max(maxLineWidth, (doc->lineLength(line) * w));
0279         }
0280 
0281         const int windowWidth = width;
0282         if (maxLineWidth > windowWidth) {
0283             maxLineWidth = windowWidth;
0284         }
0285 
0286         width = std::min(1024, maxLineWidth);
0287         width = std::max(400, width);
0288     }
0289 
0290     // Collect line layouts and calculate the needed height
0291     const int xEnd = width;
0292     int height = 0;
0293     std::vector<std::unique_ptr<KateLineLayout>> lineLayouts;
0294     for (int line = startLine; line <= endLine; ++line) {
0295         auto lineLayout = std::make_unique<KateLineLayout>(renderer);
0296         lineLayout->setLine(line, -1);
0297         renderer.layoutLine(lineLayout.get(), xEnd, false /* no layout cache */);
0298         height += lineLayout->viewLineCount() * renderer.lineHeight();
0299         lineLayouts.push_back(std::move(lineLayout));
0300     }
0301 
0302     if (m_windowDecorations->isChecked()) {
0303         height += renderer.lineHeight() + topMargin + bottomMargin;
0304     } else {
0305         height += topMargin + bottomMargin; // topmargin
0306     }
0307 
0308     int xStart = -leftMargin;
0309     int lineNoAreaWidth = 0;
0310     if (m_showLineNumbers) {
0311         int lastLine = m_absoluteLineNumbers ? (endLine - startLine) + 1 : endLine;
0312         const int lnNoWidth = renderer.currentFontMetrics().horizontalAdvance(QString::number(lastLine));
0313         lineNoAreaWidth = lnNoWidth + lnNoAreaSpacing;
0314         width += lineNoAreaWidth;
0315         xStart += -lineNoAreaWidth;
0316     }
0317 
0318     width += leftMargin + rightMargin;
0319     QPixmap pix(width, height);
0320     pix.fill(renderer.view()->rendererConfig()->backgroundColor());
0321 
0322     QPainter paint(&pix);
0323 
0324     paint.translate(0, topMargin);
0325 
0326     if (m_windowDecorations->isChecked()) {
0327         int midY = (renderer.lineHeight() + 4) / 2;
0328         int x = 24;
0329         paint.save();
0330         paint.setRenderHint(QPainter::Antialiasing, true);
0331         paint.setPen(Qt::NoPen);
0332 
0333         QBrush b(QColor(0xff5f5a)); // red
0334         paint.setBrush(b);
0335         paint.drawEllipse(QPoint(x, midY), 8, 8);
0336 
0337         x += 24;
0338         b = QColor(0xffbe2e);
0339         paint.setBrush(b);
0340         paint.drawEllipse(QPoint(x, midY), 8, 8);
0341 
0342         x += 24;
0343         b = QColor(0x2aca44);
0344         paint.setBrush(b);
0345         paint.drawEllipse(QPoint(x, midY), 8, 8);
0346 
0347         paint.setRenderHint(QPainter::Antialiasing, false);
0348         paint.restore();
0349 
0350         paint.translate(0, renderer.lineHeight() + 4);
0351     }
0352 
0353     KateRenderer::PaintTextLineFlags flags;
0354     flags.setFlag(KateRenderer::SkipDrawFirstInvisibleLineUnderlined);
0355     flags.setFlag(KateRenderer::SkipDrawLineSelection);
0356     int lineNo = m_absoluteLineNumbers ? 1 : startLine + 1;
0357     paint.setFont(renderer.currentFont());
0358     for (auto &lineLayout : lineLayouts) {
0359         renderer.paintTextLine(paint, lineLayout.get(), xStart, xEnd, QRectF{}, nullptr, flags);
0360         // draw line number
0361         if (lineNoAreaWidth != 0) {
0362             paint.drawText(QRect(leftMargin - lnNoAreaSpacing, 0, lineNoAreaWidth, renderer.lineHeight()),
0363                            Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter,
0364                            QString::number(lineNo++));
0365         }
0366         // translate for next line
0367         paint.translate(0, lineLayout->viewLineCount() * renderer.lineHeight());
0368     }
0369 
0370     m_base->setPixmap(pix);
0371 }
0372 
0373 void ScreenshotDialog::onSaveClicked()
0374 {
0375     const auto name = QFileDialog::getSaveFileName(this, i18n("Save..."));
0376     if (name.isEmpty()) {
0377         return;
0378     }
0379 
0380     QImageWriter writer(name);
0381     writer.write(m_base->grabPixmap().toImage());
0382     if (!writer.errorString().isEmpty()) {
0383         QMessageBox::warning(this, i18nc("@title:window", "Screenshot saving failed"), i18n("Screenshot saving failed: %1", writer.errorString()));
0384     }
0385 }
0386 
0387 void ScreenshotDialog::onCopyClicked()
0388 {
0389     if (auto clip = qApp->clipboard()) {
0390         clip->setPixmap(m_base->grabPixmap(), QClipboard::Clipboard);
0391     }
0392 }
0393 
0394 void ScreenshotDialog::resizeEvent(QResizeEvent *e)
0395 {
0396     QDialog::resizeEvent(e);
0397     if (!m_firstShow) {
0398         m_resizeTimer->start();
0399     }
0400     m_firstShow = false;
0401 }
0402 
0403 void ScreenshotDialog::onLineNumChangedClicked(int i)
0404 {
0405     m_showLineNumbers = i != DontShowLineNums;
0406     m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
0407 
0408     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
0409     cg.writeEntry("LineNumbers", i);
0410 
0411     renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
0412 }