1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "scriptableproxy.h"
21 
22 #include "common/action.h"
23 #include "common/appconfig.h"
24 #include "common/command.h"
25 #include "common/commandstatus.h"
26 #include "common/commandstore.h"
27 #include "common/common.h"
28 #include "common/config.h"
29 #include "common/contenttype.h"
30 #include "common/display.h"
31 #include "common/log.h"
32 #include "common/mimetypes.h"
33 #include "common/settings.h"
34 #include "common/sleeptimer.h"
35 #include "common/textdata.h"
36 #include "gui/clipboardbrowser.h"
37 #include "gui/filedialog.h"
38 #include "gui/iconfactory.h"
39 #include "gui/icons.h"
40 #include "gui/mainwindow.h"
41 #include "gui/notification.h"
42 #include "gui/pixelratio.h"
43 #include "gui/screen.h"
44 #include "gui/tabicons.h"
45 #include "gui/traymenu.h"
46 #include "gui/windowgeometryguard.h"
47 #include "item/serialize.h"
48 #include "platform/platformnativeinterface.h"
49 #include "platform/platformwindow.h"
50 
51 #include <QDialog>
52 #include <QHBoxLayout>
53 #include <QVBoxLayout>
54 
55 #include <QApplication>
56 #include <QBuffer>
57 #include <QDataStream>
58 #include <QCheckBox>
59 #include <QComboBox>
60 #include <QCursor>
61 #include <QDateTimeEdit>
62 #include <QDesktopServices>
63 #include <QDialogButtonBox>
64 #include <QFile>
65 #include <QFileInfo>
66 #include <QFileDialog>
67 #include <QWidget>
68 #include <QLabel>
69 #include <QLineEdit>
70 #include <QListWidget>
71 #include <QMetaMethod>
72 #include <QMetaType>
73 #include <QMimeData>
74 #include <QPainter>
75 #include <QPaintEvent>
76 #include <QPen>
77 #include <QPixmap>
78 #include <QPushButton>
79 #include <QScreen>
80 #include <QShortcut>
81 #include <QSpinBox>
82 #include <QStyleFactory>
83 #include <QTextEdit>
84 #include <QUrl>
85 
86 #ifdef HAS_TESTS
87 #   include <QTest>
88 #endif
89 
90 #include <type_traits>
91 
92 namespace {
93 
94 const quint32 serializedFunctionCallMagicNumber = 0x58746908;
95 const quint32 serializedFunctionCallVersion = 2;
96 
registerMetaTypes()97 void registerMetaTypes() {
98     static bool registered = false;
99     if (registered)
100         return;
101 
102     qRegisterMetaType< QPointer<QWidget> >("QPointer<QWidget>");
103     qRegisterMetaTypeStreamOperators<ClipboardMode>("ClipboardMode");
104     qRegisterMetaTypeStreamOperators<Command>("Command");
105     qRegisterMetaTypeStreamOperators<NamedValueList>("NamedValueList");
106     qRegisterMetaTypeStreamOperators<NotificationButtons>("NotificationButtons");
107     qRegisterMetaTypeStreamOperators<ScriptablePath>("ScriptablePath");
108     qRegisterMetaTypeStreamOperators<QVector<int>>("QVector<int>");
109     qRegisterMetaTypeStreamOperators<QVector<Command>>("QVector<Command>");
110     qRegisterMetaTypeStreamOperators<QVector<QVariantMap>>("QVector<QVariantMap>");
111     qRegisterMetaTypeStreamOperators<Qt::KeyboardModifiers>("Qt::KeyboardModifiers");
112 
113     registered = true;
114 }
115 
116 template<typename Predicate>
selectionRemoveIf(QList<QPersistentModelIndex> * indexes,Predicate predicate)117 void selectionRemoveIf(QList<QPersistentModelIndex> *indexes, Predicate predicate)
118 {
119     indexes->erase(
120         std::remove_if(indexes->begin(), indexes->end(), predicate),
121         indexes->end());
122 }
123 
selectionRemoveInvalid(QList<QPersistentModelIndex> * indexes)124 void selectionRemoveInvalid(QList<QPersistentModelIndex> *indexes)
125 {
126     selectionRemoveIf(
127         indexes,
128         [](const QPersistentModelIndex &index){
129             return !index.isValid();
130         });
131 }
132 
133 } // namespace
134 
135 #define BROWSER(tabName, call) \
136     ClipboardBrowser *c = fetchBrowser(tabName); \
137     if (c) \
138         (c->call)
139 
140 #define STR(str) str
141 
142 #define INVOKE_(function, arguments, functionCallId) do { \
143     static const auto f = FunctionCallSerializer(QByteArrayLiteral(STR(#function))).withSlotArguments arguments; \
144     const auto args = f.argumentList arguments; \
145     emit sendMessage(f.serialize(functionCallId, args), CommandFunctionCall); \
146 } while(false)
147 
148 #define INVOKE(FUNCTION, ARGUMENTS) do { \
149     if (!m_wnd) { \
150         using Result = decltype(FUNCTION ARGUMENTS); \
151         const auto functionCallId = ++m_lastFunctionCallId; \
152         INVOKE_(FUNCTION, ARGUMENTS, functionCallId); \
153         const auto result = waitForFunctionCallFinished(functionCallId); \
154         return result.value<Result>(); \
155     } \
156 } while(false)
157 
158 #define INVOKE2(FUNCTION, ARGUMENTS) do { \
159     if (!m_wnd) { \
160         const auto functionCallId = ++m_lastFunctionCallId; \
161         INVOKE_(FUNCTION, ARGUMENTS, functionCallId); \
162         waitForFunctionCallFinished(functionCallId); \
163         return; \
164     } \
165 } while(false)
166 
167 Q_DECLARE_METATYPE(QFile*)
168 
169 QDataStream &operator<<(QDataStream &out, const NotificationButton &button)
170 {
171     out << button.name
172         << button.script
173         << button.data;
174     Q_ASSERT(out.status() == QDataStream::Ok);
175     return out;
176 }
177 
operator >>(QDataStream & in,NotificationButton & button)178 QDataStream &operator>>(QDataStream &in, NotificationButton &button)
179 {
180     in >> button.name
181        >> button.script
182        >> button.data;
183     Q_ASSERT(in.status() == QDataStream::Ok);
184     return in;
185 }
186 
operator <<(QDataStream & out,const NamedValueList & list)187 QDataStream &operator<<(QDataStream &out, const NamedValueList &list)
188 {
189     out << list.size();
190     for (const auto &item : list)
191         out << item.name << item.value;
192     Q_ASSERT(out.status() == QDataStream::Ok);
193     return out;
194 }
195 
operator >>(QDataStream & in,NamedValueList & list)196 QDataStream &operator>>(QDataStream &in, NamedValueList &list)
197 {
198     int size;
199     in >> size;
200     for (int i = 0; i < size; ++i) {
201         NamedValue item;
202         in >> item.name >> item.value;
203         list.append(item);
204     }
205     Q_ASSERT(in.status() == QDataStream::Ok);
206     return in;
207 }
208 
operator <<(QDataStream & out,const Command & command)209 QDataStream &operator<<(QDataStream &out, const Command &command)
210 {
211     out << command.name
212         << command.re
213         << command.wndre
214         << command.matchCmd
215         << command.cmd
216         << command.sep
217         << command.input
218         << command.output
219         << command.wait
220         << command.automatic
221         << command.display
222         << command.inMenu
223         << command.isGlobalShortcut
224         << command.isScript
225         << command.transform
226         << command.remove
227         << command.hideWindow
228         << command.enable
229         << command.icon
230         << command.shortcuts
231         << command.globalShortcuts
232         << command.tab
233         << command.outputTab;
234     Q_ASSERT(out.status() == QDataStream::Ok);
235     return out;
236 }
237 
operator >>(QDataStream & in,Command & command)238 QDataStream &operator>>(QDataStream &in, Command &command)
239 {
240     in >> command.name
241        >> command.re
242        >> command.wndre
243        >> command.matchCmd
244        >> command.cmd
245        >> command.sep
246        >> command.input
247        >> command.output
248        >> command.wait
249        >> command.automatic
250        >> command.display
251        >> command.inMenu
252        >> command.isGlobalShortcut
253        >> command.isScript
254        >> command.transform
255        >> command.remove
256        >> command.hideWindow
257        >> command.enable
258        >> command.icon
259        >> command.shortcuts
260        >> command.globalShortcuts
261        >> command.tab
262        >> command.outputTab;
263     Q_ASSERT(in.status() == QDataStream::Ok);
264     return in;
265 }
266 
operator <<(QDataStream & out,ClipboardMode mode)267 QDataStream &operator<<(QDataStream &out, ClipboardMode mode)
268 {
269     const int modeId = static_cast<int>(mode);
270     out << modeId;
271     Q_ASSERT(out.status() == QDataStream::Ok);
272     return out;
273 }
274 
operator >>(QDataStream & in,ClipboardMode & mode)275 QDataStream &operator>>(QDataStream &in, ClipboardMode &mode)
276 {
277     int modeId;
278     in >> modeId;
279     Q_ASSERT(in.status() == QDataStream::Ok);
280     mode = static_cast<ClipboardMode>(modeId);
281     return in;
282 }
283 
operator <<(QDataStream & out,const ScriptablePath & path)284 QDataStream &operator<<(QDataStream &out, const ScriptablePath &path)
285 {
286     return out << path.path;
287 }
288 
operator >>(QDataStream & in,ScriptablePath & path)289 QDataStream &operator>>(QDataStream &in, ScriptablePath &path)
290 {
291     return in >> path.path;
292 }
293 
operator <<(QDataStream & out,Qt::KeyboardModifiers value)294 QDataStream &operator<<(QDataStream &out, Qt::KeyboardModifiers value)
295 {
296     return out << static_cast<int>(value);
297 }
298 
operator >>(QDataStream & in,Qt::KeyboardModifiers & value)299 QDataStream &operator>>(QDataStream &in, Qt::KeyboardModifiers &value)
300 {
301     int valueInt;
302     in >> valueInt;
303     Q_ASSERT(in.status() == QDataStream::Ok);
304     value = static_cast<Qt::KeyboardModifiers>(valueInt);
305     return in;
306 }
307 
308 namespace {
309 
310 const char propertyWidgetName[] = "CopyQ_widget_name";
311 const char propertyWidgetProperty[] = "CopyQ_widget_property";
312 
313 struct InputDialog {
314     QPointer<QDialog> dialog;
315     QVariant defaultChoice; /// Default text for list widgets.
316 };
317 
318 class FunctionCallSerializer final {
319 public:
FunctionCallSerializer(QByteArray functionName)320     explicit FunctionCallSerializer(QByteArray functionName)
321         : m_slotName(std::move(functionName))
322     {
323     }
324 
325     template<typename ...Ts>
withSlotArguments(Ts...arguments)326     FunctionCallSerializer &withSlotArguments(Ts... arguments)
327     {
328         QByteArray args;
329         for (const auto argType : std::initializer_list<const char *>{ argumentType(arguments)... }) {
330             args.append(argType);
331             args.append(',');
332         }
333         args.chop(1);
334         setSlotArgumentTypes(args);
335         return *this;
336     }
337 
serialize(int functionCallId,const QVector<QVariant> args) const338     QByteArray serialize(int functionCallId, const QVector<QVariant> args) const
339     {
340         QByteArray bytes;
341         QDataStream stream(&bytes, QIODevice::WriteOnly);
342         stream.setVersion(QDataStream::Qt_5_0);
343         stream << serializedFunctionCallMagicNumber << serializedFunctionCallVersion
344                << functionCallId << m_slotName << args;
345         return bytes;
346     }
347 
348     template<typename ...Ts>
argumentList(Ts...arguments)349     static QVector<QVariant> argumentList(Ts... arguments)
350     {
351         return { QVariant::fromValue(arguments)... };
352     }
353 
354     template<typename T>
argumentType(const T &)355     static const char *argumentType(const T &)
356     {
357         if ( std::is_same<QVariant, T>::value )
358             return "QVariant";
359 
360         return QMetaType::typeName(qMetaTypeId<T>());
361     }
362 
363 private:
setSlotArgumentTypes(const QByteArray & args)364     void setSlotArgumentTypes(const QByteArray &args)
365     {
366         m_slotName += "(" + args + ")";
367         const int slotIndex = ScriptableProxy::staticMetaObject.indexOfSlot(m_slotName);
368         if (slotIndex == -1) {
369             log("Failed to find scriptable proxy slot: " + m_slotName, LogError);
370             Q_ASSERT(false);
371         }
372     }
373 
374     QByteArray m_slotName;
375 };
376 
377 class ScreenshotRectWidget final : public QLabel {
378 public:
ScreenshotRectWidget(const QPixmap & pixmap)379     explicit ScreenshotRectWidget(const QPixmap &pixmap)
380     {
381         setWindowFlags(Qt::Widget | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
382         setCursor(Qt::CrossCursor);
383         setPixmap(pixmap);
384     }
385 
paintEvent(QPaintEvent * ev)386     void paintEvent(QPaintEvent *ev) override
387     {
388         QLabel::paintEvent(ev);
389         if (selectionRect.isValid()) {
390             QPainter p(this);
391             const auto w = pointsToPixels(1);
392 
393             p.setPen(QPen(Qt::white, w));
394             p.drawRect(selectionRect);
395 
396             p.setPen(QPen(Qt::black, w));
397             p.drawRect(selectionRect.adjusted(-w, -w, w, w));
398         }
399     }
400 
keyPressEvent(QKeyEvent * ev)401     void keyPressEvent(QKeyEvent *ev) override
402     {
403         QWidget::keyPressEvent(ev);
404         hide();
405     }
406 
mousePressEvent(QMouseEvent * ev)407     void mousePressEvent(QMouseEvent *ev) override
408     {
409         if ( ev->button() == Qt::LeftButton ) {
410             m_pos = ev->pos();
411             selectionRect.setTopLeft(m_pos);
412             update();
413         } else {
414             hide();
415         }
416     }
417 
mouseReleaseEvent(QMouseEvent *)418     void mouseReleaseEvent(QMouseEvent *) override
419     {
420         hide();
421     }
422 
mouseMoveEvent(QMouseEvent * ev)423     void mouseMoveEvent(QMouseEvent *ev) override
424     {
425         if ( !ev->buttons().testFlag(Qt::LeftButton) )
426             return;
427 
428         const auto pos = ev->pos();
429         // Types need to be explicitly specified because minmax() returns pair of references.
430         const std::pair<int,int> x = std::minmax(pos.x(), m_pos.x());
431         const std::pair<int,int> y = std::minmax(pos.y(), m_pos.y());
432         selectionRect = QRect( QPoint(x.first, y.first), QPoint(x.second, y.second) );
433         update();
434     }
435 
436     QRect selectionRect;
437 
438 private:
439     QPoint m_pos;
440 };
441 
442 /// Load icon from icon font, path or theme.
loadIcon(const QString & idPathOrName)443 QIcon loadIcon(const QString &idPathOrName)
444 {
445     if (idPathOrName.size() == 1)
446         return createPixmap(idPathOrName[0].unicode(), Qt::white, 64);
447 
448     if ( QFile::exists(idPathOrName) )
449         return QIcon(idPathOrName);
450 
451     return QIcon::fromTheme(idPathOrName);
452 }
453 
label(Qt::Orientation orientation,const QString & name,QWidget * w)454 QWidget *label(Qt::Orientation orientation, const QString &name, QWidget *w)
455 {
456     QWidget *parent = w->parentWidget();
457 
458     if ( !name.isEmpty() ) {
459         QBoxLayout *layout;
460         if (orientation == Qt::Horizontal)
461             layout = new QHBoxLayout;
462         else
463             layout = new QVBoxLayout;
464 
465         parent->layout()->addItem(layout);
466 
467         QLabel *label = new QLabel(name + ":", parent);
468         label->setBuddy(w);
469         layout->addWidget(label);
470         layout->addWidget(w, 1);
471     }
472 
473     w->setProperty(propertyWidgetName, name);
474 
475     return w;
476 }
477 
label(const QString & name,QWidget * w)478 QWidget *label(const QString &name, QWidget *w)
479 {
480     w->setProperty("text", name);
481     w->setProperty(propertyWidgetName, name);
482     return w;
483 }
484 
485 template <typename Widget>
createAndSetWidget(const char * propertyName,const QVariant & value,QWidget * parent)486 Widget *createAndSetWidget(const char *propertyName, const QVariant &value, QWidget *parent)
487 {
488     auto w = new Widget(parent);
489     w->setProperty(propertyName, value);
490     w->setProperty(propertyWidgetProperty, propertyName);
491     parent->layout()->addWidget(w);
492     return w;
493 }
494 
createDateTimeEdit(const QString & name,const char * propertyName,const QVariant & value,QWidget * parent)495 QWidget *createDateTimeEdit(
496         const QString &name, const char *propertyName, const QVariant &value, QWidget *parent)
497 {
498     QDateTimeEdit *w = createAndSetWidget<QDateTimeEdit>(propertyName, value, parent);
499     w->setCalendarPopup(true);
500     return label(Qt::Horizontal, name, w);
501 }
502 
installShortcutToCloseDialog(QDialog * dialog,QWidget * shortcutParent,int shortcut)503 void installShortcutToCloseDialog(QDialog *dialog, QWidget *shortcutParent, int shortcut)
504 {
505     QShortcut *s = new QShortcut(QKeySequence(shortcut), shortcutParent);
506     QObject::connect(s, &QShortcut::activated, dialog, &QDialog::accept);
507     QObject::connect(s, &QShortcut::activatedAmbiguously, dialog, &QDialog::accept);
508 }
509 
createListWidget(const QString & name,const QStringList & items,InputDialog * inputDialog)510 QWidget *createListWidget(const QString &name, const QStringList &items, InputDialog *inputDialog)
511 {
512     QDialog *parent = inputDialog->dialog;
513 
514     const QString currentText = inputDialog->defaultChoice.isValid()
515             ? inputDialog->defaultChoice.toString()
516             : items.value(0);
517 
518     const QLatin1String listPrefix(".list:");
519     if ( name.startsWith(listPrefix) ) {
520         QListWidget *w = createAndSetWidget<QListWidget>("currentRow", QVariant(), parent);
521         w->addItems(items);
522         const int i = items.indexOf(currentText);
523         if (i != -1)
524             w->setCurrentRow(i);
525         w->setAlternatingRowColors(true);
526         installShortcutToCloseDialog(parent, w, Qt::Key_Enter);
527         installShortcutToCloseDialog(parent, w, Qt::Key_Return);
528         return label(Qt::Vertical, name.mid(listPrefix.size()), w);
529     }
530 
531     QComboBox *w = createAndSetWidget<QComboBox>("currentText", QVariant(), parent);
532     w->setEditable(true);
533     w->addItems(items);
534     w->setCurrentIndex(items.indexOf(currentText));
535     w->lineEdit()->setText(currentText);
536     w->lineEdit()->selectAll();
537     w->setMaximumWidth( pointsToPixels(400) );
538     installShortcutToCloseDialog(parent, w, Qt::Key_Enter);
539     installShortcutToCloseDialog(parent, w, Qt::Key_Return);
540 
541     const QLatin1String comboPrefix(".combo:");
542     if ( name.startsWith(comboPrefix) ) {
543         w->setEditable(false);
544         return label(Qt::Horizontal, name.mid(comboPrefix.size()), w);
545     }
546 
547     return label(Qt::Horizontal, name, w);
548 }
549 
createSpinBox(const QString & name,const QVariant & value,QWidget * parent)550 QWidget *createSpinBox(const QString &name, const QVariant &value, QWidget *parent)
551 {
552     QSpinBox *w = createAndSetWidget<QSpinBox>("value", value, parent);
553     w->setRange(-1e9, 1e9);
554     return label(Qt::Horizontal, name, w);
555 }
556 
createLineEdit(const QVariant & value,QWidget * parent)557 QLineEdit *createLineEdit(const QVariant &value, QWidget *parent)
558 {
559     QLineEdit *lineEdit = createAndSetWidget<QLineEdit>("text", value, parent);
560     lineEdit->selectAll();
561     return lineEdit;
562 }
563 
createFileNameEdit(const QString & name,const QString & path,QWidget * parent)564 QWidget *createFileNameEdit(const QString &name, const QString &path, QWidget *parent)
565 {
566     QWidget *w = new QWidget(parent);
567     parent->layout()->addWidget(w);
568 
569     auto layout = new QHBoxLayout(w);
570     layout->setContentsMargins(0, 0, 0, 0);
571 
572     QLineEdit *lineEdit = createLineEdit(path, w);
573     lineEdit->setProperty(propertyWidgetName, name);
574 
575     QPushButton *browseButton = new QPushButton("...");
576 
577     FileDialog *dialog = new FileDialog(w, name, path);
578     QObject::connect( browseButton, &QAbstractButton::clicked,
579                       dialog, &FileDialog::exec );
580     QObject::connect( dialog, &FileDialog::fileSelected,
581                       lineEdit, &QLineEdit::setText );
582 
583     layout->addWidget(lineEdit);
584     layout->addWidget(browseButton);
585 
586     label(Qt::Vertical, name, w);
587 
588     return lineEdit;
589 }
590 
createTextEdit(const QString & name,const QVariant & value,QWidget * parent)591 QWidget *createTextEdit(const QString &name, const QVariant &value, QWidget *parent)
592 {
593     QTextEdit *w = createAndSetWidget<QTextEdit>("plainText", value, parent);
594     w->setTabChangesFocus(true);
595     return label(Qt::Vertical, name, w);
596 }
597 
createWidget(const QString & name,const QVariant & value,InputDialog * inputDialog)598 QWidget *createWidget(const QString &name, const QVariant &value, InputDialog *inputDialog)
599 {
600     QDialog *parent = inputDialog->dialog;
601 
602     switch ( value.type() ) {
603     case QVariant::Bool:
604         return label(name, createAndSetWidget<QCheckBox>("checked", value, parent));
605     case QVariant::Int:
606         return createSpinBox("value", value, parent);
607     case QVariant::Date:
608         return createDateTimeEdit(name, "date", value, parent);
609     case QVariant::Time:
610         return createDateTimeEdit(name, "time", value, parent);
611     case QVariant::DateTime:
612         return createDateTimeEdit(name, "dateTime", value, parent);
613     case QVariant::List:
614     case QVariant::StringList:
615         return createListWidget(name, value.toStringList(), inputDialog);
616     default:
617         if ( value.userType() == qMetaTypeId<ScriptablePath>() ) {
618             const auto path = value.value<ScriptablePath>();
619             return createFileNameEdit(name, path.path, parent);
620         }
621 
622         const QString text = value.toString();
623         if (text.contains('\n'))
624             return createTextEdit(name, value.toStringList(), parent);
625 
626         return label(Qt::Horizontal, name, createLineEdit(value, parent));
627     }
628 }
629 
setGeometryWithoutSave(QWidget * window,QRect geometry)630 void setGeometryWithoutSave(QWidget *window, QRect geometry)
631 {
632     setGeometryGuardBlockedUntilHidden(window, true);
633 
634     const auto pos = (geometry.x() == -1 && geometry.y() == -1)
635             ? QCursor::pos()
636             : geometry.topLeft();
637 
638     const int w = pointsToPixels(geometry.width());
639     const int h = pointsToPixels(geometry.height());
640     if (w > 0 && h > 0)
641         window->resize(w, h);
642 
643     moveWindowOnScreen(window, pos);
644 }
645 
tabNotFoundError()646 QString tabNotFoundError()
647 {
648     return ScriptableProxy::tr("Tab with given name doesn't exist!");
649 }
650 
tabNameEmptyError()651 QString tabNameEmptyError()
652 {
653     return ScriptableProxy::tr("Tab name cannot be empty!");
654 }
655 
raiseWindow(QPointer<QWidget> window)656 void raiseWindow(QPointer<QWidget> window)
657 {
658     window->raise();
659     window->activateWindow();
660     QApplication::setActiveWindow(window);
661     QApplication::processEvents();
662     const auto wid = window->winId();
663     const auto platformWindow = platformNativeInterface()->getWindow(wid);
664     if (platformWindow)
665         platformWindow->raise();
666 }
667 
668 } // namespace
669 
670 #ifdef HAS_TESTS
671 class KeyClicker final : public QObject {
672 public:
KeyClicker(MainWindow * wnd,QObject * parent)673     KeyClicker(MainWindow *wnd, QObject *parent)
674         : QObject(parent)
675         , m_wnd(wnd)
676     {
677     }
678 
keyClicksRetry(const QString & expectedWidgetName,const QString & keys,int delay,int retry)679     void keyClicksRetry(const QString &expectedWidgetName, const QString &keys, int delay, int retry)
680     {
681         if (retry > 0)
682             sendKeyClicks(expectedWidgetName, keys, delay + 100, retry - 1);
683         else
684             keyClicksFailed(expectedWidgetName);
685     }
686 
keyClicksFailed(const QString & expectedWidgetName)687     void keyClicksFailed(const QString &expectedWidgetName)
688     {
689         auto actual = keyClicksTarget();
690         auto popup = QApplication::activePopupWidget();
691         auto widget = QApplication::focusWidget();
692         auto window = QApplication::activeWindow();
693         auto modal = QApplication::activeModalWidget();
694         const auto currentWindow = platformNativeInterface()->getCurrentWindow();
695         const auto currentWindowTitle = currentWindow ? currentWindow->getTitle() : QString();
696         log( QString("Failed to send key press to target widget")
697             + "\nExpected: " + (expectedWidgetName.isEmpty() ? "Any" : expectedWidgetName)
698             + "\nActual:   " + keyClicksTargetDescription(actual)
699             + "\nPopup:    " + keyClicksTargetDescription(popup)
700             + "\nWidget:   " + keyClicksTargetDescription(widget)
701             + "\nWindow:   " + keyClicksTargetDescription(window)
702             + "\nModal:    " + keyClicksTargetDescription(modal)
703             + "\nTitle:    " + currentWindowTitle
704             , LogError );
705 
706         m_failed = true;
707     }
708 
keyClicks(const QString & expectedWidgetName,const QString & keys,int delay,int retry)709     void keyClicks(const QString &expectedWidgetName, const QString &keys, int delay, int retry)
710     {
711         auto widget = keyClicksTarget();
712         if (!widget) {
713             keyClicksRetry(expectedWidgetName, keys, delay, retry);
714             return;
715         }
716 
717         auto widgetName = keyClicksTargetDescription(widget);
718         if ( !expectedWidgetName.isEmpty() && !widgetName.contains(expectedWidgetName) ) {
719             keyClicksRetry(expectedWidgetName, keys, delay, retry);
720             return;
721         }
722 
723         // Only verified focused widget.
724         if ( keys.isEmpty() ) {
725             m_succeeded = true;
726             return;
727         }
728 
729         // There could be some animation/transition effect on check boxes
730         // so wait for checkbox to be set.
731         if ( qobject_cast<QCheckBox*>(widget) )
732             waitFor(100);
733 
734         COPYQ_LOG( QString("Sending keys \"%1\" to %2.")
735                    .arg(keys, widgetName) );
736 
737         const auto popupMessage = QString::fromLatin1("%1 (%2)")
738                 .arg( quoteString(keys), widgetName );
739         auto notification = m_wnd->createNotification(QLatin1String("tests"));
740         notification->setMessage(popupMessage);
741         notification->setIcon(IconKeyboard);
742         notification->setInterval(2000);
743 
744         if ( keys.startsWith(":") ) {
745             const auto text = keys.mid(1);
746 
747             QTest::keyClicks(widget, text, Qt::NoModifier, 0);
748 
749             // Increment key clicks sequence number after typing all the text.
750             m_succeeded = true;
751         } else {
752             const QKeySequence shortcut(keys, QKeySequence::PortableText);
753 
754             if ( shortcut.isEmpty() ) {
755                 log( QString("Cannot parse shortcut \"%1\"!").arg(keys), LogError );
756                 m_failed = true;
757                 return;
758             }
759 
760             // Increment key clicks sequence number before opening any modal dialogs.
761             m_succeeded = true;
762 
763             const auto key = static_cast<uint>(shortcut[0]);
764             QTest::keyClick( widget,
765                              Qt::Key(key & ~Qt::KeyboardModifierMask),
766                              Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask),
767                              0 );
768         }
769 
770         COPYQ_LOG( QString("Key \"%1\" sent to %2.")
771                    .arg(keys, widgetName) );
772     }
773 
sendKeyClicks(const QString & expectedWidgetName,const QString & keys,int delay,int retry)774     void sendKeyClicks(const QString &expectedWidgetName, const QString &keys, int delay, int retry)
775     {
776         m_succeeded = false;
777         m_failed = false;
778 
779         // Don't stop when modal window is open.
780         auto t = new QTimer(m_wnd);
781         t->setSingleShot(true);
782         QObject::connect( t, &QTimer::timeout, this, [=]() {
783             keyClicks(expectedWidgetName, keys, delay, retry);
784             t->deleteLater();
785         });
786         t->start(delay);
787     }
788 
succeeded() const789     bool succeeded() const { return m_succeeded; }
failed() const790     bool failed() const { return m_failed; }
791 
792 private:
keyClicksTargetDescription(QWidget * widget)793     static QString keyClicksTargetDescription(QWidget *widget)
794     {
795         if (widget == nullptr)
796             return "None";
797 
798         const auto className = widget->metaObject()->className();
799 
800         auto widgetName = QString::fromLatin1("%1:%2")
801                 .arg(widget->objectName(), className);
802 
803         const auto window = widget->window();
804         if (window && widget != window) {
805             widgetName.append(
806                 QString::fromLatin1(" in %1:%2")
807                     .arg(window->objectName(), window->metaObject()->className())
808             );
809         }
810 
811         auto parent = widget->parentWidget();
812         while (parent) {
813             if ( parent != window && !parent->objectName().startsWith("qt_") ) {
814                 widgetName.append(
815                     QString::fromLatin1(" in %1:%2")
816                         .arg(parent->objectName(), parent->metaObject()->className())
817                 );
818             }
819             parent = parent->parentWidget();
820         }
821 
822         return widgetName;
823     }
824 
keyClicksTarget()825     QWidget *keyClicksTarget()
826     {
827         auto popup = QApplication::activePopupWidget();
828         if (popup)
829             return popup;
830 
831         auto widget = QApplication::focusWidget();
832         if (widget)
833             return widget;
834 
835         auto window = QApplication::activeWindow();
836         if (window)
837             return window->focusWidget();
838 
839         auto modal = QApplication::activeModalWidget();
840         if (modal)
841             return modal->focusWidget();
842 
843 #ifdef Q_OS_MAC
844         return m_wnd->focusWidget();
845 #else
846         return nullptr;
847 #endif
848     }
849 
850 
851     MainWindow *m_wnd = nullptr;
852     bool m_succeeded = true;
853     bool m_failed = false;
854 };
855 #endif // HAS_TESTS
856 
ScriptableProxy(MainWindow * mainWindow,QObject * parent)857 ScriptableProxy::ScriptableProxy(MainWindow *mainWindow, QObject *parent)
858     : QObject(parent)
859     , m_wnd(mainWindow)
860 {
861     connect( this, &ScriptableProxy::clientDisconnected,
862              this, [this]() {
863                  m_disconnected = true;
864                  emit abortEvaluation();
865              } );
866     registerMetaTypes();
867 }
868 
callFunction(const QByteArray & serializedFunctionCall)869 void ScriptableProxy::callFunction(const QByteArray &serializedFunctionCall)
870 {
871     if (m_shouldBeDeleted)
872         return;
873 
874     ++m_functionCallStack;
875     auto t = new QTimer(this);
876     t->setSingleShot(true);
877     QObject::connect( t, &QTimer::timeout, this, [=]() {
878         const auto result = callFunctionHelper(serializedFunctionCall);
879         emit sendMessage(result, CommandFunctionCallReturnValue);
880         t->deleteLater();
881 
882         --m_functionCallStack;
883         if (m_shouldBeDeleted && m_functionCallStack == 0)
884             deleteLater();
885     });
886     t->start(0);
887 }
888 
callFunctionHelper(const QByteArray & serializedFunctionCall)889 QByteArray ScriptableProxy::callFunctionHelper(const QByteArray &serializedFunctionCall)
890 {
891     QVector<QVariant> arguments;
892     QByteArray slotName;
893     int functionCallId;
894     {
895         QDataStream stream(serializedFunctionCall);
896         stream.setVersion(QDataStream::Qt_5_0);
897 
898         quint32 magicNumber;
899         quint32 version;
900         stream >> magicNumber >> version;
901         if (stream.status() != QDataStream::Ok) {
902             log("Failed to read scriptable proxy slot call preamble", LogError);
903             Q_ASSERT(false);
904             return QByteArray();
905         }
906 
907         if (magicNumber != serializedFunctionCallMagicNumber) {
908             log("Unexpected scriptable proxy slot call preamble magic number", LogError);
909             Q_ASSERT(false);
910             return QByteArray();
911         }
912 
913         if (version != serializedFunctionCallVersion) {
914             log("Unexpected scriptable proxy slot call preamble version", LogError);
915             Q_ASSERT(false);
916             return QByteArray();
917         }
918 
919         stream >> functionCallId;
920         if (stream.status() != QDataStream::Ok) {
921             log("Failed to read scriptable proxy slot call ID", LogError);
922             Q_ASSERT(false);
923             return QByteArray();
924         }
925 
926         stream >> slotName;
927         if (stream.status() != QDataStream::Ok) {
928             log("Failed to read scriptable proxy slot call name", LogError);
929             Q_ASSERT(false);
930             return QByteArray();
931         }
932 
933         stream >> arguments;
934         if (stream.status() != QDataStream::Ok) {
935             log("Failed to read scriptable proxy slot call", LogError);
936             Q_ASSERT(false);
937             return QByteArray();
938         }
939     }
940 
941     const auto slotIndex = metaObject()->indexOfSlot(slotName);
942     if (slotIndex == -1) {
943         log("Failed to find scriptable proxy slot: " + slotName, LogError);
944         Q_ASSERT(false);
945         return QByteArray();
946     }
947 
948     const auto metaMethod = metaObject()->method(slotIndex);
949     const auto typeId = metaMethod.returnType();
950 
951     QGenericArgument args[9];
952     for (int i = 0; i < arguments.size(); ++i) {
953         auto &value = arguments[i];
954         const int argumentTypeId = metaMethod.parameterType(i);
955         if (argumentTypeId == QMetaType::QVariant) {
956             args[i] = Q_ARG(QVariant, value);
957         } else if ( value.userType() == argumentTypeId ) {
958             args[i] = QGenericArgument( value.typeName(), static_cast<void*>(value.data()) );
959         } else {
960             log( QString("Bad argument type (at index %1) for scriptable proxy slot: %2")
961                  .arg(i)
962                  .arg(metaMethod.methodSignature().constData()), LogError);
963             Q_ASSERT(false);
964             return QByteArray();
965         }
966     }
967 
968     QVariant returnValue;
969     bool called;
970 
971     if (typeId == QMetaType::Void) {
972         called = metaMethod.invoke(
973                 this, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
974     } else {
975         returnValue = QVariant(typeId, nullptr);
976         const auto genericReturnValue = returnValue.isValid()
977                 ? QGenericReturnArgument(returnValue.typeName(), static_cast<void*>(returnValue.data()) )
978                 : Q_RETURN_ARG(QVariant, returnValue);
979 
980         called = metaMethod.invoke(
981                 this, genericReturnValue,
982                 args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
983     }
984 
985     if (!called) {
986         log( QString("Bad scriptable proxy slot call: %1")
987              .arg(metaMethod.methodSignature().constData()), LogError);
988         Q_ASSERT(false);
989     }
990 
991     QByteArray bytes;
992     {
993         QDataStream stream(&bytes, QIODevice::WriteOnly);
994         stream << functionCallId << returnValue;
995     }
996 
997     return bytes;
998 }
999 
setFunctionCallReturnValue(const QByteArray & bytes)1000 void ScriptableProxy::setFunctionCallReturnValue(const QByteArray &bytes)
1001 {
1002     QDataStream stream(bytes);
1003     int functionCallId;
1004     QVariant returnValue;
1005     stream >> functionCallId >> returnValue;
1006     if (stream.status() != QDataStream::Ok) {
1007         log("Failed to read scriptable proxy slot call return value", LogError);
1008         Q_ASSERT(false);
1009         return;
1010     }
1011     emit functionCallFinished(functionCallId, returnValue);
1012 }
1013 
setInputDialogResult(const QByteArray & bytes)1014 void ScriptableProxy::setInputDialogResult(const QByteArray &bytes)
1015 {
1016     QDataStream stream(bytes);
1017     int dialogId;
1018     NamedValueList result;
1019     stream >> dialogId >> result;
1020     if (stream.status() != QDataStream::Ok) {
1021         log("Failed to read input dialog result", LogError);
1022         Q_ASSERT(false);
1023         return;
1024     }
1025     emit inputDialogFinished(dialogId, result);
1026 }
1027 
safeDeleteLater()1028 void ScriptableProxy::safeDeleteLater()
1029 {
1030     m_shouldBeDeleted = true;
1031     if (m_functionCallStack == 0)
1032         deleteLater();
1033 }
1034 
getActionData(int id)1035 QVariantMap ScriptableProxy::getActionData(int id)
1036 {
1037     INVOKE(getActionData, (id));
1038     m_actionData = m_wnd->actionData(id);
1039     m_actionId = id;
1040 
1041     auto data = m_actionData;
1042     data.remove(mimeSelectedItems);
1043     data.remove(mimeCurrentItem);
1044     return data;
1045 }
1046 
setActionData(int id,const QVariantMap & data)1047 void ScriptableProxy::setActionData(int id, const QVariantMap &data)
1048 {
1049     INVOKE2(setActionData, (id, data));
1050     m_wnd->setActionData(id, data);
1051 }
1052 
exit()1053 void ScriptableProxy::exit()
1054 {
1055     INVOKE2(exit, ());
1056     qApp->quit();
1057 }
1058 
close()1059 void ScriptableProxy::close()
1060 {
1061     INVOKE2(close, ());
1062     m_wnd->close();
1063 }
1064 
focusPrevious()1065 bool ScriptableProxy::focusPrevious()
1066 {
1067     INVOKE(focusPrevious, ());
1068     return m_wnd->focusPrevious();
1069 }
1070 
showWindow()1071 bool ScriptableProxy::showWindow()
1072 {
1073     INVOKE(showWindow, ());
1074     m_wnd->showWindow();
1075     return m_wnd->isVisible();
1076 }
1077 
showWindowAt(QRect rect)1078 bool ScriptableProxy::showWindowAt(QRect rect)
1079 {
1080     INVOKE(showWindowAt, (rect));
1081     setGeometryWithoutSave(m_wnd, rect);
1082     return showWindow();
1083 }
1084 
pasteToCurrentWindow()1085 bool ScriptableProxy::pasteToCurrentWindow()
1086 {
1087     INVOKE(pasteToCurrentWindow, ());
1088 
1089     PlatformWindowPtr window = platformNativeInterface()->getCurrentWindow();
1090     if (!window)
1091         return false;
1092     window->pasteClipboard();
1093     return true;
1094 }
1095 
copyFromCurrentWindow()1096 bool ScriptableProxy::copyFromCurrentWindow()
1097 {
1098     INVOKE(copyFromCurrentWindow, ());
1099 
1100     PlatformWindowPtr window = platformNativeInterface()->getCurrentWindow();
1101     if (!window)
1102         return false;
1103     window->copy();
1104     return true;
1105 }
1106 
isMonitoringEnabled()1107 bool ScriptableProxy::isMonitoringEnabled()
1108 {
1109     INVOKE(isMonitoringEnabled, ());
1110     return m_wnd->isMonitoringEnabled();
1111 }
1112 
isMainWindowVisible()1113 bool ScriptableProxy::isMainWindowVisible()
1114 {
1115     INVOKE(isMainWindowVisible, ());
1116     return !m_wnd->isMinimized() && m_wnd->isVisible();
1117 }
1118 
isMainWindowFocused()1119 bool ScriptableProxy::isMainWindowFocused()
1120 {
1121     INVOKE(isMainWindowFocused, ());
1122     return m_wnd->isActiveWindow();
1123 }
1124 
preview(const QVariant & arg)1125 bool ScriptableProxy::preview(const QVariant &arg)
1126 {
1127     INVOKE(preview, (arg));
1128 
1129     const bool wasVisible = m_wnd->isItemPreviewVisible();
1130 
1131     if ( arg.isValid() ) {
1132         const bool enable =
1133                 arg.canConvert<bool>() ? arg.toBool()
1134               : arg.canConvert<int>() ? arg.toInt() != 0
1135               : arg.toString() == QLatin1String("true");
1136         m_wnd->setItemPreviewVisible(enable);
1137     }
1138 
1139     return wasVisible;
1140 }
1141 
disableMonitoring(bool arg1)1142 void ScriptableProxy::disableMonitoring(bool arg1)
1143 {
1144     INVOKE2(disableMonitoring, (arg1));
1145     m_wnd->disableClipboardStoring(arg1);
1146 }
1147 
setClipboard(const QVariantMap & data,ClipboardMode mode)1148 void ScriptableProxy::setClipboard(const QVariantMap &data, ClipboardMode mode)
1149 {
1150     INVOKE2(setClipboard, (data, mode));
1151     m_wnd->setClipboard(data, mode);
1152 }
1153 
renameTab(const QString & arg1,const QString & arg2)1154 QString ScriptableProxy::renameTab(const QString &arg1, const QString &arg2)
1155 {
1156     INVOKE(renameTab, (arg1, arg2));
1157 
1158     if ( arg1.isEmpty() || arg2.isEmpty() )
1159         return tabNameEmptyError();
1160 
1161     const int i = m_wnd->findTabIndex(arg2);
1162     if (i == -1)
1163         return tabNotFoundError();
1164 
1165     if ( m_wnd->findTabIndex(arg1) != -1 )
1166         return ScriptableProxy::tr("Tab with given name already exists!");
1167 
1168     m_wnd->renameTab(arg1, i);
1169 
1170     return QString();
1171 }
1172 
removeTab(const QString & arg1)1173 QString ScriptableProxy::removeTab(const QString &arg1)
1174 {
1175     INVOKE(removeTab, (arg1));
1176 
1177     if ( arg1.isEmpty() )
1178         return tabNameEmptyError();
1179 
1180     const int i = m_wnd->findTabIndex(arg1);
1181     if (i == -1)
1182         return tabNotFoundError();
1183 
1184     m_wnd->removeTab(false, i);
1185     return QString();
1186 }
1187 
tabIcon(const QString & tabName)1188 QString ScriptableProxy::tabIcon(const QString &tabName)
1189 {
1190     INVOKE(tabIcon, (tabName));
1191     return getIconNameForTabName(tabName);
1192 }
1193 
setTabIcon(const QString & tabName,const QString & icon)1194 void ScriptableProxy::setTabIcon(const QString &tabName, const QString &icon)
1195 {
1196     INVOKE2(setTabIcon, (tabName, icon));
1197     m_wnd->setTabIcon(tabName, icon);
1198 }
1199 
unloadTabs(const QStringList & tabs)1200 QStringList ScriptableProxy::unloadTabs(const QStringList &tabs)
1201 {
1202     INVOKE(unloadTabs, (tabs));
1203     QStringList unloaded;
1204     for (const auto &tab : tabs) {
1205         if ( m_wnd->unloadTab(tab) )
1206             unloaded.append(tab);
1207     }
1208     return unloaded;
1209 }
1210 
forceUnloadTabs(const QStringList & tabs)1211 void ScriptableProxy::forceUnloadTabs(const QStringList &tabs)
1212 {
1213     INVOKE2(forceUnloadTabs, (tabs));
1214     for (const auto &tab : tabs)
1215         m_wnd->forceUnloadTab(tab);
1216 }
1217 
showBrowser(const QString & tabName)1218 bool ScriptableProxy::showBrowser(const QString &tabName)
1219 {
1220     INVOKE(showBrowser, (tabName));
1221     ClipboardBrowser *c = fetchBrowser(tabName);
1222     if (c)
1223         m_wnd->showBrowser(c);
1224     return m_wnd->isVisible();
1225 }
1226 
showBrowserAt(const QString & tabName,QRect rect)1227 bool ScriptableProxy::showBrowserAt(const QString &tabName, QRect rect)
1228 {
1229     INVOKE(showBrowserAt, (tabName, rect));
1230     setGeometryWithoutSave(m_wnd, rect);
1231     return showBrowser(tabName);
1232 }
1233 
action(const QVariantMap & arg1,const Command & arg2)1234 void ScriptableProxy::action(const QVariantMap &arg1, const Command &arg2)
1235 {
1236     INVOKE2(action, (arg1, arg2));
1237     m_wnd->action(arg1, arg2, QModelIndex());
1238 }
1239 
runInternalAction(const QVariantMap & data,const QString & command)1240 void ScriptableProxy::runInternalAction(const QVariantMap &data, const QString &command)
1241 {
1242     INVOKE2(runInternalAction, (data, command));
1243     auto action = new Action();
1244     action->setCommand(command);
1245     action->setData(data);
1246     m_wnd->runInternalAction(action);
1247 }
1248 
tryGetCommandOutput(const QString & command)1249 QByteArray ScriptableProxy::tryGetCommandOutput(const QString &command)
1250 {
1251     INVOKE(tryGetCommandOutput, (command));
1252 
1253     for (int i = 0; i < 3; ++i) {
1254         Action action;
1255         action.setCommand(command);
1256         action.setReadOutput(true);
1257 
1258         QByteArray output;
1259         connect( &action, &Action::actionOutput,
1260                  this, [&output](const QByteArray &actionOutput) {
1261                      output.append(actionOutput);
1262                  } );
1263 
1264         action.start();
1265         if ( !action.waitForFinished(5000) ) {
1266             if ( output.isEmpty() || !action.waitForFinished(30000) ) {
1267                 action.terminate();
1268                 continue;
1269             }
1270         }
1271 
1272         if ( action.actionFailed() || action.exitCode() != 0 )
1273             continue;
1274 
1275         return output;
1276     }
1277 
1278     return QByteArray();
1279 }
1280 
showMessage(const QString & title,const QString & msg,const QString & icon,int msec,const QString & notificationId,const NotificationButtons & buttons)1281 void ScriptableProxy::showMessage(const QString &title,
1282         const QString &msg,
1283         const QString &icon,
1284         int msec,
1285         const QString &notificationId,
1286         const NotificationButtons &buttons)
1287 {
1288     INVOKE2(showMessage, (title, msg, icon, msec, notificationId, buttons));
1289 
1290     auto notification = m_wnd->createNotification(notificationId);
1291     notification->setTitle(title);
1292     notification->setMessage(msg, Qt::AutoText);
1293     notification->setIcon(icon);
1294     notification->setInterval(msec);
1295     notification->setButtons(buttons);
1296 }
1297 
nextItem(const QString & tabName,int where)1298 QVariantMap ScriptableProxy::nextItem(const QString &tabName, int where)
1299 {
1300     INVOKE(nextItem, (tabName, where));
1301     ClipboardBrowser *c = fetchBrowser(tabName);
1302     if (!c)
1303         return QVariantMap();
1304 
1305     const int row = qMax(0, c->currentIndex().row()) + where;
1306     const QModelIndex index = c->index(row);
1307 
1308     if (!index.isValid())
1309         return QVariantMap();
1310 
1311     c->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
1312     return c->copyIndex(index);
1313 }
1314 
browserMoveToClipboard(const QString & tabName,int row)1315 void ScriptableProxy::browserMoveToClipboard(const QString &tabName, int row)
1316 {
1317     INVOKE2(browserMoveToClipboard, (tabName, row));
1318     ClipboardBrowser *c = fetchBrowser(tabName);
1319     m_wnd->moveToClipboard(c, row);
1320 }
1321 
browserSetCurrent(const QString & tabName,int arg1)1322 void ScriptableProxy::browserSetCurrent(const QString &tabName, int arg1)
1323 {
1324     INVOKE2(browserSetCurrent, (tabName, arg1));
1325     BROWSER(tabName, setCurrent(arg1));
1326 }
1327 
browserRemoveRows(const QString & tabName,QVector<int> rows)1328 QString ScriptableProxy::browserRemoveRows(const QString &tabName, QVector<int> rows)
1329 {
1330     INVOKE(browserRemoveRows, (tabName, rows));
1331     ClipboardBrowser *c = fetchBrowser(tabName);
1332     if (!c)
1333         return QLatin1String("Invalid tab");
1334 
1335     std::sort( rows.begin(), rows.end(), std::greater<int>() );
1336 
1337     QModelIndexList indexes;
1338     indexes.reserve(rows.size());
1339 
1340     for (int row : rows) {
1341         const QModelIndex indexToRemove = c->index(row);
1342         if ( indexToRemove.isValid() )
1343             indexes.append(indexToRemove);
1344     }
1345 
1346     const QPersistentModelIndex currentIndex = c->currentIndex();
1347 
1348     QString error;
1349     const int lastRow = c->removeIndexes(indexes, &error);
1350 
1351     if ( !error.isEmpty() )
1352         return error;
1353 
1354     if ( !currentIndex.isValid() ) {
1355         const int currentRow = qMin(lastRow, c->length() - 1);
1356         c->setCurrent(currentRow);
1357     }
1358 
1359     return QString();
1360 }
1361 
browserMoveSelected(int targetRow)1362 void ScriptableProxy::browserMoveSelected(int targetRow)
1363 {
1364     INVOKE2(browserMoveSelected, (targetRow));
1365 
1366     const QList<QPersistentModelIndex> selected = selectedIndexes();
1367     if ( selected.isEmpty() )
1368         return;
1369 
1370     ClipboardBrowser *c = m_wnd->browserForItem(selected.first());
1371     if (c == nullptr)
1372         return;
1373 
1374     QModelIndexList indexes;
1375     for (const auto &index : selected)
1376         indexes.append(index);
1377     c->move(indexes, targetRow);
1378 }
1379 
browserEditRow(const QString & tabName,int arg1)1380 void ScriptableProxy::browserEditRow(const QString &tabName, int arg1)
1381 {
1382     INVOKE2(browserEditRow, (tabName, arg1));
1383     BROWSER(tabName, editRow(arg1));
1384 }
1385 
browserEditNew(const QString & tabName,const QString & arg1,bool changeClipboard)1386 void ScriptableProxy::browserEditNew(const QString &tabName, const QString &arg1, bool changeClipboard)
1387 {
1388     INVOKE2(browserEditNew, (tabName, arg1, changeClipboard));
1389     BROWSER(tabName, editNew(arg1, changeClipboard));
1390 }
1391 
tabs()1392 QStringList ScriptableProxy::tabs()
1393 {
1394     INVOKE(tabs, ());
1395     return m_wnd->tabs();
1396 }
1397 
toggleVisible()1398 bool ScriptableProxy::toggleVisible()
1399 {
1400     INVOKE(toggleVisible, ());
1401     return m_wnd->toggleVisible();
1402 }
1403 
toggleMenu(const QString & tabName,int maxItemCount,QPoint position)1404 bool ScriptableProxy::toggleMenu(const QString &tabName, int maxItemCount, QPoint position)
1405 {
1406     INVOKE(toggleMenu, (tabName, maxItemCount, position));
1407     return m_wnd->toggleMenu(tabName, maxItemCount, position);
1408 }
1409 
toggleCurrentMenu()1410 bool ScriptableProxy::toggleCurrentMenu()
1411 {
1412     INVOKE(toggleCurrentMenu, ());
1413     return m_wnd->toggleMenu();
1414 }
1415 
findTabIndex(const QString & arg1)1416 int ScriptableProxy::findTabIndex(const QString &arg1)
1417 {
1418     INVOKE(findTabIndex, (arg1));
1419     return m_wnd->findTabIndex(arg1);
1420 }
1421 
menuItems(const QVector<QVariantMap> & items)1422 int ScriptableProxy::menuItems(const QVector<QVariantMap> &items)
1423 {
1424     INVOKE(menuItems, (items));
1425 
1426     TrayMenu menu;
1427     menu.setObjectName("CustomMenu");
1428     menu.setRowIndexFromOne( AppConfig().option<Config::row_index_from_one>() );
1429 
1430     const auto addMenuItems = [&](const QString &searchText) {
1431         menu.clearClipboardItems();
1432         for (const QVariantMap &data : items) {
1433             const QString text = getTextData(data);
1434             if ( text.contains(searchText, Qt::CaseInsensitive) )
1435                 menu.addClipboardItemAction(data, true);
1436         }
1437     };
1438     addMenuItems(QString());
1439 
1440     connect(&menu, &TrayMenu::searchRequest, addMenuItems);
1441 
1442     const QPoint pos = QCursor::pos();
1443     QAction *act = menu.exec(pos);
1444     if (act == nullptr)
1445         return -1;
1446 
1447     return items.indexOf(act->data().toMap());
1448 }
1449 
openActionDialog(const QVariantMap & arg1)1450 void ScriptableProxy::openActionDialog(const QVariantMap &arg1)
1451 {
1452     INVOKE2(openActionDialog, (arg1));
1453     m_wnd->openActionDialog(arg1);
1454 }
1455 
loadTab(const QString & arg1)1456 bool ScriptableProxy::loadTab(const QString &arg1)
1457 {
1458     INVOKE(loadTab, (arg1));
1459     return m_wnd->loadTab(arg1);
1460 }
1461 
saveTab(const QString & tabName,const QString & arg1)1462 bool ScriptableProxy::saveTab(const QString &tabName, const QString &arg1)
1463 {
1464     INVOKE(saveTab, (tabName, arg1));
1465     ClipboardBrowser *c = fetchBrowser(tabName);
1466     if (!c)
1467         return false;
1468 
1469     const int i = m_wnd->findTabIndex( c->tabName() );
1470     return m_wnd->saveTab(arg1, i);
1471 }
1472 
importData(const QString & fileName)1473 bool ScriptableProxy::importData(const QString &fileName)
1474 {
1475     INVOKE(importData, (fileName));
1476     return m_wnd->importDataFrom(fileName, ImportOptions::All);
1477 }
1478 
exportData(const QString & fileName)1479 bool ScriptableProxy::exportData(const QString &fileName)
1480 {
1481     INVOKE(exportData, (fileName));
1482     return m_wnd->exportAllData(fileName);
1483 }
1484 
config(const QVariantList & nameValue)1485 QVariant ScriptableProxy::config(const QVariantList &nameValue)
1486 {
1487     INVOKE(config, (nameValue));
1488     return m_wnd->config(nameValue);
1489 }
1490 
configDescription()1491 QString ScriptableProxy::configDescription()
1492 {
1493     INVOKE(configDescription, ());
1494     return m_wnd->configDescription();
1495 }
1496 
toggleConfig(const QString & optionName)1497 QVariant ScriptableProxy::toggleConfig(const QString &optionName)
1498 {
1499     INVOKE(toggleConfig, (optionName));
1500 
1501     QVariantList nameValue;
1502     nameValue.append(optionName);
1503     const auto values = m_wnd->config(nameValue);
1504     if ( values.type() == QVariant::StringList )
1505         return values;
1506 
1507     const auto oldValue = values.toMap().constBegin().value();
1508     if ( oldValue.type() != QVariant::Bool )
1509         return QVariant();
1510 
1511     const auto newValue = !QVariant(oldValue).toBool();
1512     nameValue.append(newValue);
1513     return m_wnd->config(nameValue).toMap().constBegin().value();
1514 }
1515 
browserLength(const QString & tabName)1516 int ScriptableProxy::browserLength(const QString &tabName)
1517 {
1518     INVOKE(browserLength, (tabName));
1519     ClipboardBrowser *c = fetchBrowser(tabName);
1520     return c ? c->length() : 0;
1521 }
1522 
browserOpenEditor(const QString & tabName,const QByteArray & arg1,bool changeClipboard)1523 bool ScriptableProxy::browserOpenEditor(const QString &tabName, const QByteArray &arg1, bool changeClipboard)
1524 {
1525     INVOKE(browserOpenEditor, (tabName, arg1, changeClipboard));
1526     ClipboardBrowser *c = fetchBrowser(tabName);
1527     return c && c->openEditor(arg1, changeClipboard);
1528 }
1529 
browserInsert(const QString & tabName,int row,const QVector<QVariantMap> & items)1530 QString ScriptableProxy::browserInsert(const QString &tabName, int row, const QVector<QVariantMap> &items)
1531 {
1532     INVOKE(browserInsert, (tabName, row, items));
1533 
1534     ClipboardBrowser *c = fetchBrowser(tabName);
1535     if (!c)
1536         return QLatin1String("Invalid tab");
1537 
1538     if ( !c->allocateSpaceForNewItems(items.size()) )
1539         return QLatin1String("Tab is full (cannot remove any items)");
1540 
1541     for (const auto &item : items) {
1542         if ( !c->add(item, row) )
1543             return QLatin1String("Failed to new add items");
1544     }
1545 
1546     return QString();
1547 }
1548 
browserChange(const QString & tabName,int row,const QVector<QVariantMap> & items)1549 QString ScriptableProxy::browserChange(const QString &tabName, int row, const QVector<QVariantMap> &items)
1550 {
1551     INVOKE(browserChange, (tabName, row, items));
1552 
1553     ClipboardBrowser *c = fetchBrowser(tabName);
1554     if (!c)
1555         return QLatin1String("Invalid tab");
1556 
1557     int currentRow = row;
1558     for (const auto &data : items) {
1559         const auto index = c->index(currentRow);
1560         QVariantMap itemData = c->model()->data(index, contentType::data).toMap();
1561         for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
1562             if ( it.value().isValid() )
1563                 itemData.insert( it.key(), it.value() );
1564             else
1565                 itemData.remove( it.key() );
1566         }
1567         c->model()->setData(index, itemData, contentType::data);
1568         ++currentRow;
1569     }
1570 
1571     return QString();
1572 }
1573 
browserItemData(const QString & tabName,int arg1,const QString & arg2)1574 QByteArray ScriptableProxy::browserItemData(const QString &tabName, int arg1, const QString &arg2)
1575 {
1576     INVOKE(browserItemData, (tabName, arg1, arg2));
1577     return itemData(tabName, arg1, arg2);
1578 }
1579 
browserItemData(const QString & tabName,int arg1)1580 QVariantMap ScriptableProxy::browserItemData(const QString &tabName, int arg1)
1581 {
1582     INVOKE(browserItemData, (tabName, arg1));
1583     return itemData(tabName, arg1);
1584 }
1585 
setCurrentTab(const QString & tabName)1586 void ScriptableProxy::setCurrentTab(const QString &tabName)
1587 {
1588     INVOKE2(setCurrentTab, (tabName));
1589     m_wnd->addAndFocusTab(tabName);
1590 }
1591 
tab(const QString & tabName)1592 QString ScriptableProxy::tab(const QString &tabName)
1593 {
1594     INVOKE(tab, (tabName));
1595     ClipboardBrowser *c = fetchBrowser(tabName);
1596     return c ? c->tabName() : QString();
1597 }
1598 
currentItem()1599 int ScriptableProxy::currentItem()
1600 {
1601     INVOKE(currentItem, ());
1602 
1603     const QPersistentModelIndex current =
1604             m_actionData.value(mimeCurrentItem).value<QPersistentModelIndex>();
1605     return current.isValid() ? current.row() : -1;
1606 }
1607 
selectItems(const QString & tabName,const QVector<int> & rows)1608 bool ScriptableProxy::selectItems(const QString &tabName, const QVector<int> &rows)
1609 {
1610     INVOKE(selectItems, (tabName, rows));
1611     ClipboardBrowser *c = fetchBrowser(tabName);
1612     if (!c)
1613         return false;
1614 
1615     c->clearSelection();
1616 
1617     if ( !rows.isEmpty() ) {
1618         c->setCurrent(rows.last());
1619 
1620         for (int i : rows) {
1621             const QModelIndex index = c->index(i);
1622             if ( index.isValid() && !c->isFiltered(i) )
1623                 c->selectionModel()->select(index, QItemSelectionModel::Select);
1624         }
1625     }
1626 
1627     return true;
1628 }
1629 
selectedItems()1630 QVector<int> ScriptableProxy::selectedItems()
1631 {
1632     INVOKE(selectedItems, ());
1633 
1634     QVector<int> selectedRows;
1635     const QList<QPersistentModelIndex> selected = selectedIndexes();
1636     selectedRows.reserve(selected.count());
1637     for (const auto &index : selected) {
1638         if (index.isValid())
1639             selectedRows.append(index.row());
1640     }
1641 
1642     return selectedRows;
1643 }
1644 
selectedItemData(int selectedIndex)1645 QVariantMap ScriptableProxy::selectedItemData(int selectedIndex)
1646 {
1647     INVOKE(selectedItemData, (selectedIndex));
1648 
1649     auto c = currentBrowser();
1650     if (!c)
1651         return QVariantMap();
1652 
1653     const auto index = selectedIndexes().value(selectedIndex);
1654     Q_ASSERT( !index.isValid() || index.model() == c->model() );
1655     return c->copyIndex(index);
1656 }
1657 
setSelectedItemData(int selectedIndex,const QVariantMap & data)1658 bool ScriptableProxy::setSelectedItemData(int selectedIndex, const QVariantMap &data)
1659 {
1660     INVOKE(setSelectedItemData, (selectedIndex, data));
1661 
1662     auto c = currentBrowser();
1663     if (!c)
1664         return false;
1665 
1666     const auto index = selectedIndexes().value(selectedIndex);
1667     if ( !index.isValid() )
1668         return false;
1669 
1670     Q_ASSERT( index.model() == c->model() );
1671     return c->model()->setData(index, data, contentType::data);
1672 }
1673 
selectedItemsData()1674 QVector<QVariantMap> ScriptableProxy::selectedItemsData()
1675 {
1676     INVOKE(selectedItemsData, ());
1677 
1678     auto c = currentBrowser();
1679     if (!c)
1680         return QVector<QVariantMap>();
1681 
1682     const auto model = c->model();
1683 
1684     QVector<QVariantMap> dataList;
1685     const auto selected = selectedIndexes();
1686     dataList.reserve(selected.size());
1687 
1688     for (const auto &index : selected) {
1689         if ( index.isValid() ) {
1690             Q_ASSERT( index.model() == model );
1691             dataList.append( c->copyIndex(index) );
1692         }
1693     }
1694 
1695     return dataList;
1696 }
1697 
setSelectedItemsData(const QVector<QVariantMap> & dataList)1698 void ScriptableProxy::setSelectedItemsData(const QVector<QVariantMap> &dataList)
1699 {
1700     INVOKE2(setSelectedItemsData, (dataList));
1701 
1702     auto c = currentBrowser();
1703     if (!c)
1704         return;
1705 
1706     const auto model = c->model();
1707 
1708     const auto indexes = selectedIndexes();
1709     const auto count = std::min( indexes.size(), dataList.size() );
1710     for ( int i = 0; i < count; ++i ) {
1711         const auto &index = indexes[i];
1712         if ( index.isValid() ) {
1713             Q_ASSERT( index.model() == model );
1714             model->setData(index, dataList[i], contentType::data);
1715         }
1716     }
1717 }
1718 
createSelection(const QString & tabName)1719 int ScriptableProxy::createSelection(const QString &tabName)
1720 {
1721     INVOKE(createSelection, (tabName));
1722     const int newSelectionId = ++m_lastSelectionId;
1723     ClipboardBrowser *c = fetchBrowser(tabName);
1724     if (c)
1725         m_selections[newSelectionId] = {c, {}};
1726     return newSelectionId;
1727 }
1728 
selectionCopy(int id)1729 int ScriptableProxy::selectionCopy(int id)
1730 {
1731     INVOKE(selectionCopy, (id));
1732     const int newSelectionId = ++m_lastSelectionId;
1733     auto selection = m_selections.value(id);
1734     if (selection.browser)
1735         m_selections[newSelectionId] = selection;
1736     return newSelectionId;
1737 }
1738 
destroySelection(int id)1739 void ScriptableProxy::destroySelection(int id)
1740 {
1741     INVOKE2(destroySelection, (id));
1742     m_selections.remove(id);
1743 }
1744 
selectionRemoveAll(int id)1745 void ScriptableProxy::selectionRemoveAll(int id)
1746 {
1747     INVOKE2(selectionRemoveAll, (id));
1748     auto selection = m_selections.take(id);
1749     if (!selection.browser)
1750         return;
1751     selectionRemoveInvalid(&selection.indexes);
1752 
1753     QModelIndexList indexes;
1754     for (const auto &index : selection.indexes)
1755         indexes.append(index);
1756 
1757     selection.browser->removeIndexes(indexes);
1758 
1759     selectionRemoveInvalid(&selection.indexes);
1760     m_selections[id] = selection;
1761 }
1762 
selectionSelectRemovable(int id)1763 void ScriptableProxy::selectionSelectRemovable(int id)
1764 {
1765     INVOKE2(selectionSelectRemovable, (id));
1766     auto selection = m_selections.take(id);
1767     if (!selection.browser)
1768         return;
1769 
1770     QList<QPersistentModelIndex> indexes;
1771     for (int row = 0; row < selection.browser->length(); ++row) {
1772         const auto index = selection.browser->index(row);
1773         if ( !selection.indexes.contains(index) && selection.browser->canRemoveItems({index}) )
1774             indexes.append(index);
1775     }
1776     selection.indexes.append(indexes);
1777     m_selections[id] = selection;
1778 }
1779 
selectionInvert(int id)1780 void ScriptableProxy::selectionInvert(int id)
1781 {
1782     INVOKE2(selectionInvert, (id));
1783     auto selection = m_selections.take(id);
1784     if (!selection.browser)
1785         return;
1786 
1787     QList<QPersistentModelIndex> indexes;
1788     for (int row = 0; row < selection.browser->length(); ++row) {
1789         const auto index = selection.browser->index(row);
1790         if ( !selection.indexes.contains(index) )
1791             indexes.append(index);
1792     }
1793     selection.indexes = indexes;
1794     m_selections[id] = selection;
1795 }
1796 
selectionSelectAll(int id)1797 void ScriptableProxy::selectionSelectAll(int id)
1798 {
1799     INVOKE2(selectionSelectAll, (id));
1800     auto selection = m_selections.take(id);
1801     if (!selection.browser)
1802         return;
1803 
1804     selection.indexes.clear();
1805     for (int row = 0; row < selection.browser->length(); ++row)
1806         selection.indexes.append(selection.browser->index(row));
1807     m_selections[id] = selection;
1808 }
1809 
selectionSelect(int id,const QVariant & maybeRe,const QString & mimeFormat)1810 void ScriptableProxy::selectionSelect(int id, const QVariant &maybeRe, const QString &mimeFormat)
1811 {
1812     INVOKE2(selectionSelect, (id, maybeRe, mimeFormat));
1813     auto selection = m_selections.take(id);
1814     if (!selection.browser)
1815         return;
1816 
1817     const QRegularExpression re = maybeRe.toRegularExpression();
1818     QList<QPersistentModelIndex> indexes;
1819     for (int row = 0; row < selection.browser->length(); ++row) {
1820         const auto index = selection.browser->index(row);
1821         if ( selection.indexes.contains(index) )
1822             continue;
1823 
1824         const QVariantMap dataMap = index.data(contentType::data).toMap();
1825         if ( mimeFormat.isEmpty() ) {
1826             if ( !maybeRe.isValid() )
1827                 continue;
1828             const QString text = getTextData(dataMap);
1829             if ( text.contains(re) )
1830                 indexes.append(index);
1831         } else if ( dataMap.contains(mimeFormat) == maybeRe.isValid() ) {
1832             const QString text = getTextData(dataMap, mimeFormat);
1833             if ( text.contains(re) )
1834                 indexes.append(index);
1835         }
1836     }
1837     selection.indexes.append(indexes);
1838     m_selections[id] = selection;
1839 }
1840 
selectionDeselectIndexes(int id,const QVector<int> & indexes)1841 void ScriptableProxy::selectionDeselectIndexes(int id, const QVector<int> &indexes)
1842 {
1843     INVOKE2(selectionDeselectIndexes, (id, indexes));
1844 
1845     auto selection = m_selections.take(id);
1846     auto indexesSorted = indexes;
1847     std::sort(indexesSorted.begin(), indexesSorted.end(), std::greater<int>());
1848     for (int index : indexesSorted)
1849         selection.indexes.removeAt(index);
1850     m_selections[id] = selection;
1851 }
1852 
selectionDeselectSelection(int id,int toDeselectId)1853 void ScriptableProxy::selectionDeselectSelection(int id, int toDeselectId)
1854 {
1855     INVOKE2(selectionDeselectSelection, (id, toDeselectId));
1856     auto selection = m_selections.take(id);
1857     const auto deselection = m_selections.value(toDeselectId);
1858 
1859     selectionRemoveIf(
1860         &selection.indexes,
1861         [&](const QPersistentModelIndex &index){
1862             return !index.isValid() || deselection.indexes.contains(index);
1863         });
1864     m_selections[id] = selection;
1865 }
1866 
selectionGetCurrent(int id)1867 void ScriptableProxy::selectionGetCurrent(int id)
1868 {
1869     INVOKE2(selectionGetCurrent, (id));
1870     auto selection = m_selections.take(id);
1871     if (!selection.browser)
1872         return;
1873 
1874     auto selected = selectedIndexes();
1875     selectionRemoveInvalid(&selected);
1876     if ( selected.isEmpty() ) {
1877         m_selections[id] = {nullptr, {}};
1878     } else {
1879         ClipboardBrowser *c = m_wnd->browserForItem(selected.first());
1880         m_selections[id] = {c, selected};
1881     }
1882 }
1883 
selectionGetSize(int id)1884 int ScriptableProxy::selectionGetSize(int id)
1885 {
1886     INVOKE(selectionGetSize, (id));
1887     return m_selections.value(id).indexes.size();
1888 }
1889 
selectionGetTabName(int id)1890 QString ScriptableProxy::selectionGetTabName(int id)
1891 {
1892     INVOKE(selectionGetTabName, (id));
1893     const auto selection = m_selections.value(id);
1894     return selection.browser ? selection.browser->tabName() : QString();
1895 }
1896 
selectionGetRows(int id)1897 QVector<int> ScriptableProxy::selectionGetRows(int id)
1898 {
1899     INVOKE(selectionGetRows, (id));
1900 
1901     auto selection = m_selections.value(id);
1902     selectionRemoveInvalid(&selection.indexes);
1903     QVector<int> rows;
1904     rows.reserve(selection.indexes.size());
1905     for (const auto &index : selection.indexes)
1906         rows.append(index.row());
1907     return rows;
1908 }
1909 
selectionGetItemIndex(int id,int index)1910 QVariantMap ScriptableProxy::selectionGetItemIndex(int id, int index)
1911 {
1912     INVOKE(selectionGetItemIndex, (id, index));
1913 
1914     auto selection = m_selections.value(id);
1915     if ( selection.indexes.isEmpty() || index < 0 || index >= selection.indexes.size() )
1916         return {};
1917 
1918     return selection.indexes[index].data(contentType::data).toMap();
1919 }
1920 
selectionSetItemIndex(int id,int index,const QVariantMap & item)1921 void ScriptableProxy::selectionSetItemIndex(int id, int index, const QVariantMap &item)
1922 {
1923     INVOKE2(selectionSetItemIndex, (id, index, item));
1924 
1925     const auto selection = m_selections.value(id);
1926     if ( !selection.browser || index < 0 || index >= selection.indexes.size() )
1927         return;
1928 
1929     const QModelIndex ind = selection.indexes[index];
1930     selection.browser->model()->setData(ind, item, contentType::data);
1931 }
1932 
selectionGetItemsData(int id)1933 QVariantList ScriptableProxy::selectionGetItemsData(int id)
1934 {
1935     INVOKE(selectionGetItemsData, (id));
1936 
1937     QVariantList dataList;
1938     const auto selection = m_selections.value(id);
1939     for (const auto &index : selection.indexes) {
1940         const auto data = index.data(contentType::data).toMap();
1941         dataList.append(data);
1942     }
1943     return dataList;
1944 }
1945 
selectionSetItemsData(int id,const QVariantList & dataList)1946 void ScriptableProxy::selectionSetItemsData(int id, const QVariantList &dataList)
1947 {
1948     INVOKE2(selectionSetItemsData, (id, dataList));
1949 
1950     const auto selection = m_selections.value(id);
1951     const auto count = std::min( selection.indexes.size(), dataList.size() );
1952     for ( int i = 0; i < count; ++i ) {
1953         const auto &index = selection.indexes[i];
1954         if ( index.isValid() )
1955             selection.browser->model()->setData(index, dataList[i], contentType::data);
1956     }
1957 }
1958 
selectionGetItemsFormat(int id,const QString & format)1959 QVariantList ScriptableProxy::selectionGetItemsFormat(int id, const QString &format)
1960 {
1961     INVOKE(selectionGetItemsFormat, (id, format));
1962 
1963     QVariantList dataList;
1964     const auto selection = m_selections.value(id);
1965     for (const auto &index : selection.indexes) {
1966         const auto data = index.data(contentType::data).toMap();
1967         dataList.append( data.value(format) );
1968     }
1969     return dataList;
1970 }
1971 
selectionSetItemsFormat(int id,const QString & mime,const QVariant & value)1972 void ScriptableProxy::selectionSetItemsFormat(int id, const QString &mime, const QVariant &value)
1973 {
1974     INVOKE2(selectionSetItemsFormat, (id, mime, value));
1975 
1976     const auto selection = m_selections.value(id);
1977     for (const auto &index : selection.indexes) {
1978         if ( index.isValid() ) {
1979             QVariantMap data = index.data(contentType::data).toMap();
1980             if (value.isValid())
1981                 data[mime] = value;
1982             else
1983                 data.remove(mime);
1984             selection.browser->model()->setData(index, data, contentType::data);
1985         }
1986     }
1987 }
1988 
selectionMove(int id,int row)1989 void ScriptableProxy::selectionMove(int id, int row)
1990 {
1991     INVOKE2(selectionMove, (id, row));
1992     auto selection = m_selections.value(id);
1993     selectionRemoveInvalid(&selection.indexes);
1994 
1995     QModelIndexList indexes;
1996     for (const auto &index : selection.indexes)
1997         indexes.append(index);
1998 
1999     if ( !indexes.isEmpty() )
2000         selection.browser->move(indexes, row);
2001 }
2002 
2003 #ifdef HAS_TESTS
sendKeys(const QString & expectedWidgetName,const QString & keys,int delay)2004 void ScriptableProxy::sendKeys(const QString &expectedWidgetName, const QString &keys, int delay)
2005 {
2006     INVOKE2(sendKeys, (expectedWidgetName, keys, delay));
2007     Q_ASSERT( keyClicker()->succeeded() || keyClicker()->failed() );
2008     keyClicker()->sendKeyClicks(expectedWidgetName, keys, delay, 10);
2009 }
2010 
sendKeysSucceeded()2011 bool ScriptableProxy::sendKeysSucceeded()
2012 {
2013     INVOKE(sendKeysSucceeded, ());
2014     return keyClicker()->succeeded();
2015 }
2016 
sendKeysFailed()2017 bool ScriptableProxy::sendKeysFailed()
2018 {
2019     INVOKE(sendKeysFailed, ());
2020     return keyClicker()->failed();
2021 }
2022 
testSelected()2023 QString ScriptableProxy::testSelected()
2024 {
2025     INVOKE(testSelected, ());
2026 
2027     ClipboardBrowser *browser = m_wnd->browser();
2028     if (!browser)
2029         return QString();
2030 
2031     if (browser->length() == 0)
2032         return browser->tabName();
2033 
2034     QModelIndexList selectedIndexes = browser->selectionModel()->selectedIndexes();
2035 
2036     QStringList result;
2037     result.reserve( selectedIndexes.size() + 1 );
2038 
2039     const QModelIndex currentIndex = browser->currentIndex();
2040     result.append(currentIndex.isValid() ? QString::number(currentIndex.row()) : "_");
2041 
2042     QList<int> selectedRows;
2043     selectedRows.reserve( selectedIndexes.size() );
2044     for (const auto &index : selectedIndexes)
2045         selectedRows.append(index.row());
2046     std::sort( selectedRows.begin(), selectedRows.end() );
2047 
2048     for (int row : selectedRows)
2049         result.append(QString::number(row));
2050 
2051     return browser->tabName() + " " + result.join(" ");
2052 }
2053 #endif // HAS_TESTS
2054 
serverLog(const QString & text)2055 void ScriptableProxy::serverLog(const QString &text)
2056 {
2057     INVOKE2(serverLog, (text));
2058     log(text, LogAlways);
2059 }
2060 
currentWindowTitle()2061 QString ScriptableProxy::currentWindowTitle()
2062 {
2063     INVOKE(currentWindowTitle, ());
2064     PlatformWindowPtr window = platformNativeInterface()->getCurrentWindow();
2065     return window ? window->getTitle() : QString();
2066 }
2067 
inputDialog(const NamedValueList & values)2068 int ScriptableProxy::inputDialog(const NamedValueList &values)
2069 {
2070     INVOKE(inputDialog, (values));
2071 
2072     InputDialog inputDialog;
2073     inputDialog.dialog = new QDialog(m_wnd);
2074     QDialog &dialog = *inputDialog.dialog;
2075 
2076     QString dialogTitle;
2077     QIcon icon;
2078     QVBoxLayout layout(&dialog);
2079     QWidgetList widgets;
2080     widgets.reserve(values.size());
2081 
2082     QString styleSheet;
2083     QRect geometry(-1, -1, 0, 0);
2084 
2085     for (const auto &value : values) {
2086         if (value.name == ".title")
2087             dialogTitle = value.value.toString();
2088         else if (value.name == ".icon")
2089             icon = loadIcon(value.value.toString());
2090         else if (value.name == ".style")
2091             styleSheet = value.value.toString();
2092         else if (value.name == ".height")
2093             geometry.setHeight( pointsToPixels(value.value.toInt()) );
2094         else if (value.name == ".width")
2095             geometry.setWidth( pointsToPixels(value.value.toInt()) );
2096         else if (value.name == ".x")
2097             geometry.setX(value.value.toInt());
2098         else if (value.name == ".y")
2099             geometry.setY(value.value.toInt());
2100         else if (value.name == ".label")
2101             createAndSetWidget<QLabel>("text", value.value, &dialog);
2102         else if (value.name == ".defaultChoice")
2103             inputDialog.defaultChoice = value.value.toString();
2104         else
2105             widgets.append( createWidget(value.name, value.value, &inputDialog) );
2106     }
2107 
2108     dialog.adjustSize();
2109 
2110     if (geometry.height() == 0)
2111         geometry.setHeight(dialog.height());
2112     if (geometry.width() == 0)
2113         geometry.setWidth(dialog.width());
2114     if (geometry.isValid())
2115         dialog.resize(geometry.size());
2116     if (geometry.x() >= 0 && geometry.y() >= 0)
2117         dialog.move(geometry.topLeft());
2118 
2119     if ( !styleSheet.isEmpty() )
2120         dialog.setStyleSheet(styleSheet);
2121 
2122     auto buttons = new QDialogButtonBox(
2123                 QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
2124     QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept );
2125     QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject );
2126     layout.addWidget(buttons);
2127 
2128     installShortcutToCloseDialog(&dialog, &dialog, Qt::ControlModifier | Qt::Key_Enter);
2129     installShortcutToCloseDialog(&dialog, &dialog, Qt::ControlModifier | Qt::Key_Return);
2130 
2131     if (icon.isNull())
2132         icon = appIcon();
2133     dialog.setWindowIcon(icon);
2134 
2135     const int dialogId = ++m_lastInputDialogId;
2136     connect(&dialog, &QDialog::finished, this, [this, dialogId, inputDialog, widgets]() {
2137         if (inputDialog.dialog == nullptr)
2138             return;
2139 
2140         NamedValueList result;
2141         result.reserve( widgets.size() );
2142 
2143         if ( inputDialog.dialog->result() ) {
2144             for ( auto w : widgets ) {
2145                 const QString propertyName = w->property(propertyWidgetProperty).toString();
2146                 const QString name = w->property(propertyWidgetName).toString();
2147                 const QVariant value = w->property(propertyName.toUtf8().constData());
2148                 result.append( NamedValue(name, value) );
2149             }
2150         }
2151 
2152         QByteArray bytes;
2153         {
2154             QDataStream stream(&bytes, QIODevice::WriteOnly);
2155             stream << dialogId << result;
2156         }
2157 
2158         inputDialog.dialog->deleteLater();
2159         emit sendMessage(bytes, CommandInputDialogFinished);
2160     });
2161 
2162     // Connecting this directly to QEventLoop::quit() doesn't seem to work always.
2163     connect(this, &ScriptableProxy::abortEvaluation, &dialog, &QDialog::reject);
2164 
2165     if ( !dialogTitle.isNull() ) {
2166         dialog.setWindowTitle(dialogTitle);
2167         dialog.setObjectName(QLatin1String("dialog_") + dialogTitle);
2168         WindowGeometryGuard::create(&dialog);
2169     }
2170 
2171     dialog.show();
2172 
2173     // Skip raising dialog in tests.
2174     if ( !qApp->property("CopyQ_test_id").isValid() )
2175         raiseWindow(&dialog);
2176 
2177     return dialogId;
2178 }
2179 
setSelectedItemsData(const QString & mime,const QVariant & value)2180 void ScriptableProxy::setSelectedItemsData(const QString &mime, const QVariant &value)
2181 {
2182     INVOKE2(setSelectedItemsData, (mime, value));
2183     const QList<QPersistentModelIndex> selected = selectedIndexes();
2184     for (const auto &index : selected) {
2185         ClipboardBrowser *c = m_wnd->browserForItem(index);
2186         if (c) {
2187             QVariantMap data = c->model()->data(index, contentType::data).toMap();
2188             if (value.isValid())
2189                 data[mime] = value;
2190             else
2191                 data.remove(mime);
2192             c->model()->setData(index, data, contentType::data);
2193         }
2194     }
2195 }
2196 
filter(const QString & text)2197 void ScriptableProxy::filter(const QString &text)
2198 {
2199     INVOKE2(filter, (text));
2200     m_wnd->setFilter(text);
2201 }
2202 
filter()2203 QString ScriptableProxy::filter()
2204 {
2205     INVOKE(filter, ());
2206     return m_wnd->filter();
2207 }
2208 
commands()2209 QVector<Command> ScriptableProxy::commands()
2210 {
2211     INVOKE(commands, ());
2212     return loadAllCommands();
2213 }
2214 
setCommands(const QVector<Command> & commands)2215 void ScriptableProxy::setCommands(const QVector<Command> &commands)
2216 {
2217     INVOKE2(setCommands, (commands));
2218     m_wnd->setCommands(commands);
2219 }
2220 
addCommands(const QVector<Command> & commands)2221 void ScriptableProxy::addCommands(const QVector<Command> &commands)
2222 {
2223     INVOKE2(addCommands, (commands));
2224     m_wnd->addCommands(commands);
2225 }
2226 
screenshot(const QString & format,const QString & screenName,bool select)2227 QByteArray ScriptableProxy::screenshot(const QString &format, const QString &screenName, bool select)
2228 {
2229     INVOKE(screenshot, (format, screenName, select));
2230 
2231     QScreen *selectedScreen = nullptr;
2232     if ( screenName.isEmpty() ) {
2233         const auto mousePosition = QCursor::pos();
2234         const int screenNumber = ::screenNumberAt(mousePosition);
2235         if (screenNumber != -1)
2236             selectedScreen = QGuiApplication::screens().value(screenNumber);
2237     } else {
2238         for ( const auto screen : QApplication::screens() ) {
2239             if (screen->name() == screenName) {
2240                 selectedScreen = screen;
2241                 break;
2242             }
2243         }
2244     }
2245 
2246     if (!selectedScreen)
2247         return QByteArray();
2248 
2249     auto pixmap = selectedScreen->grabWindow(0);
2250 
2251     const auto geometry = selectedScreen->geometry();
2252 
2253     if (select) {
2254         ScreenshotRectWidget rectWidget(pixmap);
2255         rectWidget.setGeometry(geometry);
2256         rectWidget.setWindowState(Qt::WindowFullScreen);
2257         rectWidget.setWindowModality(Qt::ApplicationModal);
2258         rectWidget.show();
2259         raiseWindow(&rectWidget);
2260 
2261         while ( !rectWidget.isHidden() )
2262             QCoreApplication::processEvents();
2263         const auto rect = rectWidget.selectionRect;
2264         if ( rect.isValid() ) {
2265             const auto ratio = pixelRatio(&pixmap);
2266             const QRect rect2( rect.topLeft() * ratio, rect.size() * ratio );
2267             pixmap = pixmap.copy(rect2);
2268         }
2269     }
2270 
2271     QByteArray bytes;
2272     {
2273         QBuffer buffer(&bytes);
2274         buffer.open(QIODevice::WriteOnly);
2275         if ( !pixmap.save(&buffer, format.toUtf8().constData()) )
2276             return QByteArray();
2277     }
2278 
2279     return bytes;
2280 }
2281 
screenNames()2282 QStringList ScriptableProxy::screenNames()
2283 {
2284     INVOKE(screenNames, ());
2285 
2286     QStringList result;
2287     const auto screens = QApplication::screens();
2288     result.reserve( screens.size() );
2289 
2290     for ( const auto screen : screens )
2291         result.append(screen->name());
2292 
2293     return result;
2294 }
2295 
queryKeyboardModifiers()2296 Qt::KeyboardModifiers ScriptableProxy::queryKeyboardModifiers()
2297 {
2298     INVOKE(queryKeyboardModifiers, ());
2299     return QApplication::queryKeyboardModifiers();
2300 }
2301 
pointerPosition()2302 QPoint ScriptableProxy::pointerPosition()
2303 {
2304     INVOKE(pointerPosition, ());
2305     return QCursor::pos();
2306 }
2307 
setPointerPosition(int x,int y)2308 void ScriptableProxy::setPointerPosition(int x, int y)
2309 {
2310     INVOKE2(setPointerPosition, (x, y));
2311     const QPoint pos(x, y);
2312 #if QT_VERSION < QT_VERSION_CHECK(5,10,0)
2313     const auto screens = QApplication::screens();
2314     const auto found = std::find_if(
2315         std::begin(screens), std::end(screens),
2316         [pos](QScreen *screen) { return screen->geometry().contains(pos); });
2317     if (found == std::end(screens))
2318         return;
2319     QScreen *screen = *found;
2320 #else
2321     QScreen *screen = QGuiApplication::screenAt(pos);
2322 #endif
2323 
2324     if (screen)
2325         QCursor::setPos(screen, pos);
2326 }
2327 
pluginsPath()2328 QString ScriptableProxy::pluginsPath()
2329 {
2330     INVOKE(pluginsPath, ());
2331     return ::pluginsPath();
2332 }
2333 
themesPath()2334 QString ScriptableProxy::themesPath()
2335 {
2336     INVOKE(themesPath, ());
2337     return ::themesPath();
2338 }
2339 
translationsPath()2340 QString ScriptableProxy::translationsPath()
2341 {
2342     INVOKE(translationsPath, ());
2343     return ::translationsPath();
2344 }
2345 
iconColor()2346 QString ScriptableProxy::iconColor()
2347 {
2348     INVOKE(iconColor, ());
2349     const auto color = m_wnd->sessionIconColor();
2350     return color.isValid() ? color.name() : QString();
2351 }
2352 
setIconColor(const QString & colorName)2353 bool ScriptableProxy::setIconColor(const QString &colorName)
2354 {
2355     INVOKE(setIconColor, (colorName));
2356 
2357     QColor color(colorName);
2358     if ( !colorName.isEmpty() && !color.isValid() )
2359         return false;
2360 
2361     m_wnd->setSessionIconColor(color);
2362     return true;
2363 }
2364 
iconTag()2365 QString ScriptableProxy::iconTag()
2366 {
2367     INVOKE(iconTag, ());
2368     return m_wnd->sessionIconTag();
2369 }
2370 
setIconTag(const QString & tag)2371 void ScriptableProxy::setIconTag(const QString &tag)
2372 {
2373     INVOKE2(setIconTag, (tag));
2374     m_wnd->setSessionIconTag(tag);
2375 }
2376 
iconTagColor()2377 QString ScriptableProxy::iconTagColor()
2378 {
2379     INVOKE(iconTagColor, ());
2380     return m_wnd->sessionIconTagColor().name();
2381 }
2382 
setIconTagColor(const QString & colorName)2383 bool ScriptableProxy::setIconTagColor(const QString &colorName)
2384 {
2385     INVOKE(setIconTagColor, (colorName));
2386     QColor color(colorName);
2387     if ( !color.isValid() )
2388         return false;
2389 
2390     m_wnd->setSessionIconTagColor(color);
2391     return true;
2392 }
2393 
setClipboardData(const QVariantMap & data)2394 void ScriptableProxy::setClipboardData(const QVariantMap &data)
2395 {
2396     INVOKE2(setClipboardData, (data));
2397     m_wnd->setClipboardData(data);
2398 }
2399 
setTitle(const QString & title)2400 void ScriptableProxy::setTitle(const QString &title)
2401 {
2402     INVOKE2(setTitle, (title));
2403 
2404     if (title.isEmpty()) {
2405         m_wnd->setWindowTitle(QString());
2406         m_wnd->setTrayTooltip(QGuiApplication::applicationDisplayName());
2407     } else {
2408         m_wnd->setWindowTitle(title);
2409         m_wnd->setTrayTooltip(title);
2410     }
2411 }
2412 
setTitleForData(const QVariantMap & data)2413 void ScriptableProxy::setTitleForData(const QVariantMap &data)
2414 {
2415     INVOKE2(setTitleForData, (data));
2416 
2417     const QString clipboardContent = textLabelForData(data);
2418     setTitle(clipboardContent);
2419 }
2420 
saveData(const QString & tab,const QVariantMap & data,ClipboardMode mode)2421 void ScriptableProxy::saveData(const QString &tab, const QVariantMap &data, ClipboardMode mode)
2422 {
2423     INVOKE2(saveData, (tab, data, mode));
2424 
2425     auto c = m_wnd->tab(tab);
2426     if (c)
2427         c->addUnique(data, mode);
2428 }
2429 
showDataNotification(const QVariantMap & data)2430 void ScriptableProxy::showDataNotification(const QVariantMap &data)
2431 {
2432     INVOKE2(showDataNotification, (data));
2433 
2434     const AppConfig appConfig;
2435     const auto maxLines = appConfig.option<Config::clipboard_notification_lines>();
2436     if (maxLines <= 0)
2437         return;
2438 
2439     const auto intervalSeconds = appConfig.option<Config::item_popup_interval>();
2440     if (intervalSeconds == 0)
2441         return;
2442 
2443     auto notification = m_wnd->createNotification("CopyQ_clipboard_notification");
2444     notification->setIcon(IconPaste);
2445     notification->setInterval(intervalSeconds * 1000);
2446 
2447     const int maximumWidthPoints = appConfig.option<Config::notification_maximum_width>();
2448     const int width = pointsToPixels(maximumWidthPoints) - 16 - 8;
2449 
2450     const QStringList formats = data.keys();
2451     const int imageIndex = formats.indexOf(QRegularExpression("^image/.*"));
2452     const QFont &font = notification->widget()
2453         ? notification->widget()->font()
2454         : qApp->font();
2455     const bool isHidden = data.contains(mimeHidden);
2456 
2457     QString title;
2458 
2459     if (data.isEmpty()) {
2460         notification->setInterval(0);
2461     } if ( !isHidden && data.contains(mimeText) ) {
2462         QString text = getTextData(data);
2463         const int n = text.count('\n') + 1;
2464 
2465         if (n > 1) {
2466             title = QObject::tr("Text Copied (%n lines)",
2467                                  "Notification title for multi-line text in clipboard", n);
2468         } else {
2469             title = QObject::tr("Text Copied", "Notification title for single-line text in clipboard");
2470         }
2471 
2472         text = elideText(text, font, QString(), false, width, maxLines);
2473         notification->setMessage(text);
2474     } else if (!isHidden && imageIndex != -1) {
2475         QPixmap pix;
2476         const QString &imageFormat = formats[imageIndex];
2477         pix.loadFromData( data[imageFormat].toByteArray(), imageFormat.toLatin1() );
2478 
2479         const int height = maxLines * QFontMetrics(font).lineSpacing();
2480         if (pix.width() > width || pix.height() > height)
2481             pix = pix.scaled(QSize(width, height), Qt::KeepAspectRatio);
2482 
2483         notification->setPixmap(pix);
2484     } else {
2485         title = QObject::tr("Data Copied", "Notification title for a copied data in clipboard");
2486         const QString text = textLabelForData(data, font, QString(), false, width, maxLines);
2487         notification->setMessage(text);
2488     }
2489 
2490     notification->setTitle(title);
2491 }
2492 
enableMenuItem(int actionId,int currentRun,int menuItemMatchCommandIndex,const QVariantMap & menuItem)2493 bool ScriptableProxy::enableMenuItem(int actionId, int currentRun, int menuItemMatchCommandIndex, const QVariantMap &menuItem)
2494 {
2495     INVOKE(enableMenuItem, (actionId, currentRun, menuItemMatchCommandIndex, menuItem));
2496     return m_wnd->setMenuItemEnabled(actionId, currentRun, menuItemMatchCommandIndex, menuItem);
2497 }
2498 
setDisplayData(int actionId,const QVariantMap & displayData)2499 QVariantMap ScriptableProxy::setDisplayData(int actionId, const QVariantMap &displayData)
2500 {
2501     INVOKE(setDisplayData, (actionId, displayData));
2502     m_actionData = m_wnd->setDisplayData(actionId, displayData);
2503     return m_actionData;
2504 }
2505 
automaticCommands()2506 QVector<Command> ScriptableProxy::automaticCommands()
2507 {
2508     INVOKE(automaticCommands, ());
2509     return m_wnd->automaticCommands();
2510 }
2511 
displayCommands()2512 QVector<Command> ScriptableProxy::displayCommands()
2513 {
2514     INVOKE(displayCommands, ());
2515     return m_wnd->displayCommands();
2516 }
2517 
scriptCommands()2518 QVector<Command> ScriptableProxy::scriptCommands()
2519 {
2520     INVOKE(scriptCommands, ());
2521     return m_wnd->scriptCommands();
2522 }
2523 
openUrls(const QStringList & urls)2524 bool ScriptableProxy::openUrls(const QStringList &urls)
2525 {
2526     INVOKE(openUrls, (urls));
2527 
2528     for (const auto &url : urls) {
2529         if ( !QDesktopServices::openUrl(QUrl(url)) )
2530             return false;
2531     }
2532 
2533     return true;
2534 }
2535 
loadTheme(const QString & path)2536 QString ScriptableProxy::loadTheme(const QString &path)
2537 {
2538     INVOKE(loadTheme, (path));
2539 
2540     {
2541         const QFileInfo fileInfo(path);
2542         if ( !fileInfo.isFile() || !fileInfo.isReadable() )
2543             return "Failed to read theme";
2544     }
2545 
2546     const QSettings settings(path, QSettings::IniFormat);
2547     if ( settings.status() != QSettings::NoError )
2548         return "Failed to load theme";
2549 
2550     m_wnd->loadTheme(settings);
2551     if ( settings.status() != QSettings::NoError )
2552         return "Failed to parse theme";
2553 
2554     return QString();
2555 }
2556 
getClipboardData(const QString & mime,ClipboardMode mode)2557 QByteArray ScriptableProxy::getClipboardData(const QString &mime, ClipboardMode mode)
2558 {
2559     INVOKE(getClipboardData, (mime, mode));
2560 
2561     const QMimeData *data = m_wnd->getClipboardData(mode);
2562     if (!data)
2563         return QByteArray();
2564 
2565     if (mime == "?")
2566         return data->formats().join("\n").toUtf8() + '\n';
2567 
2568     return cloneData(*data, QStringList(mime)).value(mime).toByteArray();
2569 }
2570 
hasClipboardFormat(const QString & mime,ClipboardMode mode)2571 bool ScriptableProxy::hasClipboardFormat(const QString &mime, ClipboardMode mode)
2572 {
2573     INVOKE(hasClipboardFormat, (mime, mode));
2574 
2575     const QMimeData *data = m_wnd->getClipboardData(mode);
2576     return data && data->hasFormat(mime);
2577 }
2578 
styles()2579 QStringList ScriptableProxy::styles()
2580 {
2581     INVOKE(styles, ());
2582     return QStyleFactory::keys();
2583 }
2584 
fetchBrowser(const QString & tabName)2585 ClipboardBrowser *ScriptableProxy::fetchBrowser(const QString &tabName)
2586 {
2587     if (tabName.isEmpty()) {
2588         const QString defaultTabName = m_actionData.value(mimeCurrentTab).toString();
2589         if (!defaultTabName.isEmpty())
2590             return fetchBrowser(defaultTabName);
2591     }
2592 
2593     return tabName.isEmpty() ? m_wnd->browser(0) : m_wnd->tab(tabName);
2594 }
2595 
itemData(const QString & tabName,int i)2596 QVariantMap ScriptableProxy::itemData(const QString &tabName, int i)
2597 {
2598     auto c = fetchBrowser(tabName);
2599     return c ? c->copyIndex( c->index(i) ) : QVariantMap();
2600 }
2601 
itemData(const QString & tabName,int i,const QString & mime)2602 QByteArray ScriptableProxy::itemData(const QString &tabName, int i, const QString &mime)
2603 {
2604     const QVariantMap data = itemData(tabName, i);
2605     if ( data.isEmpty() )
2606         return QByteArray();
2607 
2608     if (mime == "?")
2609         return QStringList(data.keys()).join("\n").toUtf8() + '\n';
2610 
2611     if (mime == mimeItems)
2612         return serializeData(data);
2613 
2614     return data.value(mime).toByteArray();
2615 }
2616 
currentBrowser() const2617 ClipboardBrowser *ScriptableProxy::currentBrowser() const
2618 {
2619     const QString currentTabName = m_actionData.value(mimeCurrentTab).toString();
2620     if (currentTabName.isEmpty())
2621         return nullptr;
2622 
2623     const int i = m_wnd->findTabIndex(currentTabName);
2624     if (i == -1)
2625         return nullptr;
2626 
2627     auto c = m_wnd->browser(i);
2628     Q_ASSERT(c->tabName() == currentTabName);
2629     return c;
2630 }
2631 
selectedIndexes() const2632 QList<QPersistentModelIndex> ScriptableProxy::selectedIndexes() const
2633 {
2634     return m_actionData.value(mimeSelectedItems)
2635             .value< QList<QPersistentModelIndex> >();
2636 }
2637 
waitForFunctionCallFinished(int functionCallId)2638 QVariant ScriptableProxy::waitForFunctionCallFinished(int functionCallId)
2639 {
2640     if (m_disconnected)
2641         return QVariant();
2642 
2643     QVariant result;
2644 
2645     QEventLoop loop;
2646     connect(this, &ScriptableProxy::functionCallFinished, &loop,
2647             [&](int receivedFunctionCallId, const QVariant &returnValue) {
2648                 if (receivedFunctionCallId != functionCallId)
2649                     return;
2650                 result = returnValue;
2651                 loop.quit();
2652             });
2653     connect(this, &ScriptableProxy::abortEvaluation, &loop, &QEventLoop::quit);
2654 
2655     connect(qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
2656     loop.exec();
2657 
2658     return result;
2659 }
2660 
2661 #ifdef HAS_TESTS
keyClicker()2662 KeyClicker *ScriptableProxy::keyClicker()
2663 {
2664     if (!m_keyClicker)
2665         m_keyClicker = new KeyClicker(m_wnd, this);
2666     return m_keyClicker;
2667 }
2668 #endif // HAS_TESTS
2669 
pluginsPath()2670 QString pluginsPath()
2671 {
2672     QDir dir;
2673     if (platformNativeInterface()->findPluginDir(&dir))
2674         return dir.absolutePath();
2675 
2676     return QString();
2677 }
2678 
themesPath()2679 QString themesPath()
2680 {
2681     return platformNativeInterface()->themePrefix();
2682 }
2683 
translationsPath()2684 QString translationsPath()
2685 {
2686     return platformNativeInterface()->translationPrefix();
2687 }
2688