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 <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)
46 , m_screenshot(new QLabel(this))
47 {
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(this, &BaseWidget::enableDropShadow);
60 }
61
62 void setColor(QColor c)
63 {
64 auto p = palette();
65 p.setColor(QPalette::Base, c);
66 p.setColor(QPalette::Window, 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(((height() - h) / 2), 0);
82 const int x = m_screenshot->geometry().x();
83 QRect r(x, y, m_screenshotSize.width(), m_screenshotSize.height());
84 r.adjust(-6, -6, 6, 6);
85 return grab(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 {
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)
118 , m_base(contents)
119 {
120 }
121
122private:
123 void scrollContentsBy(int dx, int dy) override
124 {
125 m_base->temporarilyDisableDropShadow();
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, false);
154 m_scrollArea->setFrameStyle(QFrame::NoFrame);
155
156 auto baseLayout = new QVBoxLayout(this);
157 baseLayout->setContentsMargins(0, 0, 0, 4);
158 baseLayout->addWidget(m_scrollArea);
159
160 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
161 const int color = cg.readEntry("BackgroundColor", EditorPrivate::self()->theme().textColor(KSyntaxHighlighting::Theme::Normal));
162 const auto c = QColor::fromRgba(color);
163 m_base->setColor(c);
164 m_scrollArea->setPalette(m_base->palette());
165
166 auto bottomBar = new QHBoxLayout();
167 baseLayout->addLayout(bottomBar);
168 bottomBar->setContentsMargins(0, 0, 4, 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(m_saveButton, &QPushButton::clicked, this, &ScreenshotDialog::onSaveClicked);
177 connect(m_copyButton, &QPushButton::clicked, this, &ScreenshotDialog::onCopyClicked);
178 connect(m_changeBGColor, &QPushButton::clicked, this, [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("BackgroundColor", c.rgba());
188 }
189 });
190
191 connect(m_extraDecorations, &QCheckBox::toggled, this, [this] {
192 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
193 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
194 cg.writeEntry<bool>("ShowExtraDecorations", m_extraDecorations->isChecked());
195 });
196 m_extraDecorations->setChecked(cg.readEntry<bool>("ShowExtraDecorations", true));
197
198 connect(m_windowDecorations, &QCheckBox::toggled, this, [this] {
199 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
200 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
201 cg.writeEntry<bool>("ShowWindowDecorations", m_windowDecorations->isChecked());
202 });
203 m_windowDecorations->setChecked(cg.readEntry<bool>("ShowWindowDecorations", true));
204
205 {
206 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
207 int i = cg.readEntry("LineNumbers", (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(a);
215 connect(a, &QAction::triggered, this, [this, data] {
216 onLineNumChangedClicked(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(this, [this] {
235 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
236 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("KTextEditor::Screenshot"));
237 cg.writeEntry("Geometry", saveGeometry());
238 });
239
240 const QByteArray geometry = cg.readEntry("Geometry", 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(1024, std::max(400, 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(maxLineWidth, (doc->lineLength(line) * w));
279 }
280
281 const int windowWidth = width;
282 if (maxLineWidth > windowWidth) {
283 maxLineWidth = windowWidth;
284 }
285
286 width = std::min(1024, maxLineWidth);
287 width = std::max(400, 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>(renderer);
296 lineLayout->setLine(line, -1);
297 renderer.layoutLine(lineLayout.get(), xEnd, false /* no layout cache */);
298 height += lineLayout->viewLineCount() * renderer.lineHeight();
299 lineLayouts.push_back(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(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(renderer.view()->rendererConfig()->backgroundColor());
321
322 QPainter paint(&pix);
323
324 paint.translate(0, topMargin);
325
326 if (m_windowDecorations->isChecked()) {
327 int midY = (renderer.lineHeight() + 4) / 2;
328 int x = 24;
329 paint.save();
330 paint.setRenderHint(QPainter::Antialiasing, true);
331 paint.setPen(Qt::NoPen);
332
333 QBrush b(QColor(0xff5f5a)); // red
334 paint.setBrush(b);
335 paint.drawEllipse(QPoint(x, midY), 8, 8);
336
337 x += 24;
338 b = QColor(0xffbe2e);
339 paint.setBrush(b);
340 paint.drawEllipse(QPoint(x, midY), 8, 8);
341
342 x += 24;
343 b = QColor(0x2aca44);
344 paint.setBrush(b);
345 paint.drawEllipse(QPoint(x, midY), 8, 8);
346
347 paint.setRenderHint(QPainter::Antialiasing, false);
348 paint.restore();
349
350 paint.translate(0, renderer.lineHeight() + 4);
351 }
352
356 int lineNo = m_absoluteLineNumbers ? 1 : startLine + 1;
357 paint.setFont(renderer.currentFont());
358 for (auto &lineLayout : lineLayouts) {
359 renderer.paintTextLine(paint, lineLayout.get(), xStart, xEnd, QRectF{}, nullptr, flags);
360 // draw line number
361 if (lineNoAreaWidth != 0) {
362 paint.drawText(QRect(leftMargin - lnNoAreaSpacing, 0, lineNoAreaWidth, renderer.lineHeight()),
364 QString::number(lineNo++));
365 }
366 // translate for next line
367 paint.translate(0, lineLayout->viewLineCount() * renderer.lineHeight());
368 }
369
370 m_base->setPixmap(pix);
371}
372
373void ScreenshotDialog::onSaveClicked()
374{
375 const auto name = QFileDialog::getSaveFileName(this, i18n("Save..."));
376 if (name.isEmpty()) {
377 return;
378 }
379
380 QImageWriter writer(name);
381 writer.write(m_base->grabPixmap().toImage());
382 if (!writer.errorString().isEmpty()) {
383 QMessageBox::warning(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(), QClipboard::Clipboard);
391 }
392}
393
394void ScreenshotDialog::resizeEvent(QResizeEvent *e)
395{
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("LineNumbers", i);
410
411 renderScreenshot(static_cast<KTextEditor::ViewPrivate *>(parentWidget())->renderer());
412}
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(StandardShortcut 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)
QObject * parent() 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
TextDontClip
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)
QPixmap grab(const QRect &rectangle)
QLayout * layout() const const
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 3 2024 11:46:29 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.