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 | |
39 | using namespace KTextEditor; |
40 | |
41 | class BaseWidget : public QWidget |
42 | { |
43 | public: |
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 | |
96 | private: |
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 | |
113 | class ScrollArea : public QScrollArea |
114 | { |
115 | public: |
116 | explicit ScrollArea(BaseWidget *contents, QWidget *parent = nullptr) |
117 | : QScrollArea(parent) |
118 | , m_base(contents) |
119 | { |
120 | } |
121 | |
122 | private: |
123 | void scrollContentsBy(int dx, int dy) override |
124 | { |
125 | m_base->temporarilyDisableDropShadow(); |
126 | QScrollArea::scrollContentsBy(dx, dy); |
127 | } |
128 | |
129 | private: |
130 | BaseWidget *const m_base; |
131 | }; |
132 | |
133 | ScreenshotDialog::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 = [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 | |
246 | ScreenshotDialog::~ScreenshotDialog() |
247 | { |
248 | m_resizeTimer->stop(); |
249 | } |
250 | |
251 | void 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 | |
373 | void 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 | |
387 | void ScreenshotDialog::onCopyClicked() |
388 | { |
389 | if (auto clip = qApp->clipboard()) { |
390 | clip->setPixmap(m_base->grabPixmap(), mode: QClipboard::Clipboard); |
391 | } |
392 | } |
393 | |
394 | void ScreenshotDialog::resizeEvent(QResizeEvent *e) |
395 | { |
396 | QDialog::resizeEvent(e); |
397 | if (!m_firstShow) { |
398 | m_resizeTimer->start(); |
399 | } |
400 | m_firstShow = false; |
401 | } |
402 | |
403 | void 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 | |