1/*
2 SPDX-FileCopyrightText: 2023 Waqar Ahmed <waqar.17a@gmail.com>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5#include "screenshotdialog.h"
6
7#include "katedocument.h"
8#include "kateglobal.h"
9#include "katelinelayout.h"
10#include "katerenderer.h"
11#include "kateview.h"
12
13#include <QActionGroup>
14#include <QApplication>
15#include <QBitmap>
16#include <QCheckBox>
17#include <QClipboard>
18#include <QColorDialog>
19#include <QDebug>
20#include <QFileDialog>
21#include <QGraphicsDropShadowEffect>
22#include <QImageWriter>
23#include <QLabel>
24#include <QMenu>
25#include <QMessageBox>
26#include <QPainter>
27#include <QPainterPath>
28#include <QPushButton>
29#include <QScrollArea>
30#include <QScrollBar>
31#include <QTimer>
32#include <QToolButton>
33#include <QVBoxLayout>
34
35#include <KConfigGroup>
36#include <KLocalizedString>
37#include <KSyntaxHighlighting/Theme>
38
39using namespace KTextEditor;
40
41class BaseWidget : public QWidget
42{
43public:
44 explicit BaseWidget(QWidget *parent = nullptr)
45 : QWidget(parent)
46 , m_screenshot(new QLabel(this))
47 {
48 setAutoFillBackground(true);
49 setContentsMargins({});
50 auto layout = new QHBoxLayout(this);
51 setColor(Qt::yellow);
52
53 layout->addStretch();
54 layout->addWidget(m_screenshot);
55 layout->addStretch();
56
57 m_renableEffects.setInterval(500);
58 m_renableEffects.setSingleShot(true);
59 m_renableEffects.callOnTimeout(args: this, args: &BaseWidget::enableDropShadow);
60 }
61
62 void setColor(QColor c)
63 {
64 auto p = palette();
65 p.setColor(acr: QPalette::Base, acolor: c);
66 p.setColor(acr: QPalette::Window, acolor: c);
67 setPalette(p);
68 }
69
70 void setPixmap(const QPixmap &p)
71 {
72 temporarilyDisableDropShadow();
73
74 m_screenshot->setPixmap(p);
75 m_screenshotSize = p.size();
76 }
77
78 QPixmap grabPixmap()
79 {
80 const int h = m_screenshotSize.height();
81 const int y = std::max(a: ((height() - h) / 2), b: 0);
82 const int x = m_screenshot->geometry().x();
83 QRect r(x, y, m_screenshotSize.width(), m_screenshotSize.height());
84 r.adjust(dx1: -6, dy1: -6, dx2: 6, dy2: 6);
85 return grab(rectangle: r);
86 }
87
88 void temporarilyDisableDropShadow()
89 {
90 // Disable drop shadow because on large pixmaps
91 // it is too slow
92 m_screenshot->setGraphicsEffect(nullptr);
93 m_renableEffects.start();
94 }
95
96private:
97 void enableDropShadow()
98 {
99 QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(m_screenshot);
100 e->setColor(Qt::black);
101 e->setOffset(2.);
102 e->setBlurRadius(15.);
103 m_screenshot->setGraphicsEffect(e);
104 }
105
106 QLabel *const m_screenshot;
107 QSize m_screenshotSize;
108 QTimer m_renableEffects;
109
110 friend class ScrollArea;
111};
112
113class ScrollArea : public QScrollArea
114{
115public:
116 explicit ScrollArea(BaseWidget *contents, QWidget *parent = nullptr)
117 : QScrollArea(parent)
118 , m_base(contents)
119 {
120 }
121
122private:
123 void scrollContentsBy(int dx, int dy) override
124 {
125 m_base->temporarilyDisableDropShadow();
126 QScrollArea::scrollContentsBy(dx, dy);
127 }
128
129private:
130 BaseWidget *const m_base;
131};
132
133ScreenshotDialog::ScreenshotDialog(KTextEditor::Range selRange, KTextEditor::ViewPrivate *parent)
134 : QDialog(parent)
135 , m_base(new BaseWidget(this))
136 , m_selRange(selRange)
137 , m_scrollArea(new ScrollArea(m_base, this))
138 , m_saveButton(new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save")))
139 , m_copyButton(new QPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy")))
140 , m_changeBGColor(new QPushButton(QIcon::fromTheme(QStringLiteral("color-fill")), i18n("Background Color...")))
141 , m_lineNumButton(new QToolButton(this))
142 , m_extraDecorations(new QCheckBox(i18n("Show Extra Decorations"), this))
143 , m_windowDecorations(new QCheckBox(i18n("Show Window Decorations"), this))
144 , m_lineNumMenu(new QMenu(this))
145 , m_resizeTimer(new QTimer(this))
146{
147 setModal(true);
148 setWindowTitle(i18n("Screenshot..."));
149
150 m_scrollArea->setWidget(m_base);
151 m_scrollArea->setWidgetResizable(true);
152 m_scrollArea->setAutoFillBackground(true);
153 m_scrollArea->setAttribute(Qt::WA_Hover, on: false);
154 m_scrollArea->setFrameStyle(QFrame::NoFrame);
155
156 auto baseLayout = new QVBoxLayout(this);
157 baseLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 4);
158 baseLayout->addWidget(m_scrollArea);
159
160 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
161 const int color = cg.readEntry(key: "BackgroundColor", defaultValue: EditorPrivate::self()->theme().textColor(style: KSyntaxHighlighting::Theme::Normal));
162 const auto c = QColor::fromRgba(rgba: color);
163 m_base->setColor(c);
164 m_scrollArea->setPalette(m_base->palette());
165
166 auto bottomBar = new QHBoxLayout();
167 baseLayout->addLayout(layout: bottomBar);
168 bottomBar->setContentsMargins(left: 0, top: 0, right: 4, bottom: 0);
169 bottomBar->addStretch();
170 bottomBar->addWidget(m_windowDecorations);
171 bottomBar->addWidget(m_extraDecorations);
172 bottomBar->addWidget(m_lineNumButton);
173 bottomBar->addWidget(m_changeBGColor);
174 bottomBar->addWidget(m_saveButton);
175 bottomBar->addWidget(m_copyButton);
176 connect(sender: m_saveButton, signal: &QPushButton::clicked, context: this, slot: &ScreenshotDialog::onSaveClicked);
177 connect(sender: m_copyButton, signal: &QPushButton::clicked, context: this, slot: &ScreenshotDialog::onCopyClicked);
178 connect(sender: m_changeBGColor, signal: &QPushButton::clicked, context: this, slot: [this] {
179 QColorDialog dlg(this);
180 int e = dlg.exec();
181 if (e == QDialog::Accepted) {
182 QColor c = dlg.selectedColor();
183 m_base->setColor(c);
184 m_scrollArea->setPalette(m_base->palette());
185
186 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
187 cg.writeEntry(key: "BackgroundColor", value: c.rgba());
188 }
189 });
190
191 connect(sender: m_extraDecorations, signal: &QCheckBox::toggled, context: this, slot: [this] {
192 renderScreenshot(renderer: static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
193 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
194 cg.writeEntry<bool>(key: "ShowExtraDecorations", value: m_extraDecorations->isChecked());
195 });
196 m_extraDecorations->setChecked(cg.readEntry<bool>(key: "ShowExtraDecorations", defaultValue: true));
197
198 connect(sender: m_windowDecorations, signal: &QCheckBox::toggled, context: this, slot: [this] {
199 renderScreenshot(renderer: static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
200 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
201 cg.writeEntry<bool>(key: "ShowWindowDecorations", value: m_windowDecorations->isChecked());
202 });
203 m_windowDecorations->setChecked(cg.readEntry<bool>(key: "ShowWindowDecorations", defaultValue: true));
204
205 {
206 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
207 int i = cg.readEntry(key: "LineNumbers", defaultValue: (int)ShowAbsoluteLineNums);
208
209 auto gp = new QActionGroup(m_lineNumMenu);
210 auto addMenuAction = [this, gp](const QString &text, int data) {
211 auto a = new QAction(text, m_lineNumMenu);
212 a->setCheckable(true);
213 a->setActionGroup(gp);
214 m_lineNumMenu->addAction(action: a);
215 connect(sender: a, signal: &QAction::triggered, context: this, slot: [this, data] {
216 onLineNumChangedClicked(i: data);
217 });
218 return a;
219 };
220 addMenuAction(i18n("Don't Show Line Numbers"), DontShowLineNums)->setChecked(i == DontShowLineNums);
221 addMenuAction(i18n("Show Line Numbers From 1"), ShowAbsoluteLineNums)->setChecked(i == ShowAbsoluteLineNums);
222 addMenuAction(i18n("Show Actual Line Numbers"), ShowActualLineNums)->setChecked(i == ShowActualLineNums);
223
224 m_showLineNumbers = i != DontShowLineNums;
225 m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
226 }
227
228 m_lineNumButton->setText(i18n("Line Numbers"));
229 m_lineNumButton->setPopupMode(QToolButton::InstantPopup);
230 m_lineNumButton->setMenu(m_lineNumMenu);
231
232 m_resizeTimer->setSingleShot(true);
233 m_resizeTimer->setInterval(500);
234 m_resizeTimer->callOnTimeout(args: this, args: [this] {
235 renderScreenshot(renderer: static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
236 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
237 cg.writeEntry(key: "Geometry", value: saveGeometry());
238 });
239
240 const QByteArray geometry = cg.readEntry(key: "Geometry", defaultValue: QByteArray());
241 if (!geometry.isEmpty()) {
242 restoreGeometry(geometry);
243 }
244}
245
246ScreenshotDialog::~ScreenshotDialog()
247{
248 m_resizeTimer->stop();
249}
250
251void ScreenshotDialog::renderScreenshot(KateRenderer *r)
252{
253 if (m_selRange.isEmpty()) {
254 return;
255 }
256
257 constexpr int leftMargin = 16;
258 constexpr int rightMargin = 16;
259 constexpr int topMargin = 8;
260 constexpr int bottomMargin = 8;
261 constexpr int lnNoAreaSpacing = 8;
262
263 KateRenderer renderer(r->doc(), r->folding(), r->view());
264 renderer.setPrinterFriendly(!m_extraDecorations->isChecked());
265
266 int startLine = m_selRange.start().line();
267 int endLine = m_selRange.end().line();
268
269 int width = std::min(a: 1024, b: std::max(a: 400, b: this->width() - (m_scrollArea->horizontalScrollBar()->height())));
270
271 // If the font is fixed width, try to find the best width
272 const bool fixedWidth = QFontInfo(renderer.currentFont()).fixedPitch();
273 if (fixedWidth) {
274 int maxLineWidth = 0;
275 auto doc = renderer.view()->doc();
276 int w = renderer.currentFontMetrics().averageCharWidth();
277 for (int line = startLine; line <= endLine; ++line) {
278 maxLineWidth = std::max(a: maxLineWidth, b: (doc->lineLength(line) * w));
279 }
280
281 const int windowWidth = width;
282 if (maxLineWidth > windowWidth) {
283 maxLineWidth = windowWidth;
284 }
285
286 width = std::min(a: 1024, b: maxLineWidth);
287 width = std::max(a: 400, b: width);
288 }
289
290 // Collect line layouts and calculate the needed height
291 const int xEnd = width;
292 int height = 0;
293 std::vector<std::unique_ptr<KateLineLayout>> lineLayouts;
294 for (int line = startLine; line <= endLine; ++line) {
295 auto lineLayout = std::make_unique<KateLineLayout>(args&: renderer);
296 lineLayout->setLine(line, virtualLine: -1);
297 renderer.layoutLine(line: lineLayout.get(), maxwidth: xEnd, cacheLayout: false /* no layout cache */);
298 height += lineLayout->viewLineCount() * renderer.lineHeight();
299 lineLayouts.push_back(x: std::move(lineLayout));
300 }
301
302 if (m_windowDecorations->isChecked()) {
303 height += renderer.lineHeight() + topMargin + bottomMargin;
304 } else {
305 height += topMargin + bottomMargin; // topmargin
306 }
307
308 int xStart = -leftMargin;
309 int lineNoAreaWidth = 0;
310 if (m_showLineNumbers) {
311 int lastLine = m_absoluteLineNumbers ? (endLine - startLine) + 1 : endLine;
312 const int lnNoWidth = renderer.currentFontMetrics().horizontalAdvance(string: QString::number(lastLine));
313 lineNoAreaWidth = lnNoWidth + lnNoAreaSpacing;
314 width += lineNoAreaWidth;
315 xStart += -lineNoAreaWidth;
316 }
317
318 width += leftMargin + rightMargin;
319 QPixmap pix(width, height);
320 pix.fill(fillColor: renderer.view()->rendererConfig()->backgroundColor());
321
322 QPainter paint(&pix);
323
324 paint.translate(dx: 0, dy: topMargin);
325
326 if (m_windowDecorations->isChecked()) {
327 int midY = (renderer.lineHeight() + 4) / 2;
328 int x = 24;
329 paint.save();
330 paint.setRenderHint(hint: QPainter::Antialiasing, on: true);
331 paint.setPen(Qt::NoPen);
332
333 QBrush b(QColor(0xff5f5a)); // red
334 paint.setBrush(b);
335 paint.drawEllipse(center: QPoint(x, midY), rx: 8, ry: 8);
336
337 x += 24;
338 b = QColor(0xffbe2e);
339 paint.setBrush(b);
340 paint.drawEllipse(center: QPoint(x, midY), rx: 8, ry: 8);
341
342 x += 24;
343 b = QColor(0x2aca44);
344 paint.setBrush(b);
345 paint.drawEllipse(center: QPoint(x, midY), rx: 8, ry: 8);
346
347 paint.setRenderHint(hint: QPainter::Antialiasing, on: false);
348 paint.restore();
349
350 paint.translate(dx: 0, dy: renderer.lineHeight() + 4);
351 }
352
353 KateRenderer::PaintTextLineFlags flags;
354 flags.setFlag(flag: KateRenderer::SkipDrawFirstInvisibleLineUnderlined);
355 flags.setFlag(flag: KateRenderer::SkipDrawLineSelection);
356 int lineNo = m_absoluteLineNumbers ? 1 : startLine + 1;
357 paint.setFont(renderer.currentFont());
358 for (auto &lineLayout : lineLayouts) {
359 renderer.paintTextLine(paint, range: lineLayout.get(), xStart, xEnd, textClipRect: QRectF{}, cursor: nullptr, flags);
360 // draw line number
361 if (lineNoAreaWidth != 0) {
362 paint.drawText(r: QRect(leftMargin - lnNoAreaSpacing, 0, lineNoAreaWidth, renderer.lineHeight()),
363 flags: Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter,
364 text: QString::number(lineNo++));
365 }
366 // translate for next line
367 paint.translate(dx: 0, dy: lineLayout->viewLineCount() * renderer.lineHeight());
368 }
369
370 m_base->setPixmap(pix);
371}
372
373void ScreenshotDialog::onSaveClicked()
374{
375 const auto name = QFileDialog::getSaveFileName(parent: this, i18n("Save..."));
376 if (name.isEmpty()) {
377 return;
378 }
379
380 QImageWriter writer(name);
381 writer.write(image: m_base->grabPixmap().toImage());
382 if (!writer.errorString().isEmpty()) {
383 QMessageBox::warning(parent: this, i18nc("@title:window", "Screenshot saving failed"), i18n("Screenshot saving failed: %1", writer.errorString()));
384 }
385}
386
387void ScreenshotDialog::onCopyClicked()
388{
389 if (auto clip = qApp->clipboard()) {
390 clip->setPixmap(m_base->grabPixmap(), mode: QClipboard::Clipboard);
391 }
392}
393
394void ScreenshotDialog::resizeEvent(QResizeEvent *e)
395{
396 QDialog::resizeEvent(e);
397 if (!m_firstShow) {
398 m_resizeTimer->start();
399 }
400 m_firstShow = false;
401}
402
403void ScreenshotDialog::onLineNumChangedClicked(int i)
404{
405 m_showLineNumbers = i != DontShowLineNums;
406 m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
407
408 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
409 cg.writeEntry(key: "LineNumbers", value: i);
410
411 renderScreenshot(r: static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
412}
413

source code of ktexteditor/src/view/screenshotdialog.cpp