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 | |
42 | using namespace KTextEditor; |
43 | |
44 | static constexpr QPoint noDragStartCandidatePos = {-1, -1}; |
45 | |
46 | class BaseWidget : public QWidget |
47 | { |
48 | public: |
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 | |
133 | private: |
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 | |
150 | class ScrollArea : public QScrollArea |
151 | { |
152 | public: |
153 | explicit ScrollArea(BaseWidget *contents, QWidget *parent = nullptr) |
154 | : QScrollArea(parent) |
155 | , m_base(contents) |
156 | { |
157 | } |
158 | |
159 | private: |
160 | void scrollContentsBy(int dx, int dy) override |
161 | { |
162 | m_base->temporarilyDisableDropShadow(); |
163 | QScrollArea::scrollContentsBy(dx, dy); |
164 | } |
165 | |
166 | private: |
167 | BaseWidget *const m_base; |
168 | }; |
169 | |
170 | ScreenshotDialog::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 = [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 | |
283 | ScreenshotDialog::~ScreenshotDialog() |
284 | { |
285 | m_resizeTimer->stop(); |
286 | } |
287 | |
288 | void 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 | |
413 | void 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 | |
426 | void ScreenshotDialog::onCopyClicked() |
427 | { |
428 | if (auto clip = qApp->clipboard()) { |
429 | clip->setPixmap(m_base->grabPixmap(), mode: QClipboard::Clipboard); |
430 | } |
431 | } |
432 | |
433 | void ScreenshotDialog::resizeEvent(QResizeEvent *e) |
434 | { |
435 | QDialog::resizeEvent(e); |
436 | if (!m_firstShow) { |
437 | m_resizeTimer->start(); |
438 | } |
439 | m_firstShow = false; |
440 | } |
441 | |
442 | void 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 | |