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