1 /*
2 * LXImage-Qt - a simple and fast image viewer
3 * Copyright (C) 2013 PCMan <pcman.tw@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 */
20
21 #include "screenshotdialog.h"
22 #include "screenshotselectarea.h"
23 #include <QTimer>
24 #include <QPixmap>
25 #include <QImage>
26 #include <QPainter>
27
28 #include "application.h"
29 #include <QDesktopWidget>
30 #include <QScreen>
31 #include <QDateTime>
32 #include <QDir>
33 #include <QFile>
34 #include <QFileInfo>
35
36 #include <QX11Info>
37 #include <X11/Xlib.h>
38 #include <X11/Xatom.h>
39 #include <X11/extensions/Xfixes.h>
40 #include <sstream>
41 #include <iostream>
42
43 /*
44 * Used options for Artistic Style to format:
45 --style=google
46 --break-closing-braces
47 --break-one-line-headers
48 --add-braces
49 --break-elseifs
50 --indent=spaces=2
51 --unpad-paren
52 * */
53 using namespace LxImage;
54
hasXFixes()55 static bool hasXFixes() {
56 int event_base, error_base;
57 return XFixesQueryExtension(QX11Info::display(), &event_base, &error_base);
58 }
59
ScreenshotDialog(QWidget * parent,Qt::WindowFlags f)60 ScreenshotDialog::ScreenshotDialog(QWidget* parent, Qt::WindowFlags f): QDialog(parent, f), hasXfixes_(hasXFixes()) {
61 ui.setupUi(this);
62 Application* app = static_cast<Application*>(qApp);
63 app->addWindow();
64 if(!hasXfixes_) {
65 ui.includeCursor->hide();
66 }
67 }
68
~ScreenshotDialog()69 ScreenshotDialog::~ScreenshotDialog() {
70 Application* app = static_cast<Application*>(qApp);
71 app->removeWindow();
72 }
73
done(int r)74 void ScreenshotDialog::done(int r) {
75 if(r == QDialog::Accepted) {
76 hide();
77 QDialog::done(r);
78 XSync(QX11Info::display(), 0); // is this useful?
79
80 int delay = ui.delay->value();
81 if(delay == 0) {
82 // NOTE:
83 // Well, we need to give X and the window manager some time to
84 // really hide our own dialog from the screen.
85 // Nobody knows how long it will take, and there is no reliable
86 // way to ensure that. Let's wait for 400 ms here for it.
87 delay = 400;
88 }
89 else {
90 delay *= 1000;
91 }
92 // the dialog object will be deleted in doScreenshot().
93 QTimer::singleShot(delay, this, SLOT(doScreenshot()));
94 }
95 else {
96 deleteLater();
97 }
98 }
99
windowFrame(WId wid)100 QRect ScreenshotDialog::windowFrame(WId wid) {
101 QRect result;
102 XWindowAttributes wa;
103 if(XGetWindowAttributes(QX11Info::display(), wid, &wa)) {
104 Window child;
105 int x, y;
106 // translate to root coordinate
107 XTranslateCoordinates(QX11Info::display(), wid, wa.root, 0, 0, &x, &y, &child);
108 //qDebug("%d, %d, %d, %d", x, y, wa.width, wa.height);
109 result.setRect(x, y, wa.width, wa.height);
110
111 // get the frame widths added by the window manager
112 Atom atom = XInternAtom(QX11Info::display(), "_NET_FRAME_EXTENTS", false);
113 unsigned long type, resultLen, rest;
114 int format;
115 unsigned char* data = nullptr;
116 if(XGetWindowProperty(QX11Info::display(), wid, atom, 0, G_MAXLONG, false,
117 XA_CARDINAL, &type, &format, &resultLen, &rest, &data) == Success) {
118 }
119 if(data) { // left, right, top, bottom
120 long* offsets = reinterpret_cast<long*>(data);
121 result.setLeft(result.left() - offsets[0]);
122 result.setRight(result.right() + offsets[1]);
123 result.setTop(result.top() - offsets[2]);
124 result.setBottom(result.bottom() + offsets[3]);
125 XFree(data);
126 }
127 }
128 return result;
129 }
130
activeWindowId()131 WId ScreenshotDialog::activeWindowId() {
132 WId root = WId(QX11Info::appRootWindow());
133 Atom atom = XInternAtom(QX11Info::display(), "_NET_ACTIVE_WINDOW", false);
134 unsigned long type, resultLen, rest;
135 int format;
136 WId result = 0;
137 unsigned char* data = nullptr;
138 if(XGetWindowProperty(QX11Info::display(), root, atom, 0, 1, false,
139 XA_WINDOW, &type, &format, &resultLen, &rest, &data) == Success) {
140 result = *reinterpret_cast<long*>(data);
141 XFree(data);
142 }
143 return result;
144 }
145
takeScreenshot(const WId & wid,const QRect & rect,bool takeCursor)146 QImage ScreenshotDialog::takeScreenshot(const WId& wid, const QRect& rect, bool takeCursor) {
147 QImage image;
148 QScreen *screen = QGuiApplication::primaryScreen();
149 if(screen) {
150 QPixmap pixmap = screen->grabWindow(wid, rect.x(), rect.y(), rect.width(), rect.height());
151 image = pixmap.toImage();
152
153 //call to hasXFixes() maybe executed here from cmd line with no gui mode (some day though, currently ignore cursor)
154 if(takeCursor && hasXFixes()) {
155 // capture the cursor if needed
156 XFixesCursorImage* cursor = XFixesGetCursorImage(QX11Info::display());
157 if(cursor) {
158 if(cursor->pixels) { // pixles should be an ARGB array
159 QImage cursorImage;
160 if(sizeof(long) == 4) {
161 // FIXME: will we encounter byte-order problems here?
162 cursorImage = QImage((uchar*)cursor->pixels, cursor->width, cursor->height, QImage::Format_ARGB32);
163 }
164 else { // XFixes returns long integers which is not 32 bit on 64 bit systems.
165 long len = cursor->width * cursor->height;
166 quint32* buf = new quint32[len];
167 for(long i = 0; i < len; ++i) {
168 buf[i] = (quint32)cursor->pixels[i];
169 }
170 cursorImage = QImage((uchar*)buf, cursor->width, cursor->height, QImage::Format_ARGB32, [](void* b) {
171 delete[](quint32*)b;
172 }, buf);
173 }
174 // paint the cursor on the current image
175 QPainter painter(&image);
176 painter.drawImage(cursor->x - cursor->xhot, cursor->y - cursor->yhot, cursorImage);
177 }
178 XFree(cursor);
179 }
180 }
181 }
182 return image;
183 }
184
doScreenshot()185 void ScreenshotDialog::doScreenshot() {
186 WId wid = 0;
187 QRect rect{0, 0, -1, -1};
188
189 wid = QApplication::desktop()->winId(); // get desktop window
190 if(ui.currentWindow->isChecked()) {
191 WId activeWid = activeWindowId();
192 if(activeWid) {
193 if(ui.includeFrame->isChecked()) {
194 rect = windowFrame(activeWid);
195 }
196 else {
197 wid = activeWid;
198 }
199 }
200 }
201
202 //using stored hasXfixes_ so avoid extra call to function later
203 QImage image{takeScreenshot(wid, rect, hasXfixes_ && ui.includeCursor->isChecked())};
204
205 if(ui.screenArea->isChecked() && !image.isNull()) {
206 ScreenshotSelectArea selectArea(image);
207 if(QDialog::Accepted == selectArea.exec()) {
208 image = image.copy(selectArea.selectedArea());
209 }
210 }
211
212 Application* app = static_cast<Application*>(qApp);
213 MainWindow* window = app->createWindow();
214 window->resize(app->settings().windowWidth(), app->settings().windowHeight());
215 if(!image.isNull()) {
216 window->pasteImage(image);
217 }
218 window->show();
219
220 deleteLater(); // destroy ourself
221 }
222
buildNumericFnPart()223 static QString buildNumericFnPart() {
224 //we may have many copies running with no gui, for example user presses hot keys fast
225 //so they must have different file names to save, lets do it time + pid
226 const auto now = QDateTime::currentDateTime().toMSecsSinceEpoch();
227 const auto pid = getpid();
228 return QStringLiteral("%1_%2").arg(now).arg(pid);
229 }
230
getWindowName(WId wid)231 static QString getWindowName(WId wid) {
232 QString result;
233 if(wid) {
234 static const char* atoms[] = {
235 "WM_NAME",
236 "_NET_WM_NAME",
237 "STRING",
238 "UTF8_STRING",
239 };
240
241
242 const auto display = QX11Info::display();
243
244 Atom a = None, type;
245
246
247 for(const auto& c : atoms) {
248 if(None != (a = XInternAtom(display, c, true))) {
249 int form;
250 unsigned long remain, len;
251 unsigned char *list;
252
253 errno = 0;
254 if(XGetWindowProperty(display, wid, a, 0, 1024, False, XA_STRING,
255 &type, &form, &len, &remain, &list) == Success) {
256
257 if(list && *list) {
258
259 std::string dump((const char*)list);
260 std::stringstream ss;
261 for(const auto& sym : dump) {
262 if(std::isalnum(sym)) {
263 ss.put(sym);
264 }
265 }
266 result = QString::fromStdString(ss.str());
267 break;
268 }
269 }
270
271 }
272 }
273 }
274 return (result.isEmpty()) ? QStringLiteral("UKNOWN") : result;
275 }
276
cmdTopShotToDir(QString path)277 void ScreenshotDialog::cmdTopShotToDir(QString path) {
278
279 WId activeWid = activeWindowId();
280 const QRect rect = (activeWid) ? windowFrame(activeWid) : QRect{0, 0, -1, -1};
281 QImage img{takeScreenshot(QApplication::desktop()->winId(), rect, false)};
282
283 QDir d;
284 d.mkpath(path);
285 QFileInfo fi(path);
286 if(!fi.exists() || !fi.isDir() || !fi.isWritable()) {
287 path = QDir::homePath();
288 }
289 const QString filename = QStringLiteral("%1/%2_%3").arg(path).arg(getWindowName(activeWid)).arg(buildNumericFnPart());
290
291 const auto static png = QStringLiteral(".png");
292 QString finalName = filename % png;
293
294 //most unlikelly this will happen ... but user might change system clock or so and we dont want to overwrite file
295 for(int counter = 0; QFile::exists(finalName) && counter < 5000; ++counter) {
296 finalName = QStringLiteral("%1_%2%3").arg(filename).arg(counter).arg(png);
297 }
298 //std::cout << finalName.toStdString() << std::endl;
299 img.save(finalName);
300 }
301