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

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