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 }