KTextEditor

screenshotdialog.cpp
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)
51 , m_screenshot(new QLabel(this))
52 {
55 auto layout = new QHBoxLayout(this);
56 setColor(Qt::yellow);
57
58 layout->addStretch();
59 layout->addWidget(m_screenshot);
60 layout->addStretch();
61
62 m_renableEffects.setInterval(500);
63 m_renableEffects.setSingleShot(true);
64 m_renableEffects.callOnTimeout(this, &BaseWidget::enableDropShadow);
65 }
66
67 void setColor(QColor c)
68 {
69 auto p = palette();
70 p.setColor(QPalette::Base, c);
71 p.setColor(QPalette::Window, c);
72 setPalette(p);
73 }
74
75 void setPixmap(const QPixmap &p)
76 {
77 temporarilyDisableDropShadow();
78
79 m_screenshot->setPixmap(p);
80 m_screenshotSize = p.size();
81 }
82
83 QPixmap grabPixmap()
84 {
85 const int h = m_screenshotSize.height();
86 const int y = std::max(((height() - h) / 2), 0);
87 const int x = m_screenshot->geometry().x();
88 QRect r(x, y, m_screenshotSize.width(), m_screenshotSize.height());
89 r.adjust(-6, -6, 6, 6);
90 return grab(r);
91 }
92
93 void mousePressEvent(QMouseEvent *event) override
94 {
95 QWidget *childAtEvent = childAt(event->pos());
96 if ((childAtEvent != m_screenshot) || (event->buttons() != Qt::LeftButton)) {
97 m_dragStartCandidatePos = noDragStartCandidatePos;
99 return;
100 }
101
102 m_dragStartCandidatePos = event->pos();
103 }
104
105 void mouseMoveEvent(QMouseEvent *event) override
106 {
107 if ((m_dragStartCandidatePos == noDragStartCandidatePos) || (event->buttons() != Qt::LeftButton)
108 || ((event->pos() - m_dragStartCandidatePos).manhattanLength() < QApplication::startDragDistance())) {
110 return;
111 }
112
113 const QPixmap pixmap = grabPixmap();
114
115 auto *mimeData = new QMimeData;
116 mimeData->setImageData(pixmap);
117
118 auto *drag = new QDrag(this);
119 drag->setMimeData(mimeData);
120
121 // 256x256, following size used by spectacle 24.05
122 drag->setPixmap(pixmap.scaled(256, 256, Qt::KeepAspectRatio, Qt::SmoothTransformation));
123
124 drag->exec(Qt::CopyAction);
125 }
126
127 void temporarilyDisableDropShadow()
128 {
129 // Disable drop shadow because on large pixmaps
130 // it is too slow
131 m_screenshot->setGraphicsEffect(nullptr);
132 m_renableEffects.start();
133 }
134
135private:
136 void enableDropShadow()
137 {
140 e->setOffset(2.);
141 e->setBlurRadius(15.);
142 m_screenshot->setGraphicsEffect(e);
143 }
144
145 QLabel *const m_screenshot;
146 QSize m_screenshotSize;
147 QTimer m_renableEffects;
148 QPoint m_dragStartCandidatePos;
149
150 friend class ScrollArea;
151};
152
153class ScrollArea : public QScrollArea
154{
155public:
156 explicit ScrollArea(BaseWidget *contents, QWidget *parent = nullptr)
158 , m_base(contents)
159 {
160 }
161
162private:
163 void scrollContentsBy(int dx, int dy) override
164 {
165 m_base->temporarilyDisableDropShadow();
167 }
168
169private:
170 BaseWidget *const m_base;
171};
172
173ScreenshotDialog::ScreenshotDialog(KTextEditor::Range selRange, KTextEditor::ViewPrivate *parent)
174 : QDialog(parent)
175 , m_base(new BaseWidget(this))
176 , m_selRange(selRange)
177 , m_scrollArea(new ScrollArea(m_base, this))
178 , m_saveButton(new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save")))
179 , m_copyButton(new QPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy")))
180 , m_changeBGColor(new QPushButton(QIcon::fromTheme(QStringLiteral("color-fill")), i18nc("@action:button", "Background Color…")))
181 , m_lineNumButton(new QToolButton(this))
182 , m_extraDecorations(new QCheckBox(i18n("Show Extra Decorations"), this))
183 , m_windowDecorations(new QCheckBox(i18n("Show Window Decorations"), this))
184 , m_lineNumMenu(new QMenu(this))
185 , m_resizeTimer(new QTimer(this))
186{
187 setModal(true);
188 setWindowTitle(i18nc("@title:window", "Take Screenshot of Selection"));
189
190 m_scrollArea->setWidget(m_base);
191 m_scrollArea->setWidgetResizable(true);
192 m_scrollArea->setAutoFillBackground(true);
193 m_scrollArea->setAttribute(Qt::WA_Hover, false);
194 m_scrollArea->setFrameStyle(QFrame::NoFrame);
195
196 auto baseLayout = new QVBoxLayout(this);
197 baseLayout->setContentsMargins(0, 0, 0, 4);
198 baseLayout->addWidget(m_scrollArea);
199
200 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
201 const int color = cg.readEntry("BackgroundColor", EditorPrivate::self()->theme().textColor(KSyntaxHighlighting::Theme::Normal));
202 const auto c = QColor::fromRgba(color);
203 m_base->setColor(c);
204 m_scrollArea->setPalette(m_base->palette());
205
206 auto bottomBar = new QHBoxLayout();
207 baseLayout->addLayout(bottomBar);
208 bottomBar->setContentsMargins(0, 0, 4, 0);
209 bottomBar->addStretch();
210 bottomBar->addWidget(m_windowDecorations);
211 bottomBar->addWidget(m_extraDecorations);
212 bottomBar->addWidget(m_lineNumButton);
213 bottomBar->addWidget(m_changeBGColor);
214 bottomBar->addWidget(m_saveButton);
215 bottomBar->addWidget(m_copyButton);
216 connect(m_saveButton, &QPushButton::clicked, this, &ScreenshotDialog::onSaveClicked);
217 connect(m_copyButton, &QPushButton::clicked, this, &ScreenshotDialog::onCopyClicked);
218 connect(m_changeBGColor, &QPushButton::clicked, this, [this] {
219 QColorDialog dlg(this);
220 int e = dlg.exec();
221 if (e == QDialog::Accepted) {
222 QColor c = dlg.selectedColor();
223 m_base->setColor(c);
224 m_scrollArea->setPalette(m_base->palette());
225
226 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
227 cg.writeEntry("BackgroundColor", c.rgba());
228 }
229 });
230
231 connect(m_extraDecorations, &QCheckBox::toggled, this, [this] {
232 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
233 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
234 cg.writeEntry<bool>("ShowExtraDecorations", m_extraDecorations->isChecked());
235 });
236 m_extraDecorations->setChecked(cg.readEntry<bool>("ShowExtraDecorations", true));
237
238 connect(m_windowDecorations, &QCheckBox::toggled, this, [this] {
239 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
240 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
241 cg.writeEntry<bool>("ShowWindowDecorations", m_windowDecorations->isChecked());
242 });
243 m_windowDecorations->setChecked(cg.readEntry<bool>("ShowWindowDecorations", true));
244
245 {
246 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
247 int i = cg.readEntry("LineNumbers", (int)ShowAbsoluteLineNums);
248
249 auto gp = new QActionGroup(m_lineNumMenu);
250 auto addMenuAction = [this, gp](const QString &text, int data) {
251 auto a = new QAction(text, m_lineNumMenu);
252 a->setCheckable(true);
253 a->setActionGroup(gp);
254 m_lineNumMenu->addAction(a);
255 connect(a, &QAction::triggered, this, [this, data] {
256 onLineNumChangedClicked(data);
257 });
258 return a;
259 };
260 addMenuAction(i18n("Don't Show Line Numbers"), DontShowLineNums)->setChecked(i == DontShowLineNums);
261 addMenuAction(i18n("Show Line Numbers From 1"), ShowAbsoluteLineNums)->setChecked(i == ShowAbsoluteLineNums);
262 addMenuAction(i18n("Show Actual Line Numbers"), ShowActualLineNums)->setChecked(i == ShowActualLineNums);
263
264 m_showLineNumbers = i != DontShowLineNums;
265 m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
266 }
267
268 m_lineNumButton->setText(i18n("Line Numbers"));
269 m_lineNumButton->setPopupMode(QToolButton::InstantPopup);
270 m_lineNumButton->setMenu(m_lineNumMenu);
271
272 m_resizeTimer->setSingleShot(true);
273 m_resizeTimer->setInterval(500);
274 m_resizeTimer->callOnTimeout(this, [this] {
275 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
276 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
277 cg.writeEntry("Geometry", saveGeometry());
278 });
279
280 const QByteArray geometry = cg.readEntry("Geometry", QByteArray());
281 if (!geometry.isEmpty()) {
282 restoreGeometry(geometry);
283 }
284}
285
286ScreenshotDialog::~ScreenshotDialog()
287{
288 m_resizeTimer->stop();
289}
290
291void ScreenshotDialog::renderScreenshot(KateRenderer *r)
292{
293 if (m_selRange.isEmpty()) {
294 return;
295 }
296
297 constexpr int leftMargin = 16;
298 constexpr int rightMargin = 16;
299 constexpr int topMargin = 8;
300 constexpr int bottomMargin = 8;
301 constexpr int lnNoAreaSpacing = 8;
302
303 KateRenderer renderer(r->doc(), r->folding(), r->view());
304 renderer.setPrinterFriendly(!m_extraDecorations->isChecked());
305
306 int startLine = m_selRange.start().line();
307 int endLine = m_selRange.end().line();
308
309 int width = std::min(1024, std::max(400, this->width() - (m_scrollArea->horizontalScrollBar()->height())));
310
311 // If the font is fixed width, try to find the best width
312 const bool fixedWidth = QFontInfo(renderer.currentFont()).fixedPitch();
313 if (fixedWidth) {
314 int maxLineWidth = 0;
315 auto doc = renderer.view()->doc();
316 int w = renderer.currentFontMetrics().averageCharWidth();
317 for (int line = startLine; line <= endLine; ++line) {
318 maxLineWidth = std::max(maxLineWidth, (doc->lineLength(line) * w));
319 }
320
321 const int windowWidth = width;
322 if (maxLineWidth > windowWidth) {
323 maxLineWidth = windowWidth;
324 }
325
326 width = std::min(1024, maxLineWidth);
327 width = std::max(400, width);
328 }
329
330 // Collect line layouts and calculate the needed height
331 const int xEnd = width;
332 int height = 0;
333 std::vector<std::unique_ptr<KateLineLayout>> lineLayouts;
334 for (int line = startLine; line <= endLine; ++line) {
335 auto lineLayout = std::make_unique<KateLineLayout>(renderer);
336 lineLayout->setLine(line, -1);
337 renderer.layoutLine(lineLayout.get(), xEnd, false /* no layout cache */);
338 height += lineLayout->viewLineCount() * renderer.lineHeight();
339 lineLayouts.push_back(std::move(lineLayout));
340 }
341
342 if (m_windowDecorations->isChecked()) {
343 height += renderer.lineHeight() + topMargin + bottomMargin;
344 } else {
345 height += topMargin + bottomMargin; // topmargin
346 }
347
348 int xStart = -leftMargin;
349 int lineNoAreaWidth = 0;
350 if (m_showLineNumbers) {
351 int lastLine = m_absoluteLineNumbers ? (endLine - startLine) + 1 : endLine;
352 const int lnNoWidth = renderer.currentFontMetrics().horizontalAdvance(QString::number(lastLine));
353 lineNoAreaWidth = lnNoWidth + lnNoAreaSpacing;
354 width += lineNoAreaWidth;
355 xStart += -lineNoAreaWidth;
356 }
357
358 width += leftMargin + rightMargin;
359 QPixmap pix(width, height);
360 pix.fill(renderer.view()->rendererConfig()->backgroundColor());
361
362 QPainter paint(&pix);
363
364 paint.translate(0, topMargin);
365
366 if (m_windowDecorations->isChecked()) {
367 int midY = (renderer.lineHeight() + 4) / 2;
368 int x = 24;
369 paint.save();
370 paint.setRenderHint(QPainter::Antialiasing, true);
371 paint.setPen(Qt::NoPen);
372
373 QBrush b(QColor(0xff5f5a)); // red
374 paint.setBrush(b);
375 paint.drawEllipse(QPoint(x, midY), 8, 8);
376
377 x += 24;
378 b = QColor(0xffbe2e);
379 paint.setBrush(b);
380 paint.drawEllipse(QPoint(x, midY), 8, 8);
381
382 x += 24;
383 b = QColor(0x2aca44);
384 paint.setBrush(b);
385 paint.drawEllipse(QPoint(x, midY), 8, 8);
386
387 paint.setRenderHint(QPainter::Antialiasing, false);
388 paint.restore();
389
390 paint.translate(0, renderer.lineHeight() + 4);
391 }
392
396 int lineNo = m_absoluteLineNumbers ? 1 : startLine + 1;
397 paint.setFont(renderer.currentFont());
398 for (auto &lineLayout : lineLayouts) {
399 renderer.paintTextLine(paint, lineLayout.get(), xStart, xEnd, QRectF{}, nullptr, flags);
400 // draw line number
401 if (lineNoAreaWidth != 0) {
402 paint.drawText(QRect(leftMargin - lnNoAreaSpacing, 0, lineNoAreaWidth, renderer.lineHeight()),
404 QString::number(lineNo++));
405 }
406 // translate for next line
407 paint.translate(0, lineLayout->viewLineCount() * renderer.lineHeight());
408 }
409
410 m_base->setPixmap(pix);
411}
412
413void ScreenshotDialog::onSaveClicked()
414{
415 const auto name = QFileDialog::getSaveFileName(this);
416 if (name.isEmpty()) {
417 return;
418 }
419
420 QImageWriter writer(name);
421 writer.write(m_base->grabPixmap().toImage());
422 if (!writer.errorString().isEmpty()) {
423 QMessageBox::warning(this, i18nc("@title:window", "Screenshot saving failed"), i18n("Screenshot saving failed: %1", writer.errorString()));
424 }
425}
426
427void ScreenshotDialog::onCopyClicked()
428{
429 if (auto clip = qApp->clipboard()) {
430 clip->setPixmap(m_base->grabPixmap(), QClipboard::Clipboard);
431 }
432}
433
434void ScreenshotDialog::resizeEvent(QResizeEvent *e)
435{
437 if (!m_firstShow) {
438 m_resizeTimer->start();
439 }
440 m_firstShow = false;
441}
442
443void ScreenshotDialog::onLineNumChangedClicked(int i)
444{
445 m_showLineNumbers = i != DontShowLineNums;
446 m_absoluteLineNumbers = i == ShowAbsoluteLineNums;
447
448 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
449 cg.writeEntry("LineNumbers", i);
450
451 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
452}
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static KTextEditor::EditorPrivate * self()
Kate Part Internal stuff ;)
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
Handles all of the work of rendering the text (used for the views and printing)
Kate::TextFolding & folding() const
Returns the folding info to which this renderer is bound.
KTextEditor::ViewPrivate * view() const
Returns the view to which this renderer is bound.
@ SkipDrawLineSelection
Skip drawing the line selection This is useful when we are drawing the draggable pixmap for drag even...
@ SkipDrawFirstInvisibleLineUnderlined
Skip drawing the dashed underline at the start of a folded block of text?
KTextEditor::DocumentPrivate * doc() const
Returns the document to which this renderer is bound.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString name(StandardAction id)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
QScrollBar * horizontalScrollBar() const const
void triggered(bool checked)
bool isEmpty() const const
QColor fromRgba(QRgb rgba)
QRgb rgba() const const
virtual void resizeEvent(QResizeEvent *) override
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QFlags< T > & setFlag(Enum flag, bool on)
bool fixedPitch() const const
void setBlurRadius(qreal blurRadius)
void setColor(const QColor &color)
void setOffset(const QPointF &ofs)
void setPixmap(const QPixmap &)
void addWidget(QWidget *w)
StandardButton warning(QWidget *parent, const QString &title, const QString &text, StandardButtons buttons, StandardButton defaultButton)
void setImageData(const QVariant &image)
QObject * parent() const const
QPixmap scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QSize size() const const
QImage toImage() const const
virtual void scrollContentsBy(int dx, int dy) override
int height() const const
int width() const const
bool isEmpty() const const
QString number(double n, char format, int precision)
AlignRight
KeepAspectRatio
CopyAction
LeftButton
TextDontClip
SmoothTransformation
WA_Hover
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QMetaObject::Connection callOnTimeout(Functor &&slot)
void setInterval(int msec)
void setSingleShot(bool singleShot)
void start()
void stop()
void setAutoFillBackground(bool enabled)
QWidget * childAt(const QPoint &p) const const
virtual bool event(QEvent *event) override
QPixmap grab(const QRect &rectangle)
QLayout * layout() const const
virtual void mouseMoveEvent(QMouseEvent *event)
virtual void mousePressEvent(QMouseEvent *event)
QWidget * parentWidget() const const
void setContentsMargins(const QMargins &margins)
void setGraphicsEffect(QGraphicsEffect *effect)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 12:01:51 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.