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 "mainwindow.h"
21 #include "ui_mainwindow.h"
22 
23 #include "common/action.h"
24 #include "common/actionoutput.h"
25 #include "common/appconfig.h"
26 #include "common/common.h"
27 #include "common/command.h"
28 #include "common/commandstore.h"
29 #include "common/config.h"
30 #include "common/contenttype.h"
31 #include "common/display.h"
32 #include "common/log.h"
33 #include "common/mimetypes.h"
34 #include "common/shortcuts.h"
35 #include "common/tabs.h"
36 #include "common/textdata.h"
37 #include "common/timer.h"
38 #include "gui/aboutdialog.h"
39 #include "gui/actiondialog.h"
40 #include "gui/actionhandler.h"
41 #include "gui/clipboardbrowser.h"
42 #include "gui/clipboardbrowserplaceholder.h"
43 #include "gui/clipboardbrowsershared.h"
44 #include "gui/clipboarddialog.h"
45 #include "gui/commandaction.h"
46 #include "gui/commanddialog.h"
47 #include "gui/configurationmanager.h"
48 #include "gui/importexportdialog.h"
49 #include "gui/iconfactory.h"
50 #include "gui/iconfactory.h"
51 #include "gui/iconselectdialog.h"
52 #include "gui/icons.h"
53 #include "gui/logdialog.h"
54 #include "gui/notification.h"
55 #include "gui/notificationbutton.h"
56 #include "gui/notificationdaemon.h"
57 #include "gui/tabdialog.h"
58 #include "gui/tabicons.h"
59 #include "gui/tabwidget.h"
60 #include "gui/theme.h"
61 #include "gui/traymenu.h"
62 #include "gui/windowgeometryguard.h"
63 #include "item/itemfactory.h"
64 #include "item/serialize.h"
65 #include "platform/platformclipboard.h"
66 #include "platform/platformnativeinterface.h"
67 #include "platform/platformwindow.h"
68 
69 #ifdef Q_OS_MAC
70 #  include "platform/mac/foregroundbackgroundfilter.h"
71 #endif
72 
73 #include <QAction>
74 #include <QCloseEvent>
75 #include <QDesktopServices>
76 #include <QFile>
77 #include <QFileDialog>
78 #include <QFlags>
79 #include <QMenu>
80 #include <QMenuBar>
81 #include <QMessageBox>
82 #include <QMimeData>
83 #include <QModelIndex>
84 #include <QPushButton>
85 #include <QTemporaryFile>
86 #include <QTimer>
87 #include <QToolBar>
88 #include <QUrl>
89 
90 #include <algorithm>
91 #include <memory>
92 
93 namespace {
94 
95 const int contextMenuUpdateIntervalMsec = 100;
96 const int itemPreviewUpdateIntervalMsec = 100;
97 
98 const QLatin1String menuItemKeyColor("color");
99 const QLatin1String menuItemKeyIcon("icon");
100 const QLatin1String menuItemKeyTag("tag");
101 
iconClipboard()102 const QIcon iconClipboard() { return getIcon("clipboard", IconPaste); }
iconTabIcon()103 const QIcon iconTabIcon() { return getIconFromResources("tab_icon"); }
iconTabNew()104 const QIcon iconTabNew() { return getIconFromResources("tab_new"); }
iconTabRemove()105 const QIcon iconTabRemove() { return getIconFromResources("tab_remove"); }
iconTabRename()106 const QIcon iconTabRename() { return getIconFromResources("tab_rename"); }
107 
108 const char propertyWidgetSizeGuarded[] = "CopyQ_widget_size_guarded";
109 
110 const char propertyActionFilterCommandFailed[] = "CopyQ_action_filter_command_failed";
111 
112 /// Omit size changes of a widget.
113 class WidgetSizeGuard final : public QObject {
114 public:
WidgetSizeGuard(QWidget * guardedObject)115     explicit WidgetSizeGuard(QWidget *guardedObject)
116         : m_guarded(guardedObject)
117     {
118         if ( m_guarded->property(propertyWidgetSizeGuarded).toBool() ) {
119             m_guarded = nullptr;
120         } else {
121             m_guarded->setProperty(propertyWidgetSizeGuarded, true);
122             m_guarded->setFixedSize( m_guarded->size() );
123         }
124     }
125 
~WidgetSizeGuard()126     ~WidgetSizeGuard()
127     {
128         if (m_guarded) {
129             m_guarded->setFixedSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
130             m_guarded->adjustSize();
131             m_guarded->resize( m_guarded->sizeHint() );
132             m_guarded->setProperty(propertyWidgetSizeGuarded, false);
133         }
134     }
135 
136 private:
137     QWidget *m_guarded;
138 };
139 
canPaste()140 bool canPaste()
141 {
142     return !QApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier);
143 }
144 
matchData(const QRegularExpression & re,const QVariantMap & data,const QString & format)145 bool matchData(const QRegularExpression &re, const QVariantMap &data, const QString &format)
146 {
147     if ( re.pattern().isEmpty() )
148         return true;
149 
150     const QString text = getTextData(data, format);
151     return text.contains(re);
152 }
153 
canExecuteCommand(const Command & command,const QVariantMap & data,const QString & sourceTabName)154 bool canExecuteCommand(const Command &command, const QVariantMap &data, const QString &sourceTabName)
155 {
156     // Verify that an action is provided.
157     if ( command.cmd.isEmpty() && !command.remove
158          && (command.tab.isEmpty() || command.tab == sourceTabName) )
159     {
160         return false;
161     }
162 
163     // Verify that data for given MIME is available.
164     if ( !command.input.isEmpty() ) {
165         if (command.input == mimeItems || command.input == "!OUTPUT") {
166             // Disallow applying action that takes serialized item more times.
167             if ( data.contains(command.output) )
168                 return false;
169         } else if ( !data.contains(command.input) ) {
170             return false;
171         }
172     }
173 
174     // Verify that and text matches given regexp.
175     if ( !matchData(command.re, data, mimeText) )
176         return false;
177 
178     // Verify that window title matches given regexp.
179     if ( !matchData(command.wndre, data, mimeWindowTitle) )
180         return false;
181 
182     return true;
183 }
184 
stealFocus(const QWidget & window)185 void stealFocus(const QWidget &window)
186 {
187     WId wid = window.winId();
188     PlatformWindowPtr platformWindow = platformNativeInterface()->getWindow(wid);
189     if (platformWindow)
190         platformWindow->raise();
191 }
192 
193 template <typename WidgetOrAction>
disableActionWhenTabGroupSelected(WidgetOrAction * action,MainWindow * window)194 void disableActionWhenTabGroupSelected(WidgetOrAction *action, MainWindow *window)
195 {
196     QObject::connect( window, &MainWindow::tabGroupSelected,
197                       action, &WidgetOrAction::setDisabled );
198 }
199 
200 /// Adds information about current tab and selection if command is triggered by user.
addSelectionData(const ClipboardBrowser & c,const QModelIndex & currentIndex,const QModelIndexList & selectedIndexes)201 QVariantMap addSelectionData(
202         const ClipboardBrowser &c,
203         const QModelIndex &currentIndex,
204         const QModelIndexList &selectedIndexes)
205 {
206     auto result = c.copyIndexes(selectedIndexes);
207 
208     result.insert(mimeCurrentTab, c.tabName());
209 
210     if ( currentIndex.isValid() ) {
211         const QPersistentModelIndex current = currentIndex;
212         result.insert(mimeCurrentItem, QVariant::fromValue(current));
213     }
214 
215     if ( !selectedIndexes.isEmpty() ) {
216         QList<QPersistentModelIndex> selected;
217         selected.reserve(selectedIndexes.size());
218         for (const auto &index : selectedIndexes)
219             selected.append(index);
220         std::sort(selected.begin(), selected.end());
221         result.insert(mimeSelectedItems, QVariant::fromValue(selected));
222     }
223 
224     return result;
225 }
226 
addSelectionData(const ClipboardBrowser & c)227 QVariantMap addSelectionData(const ClipboardBrowser &c)
228 {
229     const QModelIndexList selectedIndexes = c.selectionModel()->selectedIndexes();
230     const auto current = c.selectionModel()->currentIndex();
231     return addSelectionData(c, current, selectedIndexes);
232 }
233 
findSubMenu(const QString & name,const QMenu & menu)234 QMenu *findSubMenu(const QString &name, const QMenu &menu)
235 {
236     for (auto action : menu.actions()) {
237         QMenu *subMenu = action->menu();
238         if (subMenu && subMenu->title() == name)
239             return subMenu;
240     }
241 
242     return nullptr;
243 }
244 
createSubMenus(QString * name,QMenu * menu)245 QMenu *createSubMenus(QString *name, QMenu *menu)
246 {
247     QStringList path = name->split('|');
248     if (path.size() == 1)
249         return menu;
250 
251     *name = path.takeLast();
252 
253     QMenu *parentMenu = menu;
254 
255     for (const auto &subMenuName : path) {
256         QMenu *subMenu = findSubMenu(subMenuName, *parentMenu);
257 
258         if (!subMenu) {
259             subMenu = new QMenu(subMenuName, parentMenu);
260             parentMenu->addMenu(subMenu);
261         }
262 
263         parentMenu = subMenu;
264     }
265 
266     return parentMenu;
267 }
268 
269 // WORKAROUND: setWindowFlags() hides the window.
270 // See: https://doc.qt.io/qt-5/qwidget.html#windowFlags-prop
setWindowFlag(QWidget * window,Qt::WindowType flag,bool enable)271 bool setWindowFlag(QWidget *window, Qt::WindowType flag, bool enable)
272 {
273     if (!window)
274         return false;
275 
276     const Qt::WindowFlags flags = window->windowFlags();
277     const bool wasEnabled = flags.testFlag(flag);
278     if (wasEnabled == enable)
279         return false;
280 
281     const bool wasVisible = window->isVisible();
282     const bool wasActive = window->isActiveWindow();
283     window->setWindowFlags(flags ^ flag);
284     if (wasVisible) {
285         if (wasActive) {
286             window->show();
287         } else {
288             const bool showWithoutActivating = window->testAttribute(Qt::WA_ShowWithoutActivating);
289             window->setAttribute(Qt::WA_ShowWithoutActivating);
290             window->show();
291             window->setAttribute(Qt::WA_ShowWithoutActivating, showWithoutActivating);
292         }
293 
294         if (wasActive) {
295             window->raise();
296             window->activateWindow();
297             QApplication::setActiveWindow(window);
298             stealFocus(*window);
299         }
300     }
301 
302     return true;
303 }
304 
setAlwaysOnTop(QWidget * window,bool alwaysOnTop)305 void setAlwaysOnTop(QWidget *window, bool alwaysOnTop)
306 {
307     if ( setWindowFlag(window, Qt::WindowStaysOnTopHint, alwaysOnTop) ) {
308         // Workaround for QTBUG-28601.
309         window->setAcceptDrops(true);
310     }
311 }
312 
setHideInTaskBar(QWidget * window,bool hideInTaskBar)313 void setHideInTaskBar(QWidget *window, bool hideInTaskBar)
314 {
315     setWindowFlag(window, Qt::Tool, hideInTaskBar);
316 }
317 
318 template<typename Dialog, typename ...Ts>
319 Dialog *openDialog(Ts... arguments)
320 {
321     std::unique_ptr<Dialog> dialog( new Dialog(arguments...) );
322     WindowGeometryGuard::create( dialog.get() );
323     dialog->setAttribute(Qt::WA_DeleteOnClose, true);
324     dialog->setWindowIcon(appIcon());
325     dialog->activateWindow();
326     dialog->show();
327     return dialog.release();
328 }
329 
isItemActivationShortcut(const QKeySequence & shortcut)330 bool isItemActivationShortcut(const QKeySequence &shortcut)
331 {
332     return (shortcut[0] == Qt::Key_Return || shortcut[0] == Qt::Key_Enter)
333             && shortcut[1] == 0
334             && shortcut[2] == 0
335             && shortcut[3] == 0;
336 }
337 
importExportFileDialogFilter()338 QString importExportFileDialogFilter()
339 {
340     return MainWindow::tr("CopyQ Items (*.cpq)");
341 }
342 
343 /**
344  * Returns QVariant value that can be serialized and deserialized with QDataStream.
345  *
346  * WORKAROUND: Invalid QVariant() can be serialized but deserialization fails.
347  */
serializableValue(const QSettings & settings,const QString & key)348 QVariant serializableValue(const QSettings &settings, const QString &key)
349 {
350     const auto value = settings.value(key);
351     if ( value.isValid() )
352         return value;
353 
354     return QString();
355 }
356 
isAnyApplicationWindowActive()357 bool isAnyApplicationWindowActive()
358 {
359     if ( qApp->activeWindow() )
360         return true;
361 
362     const auto platform = platformNativeInterface();
363     const auto currentWindow = platform->getCurrentWindow();
364     if (!currentWindow)
365         return false;
366 
367     const auto currentWindowTitle = currentWindow->getTitle();
368     if ( currentWindowTitle.isEmpty() )
369         return false;
370 
371     for ( auto window : qApp->topLevelWidgets() ) {
372         const auto ownWindow = platform->getWindow( window->winId() );
373         if ( ownWindow && currentWindowTitle == ownWindow->getTitle() )
374             return true;
375     }
376 
377     return false;
378 }
379 
hasDialogOpen(QWidget * parent)380 bool hasDialogOpen(QWidget *parent)
381 {
382     for ( auto window : qApp->topLevelWidgets() ) {
383         if ( window->isVisible() && window->parentWidget() == parent )
384             return true;
385     }
386 
387     return false;
388 }
389 
deleteSubMenus(QObject * parent)390 void deleteSubMenus(QObject *parent)
391 {
392     for (auto subMenu : parent->findChildren<QMenu*>()) {
393         if (subMenu->parent() == parent)
394             delete subMenu;
395     }
396 }
397 
clearActions(QMenu * menu)398 void clearActions(QMenu *menu)
399 {
400     for (QAction *action : menu->actions()) {
401         action->setVisible(false);
402         action->setDisabled(true);
403         action->setShortcuts({});
404         action->deleteLater();
405         menu->removeAction(action);
406     }
407 
408     deleteSubMenus(menu);
409     menu->clear();
410 }
411 
clearActions(QToolBar * toolBar)412 void clearActions(QToolBar *toolBar)
413 {
414     for (QAction *action : toolBar->actions()) {
415         // Omit removing action from other menus.
416         if (action->parent() == toolBar)
417             delete action;
418     }
419 
420     deleteSubMenus(toolBar);
421     toolBar->clear();
422 }
423 
hasCommandFuzzy(const QVector<Command> & commands,const Command & command)424 bool hasCommandFuzzy(const QVector<Command> &commands, const Command &command)
425 {
426     return std::any_of(std::begin(commands), std::end(commands), [&command](const Command &cmd){
427         return command.name == cmd.name || command.cmd == cmd.cmd;
428     });
429 }
430 
431 } // namespace
432 
433 class ToolBar final : public QToolBar {
434 public:
ToolBar(QWidget * parent)435     explicit ToolBar(QWidget *parent)
436         : QToolBar(parent)
437     {
438         setObjectName("toolBar");
439         setContextMenuPolicy(Qt::PreventContextMenu);
440         initSingleShotTimer( &m_timerUnfreeze, 50, this, &ToolBar::unfreeze );
441     }
442 
setFrozen(bool frozen)443     void setFrozen(bool frozen) {
444         if (frozen) {
445             m_frozen = true;
446             setUpdatesEnabled(false);
447             setEnabled(false);
448             m_timerUnfreeze.stop();
449         } else {
450             m_timerUnfreeze.start();
451         }
452     }
453 
454 protected:
paintEvent(QPaintEvent * ev)455     void paintEvent(QPaintEvent *ev) override
456     {
457         if (m_frozen)
458             return;
459 
460         QToolBar::paintEvent(ev);
461     }
462 
463 private:
unfreeze()464     void unfreeze()
465     {
466         m_frozen = false;
467         setEnabled(true);
468         setUpdatesEnabled(true);
469         update();
470     }
471 
472     bool m_frozen = false;
473     QTimer m_timerUnfreeze;
474 };
475 
MainWindow(const ClipboardBrowserSharedPtr & sharedData,QWidget * parent)476 MainWindow::MainWindow(const ClipboardBrowserSharedPtr &sharedData, QWidget *parent)
477     : QMainWindow(parent)
478     , cm(nullptr)
479     , ui(new Ui::MainWindow)
480     , m_menuItem(nullptr)
481     , m_trayMenu( new TrayMenu(this) )
482     , m_tray(nullptr)
483     , m_toolBar(new ToolBar(this))
484     , m_actionToggleClipboardStoring()
485     , m_sharedData(sharedData)
486     , m_lastWindow()
487     , m_menu( new TrayMenu(this) )
488     , m_menuMaxItemCount(-1)
489     , m_commandDialog(nullptr)
490     , m_menuItems(menuItems())
491     , m_clipboard(platformNativeInterface()->clipboard())
492 {
493     ui->setupUi(this);
494 
495 #ifdef Q_OS_MAC
496     // Open above fullscreen windows on OS X.
497     setWindowModality(Qt::WindowModal);
498     setWindowFlag(Qt::Sheet);
499 #endif
500 
501     menuBar()->setObjectName("menu_bar");
502     createMenu();
503 
504     ui->tabWidget->addToolBars(this);
505     addToolBar(Qt::RightToolBarArea, m_toolBar);
506 
507     ui->dockWidgetItemPreview->setFocusProxy(ui->scrollAreaItemPreview);
508     ui->dockWidgetItemPreview->hide();
509 
510     WindowGeometryGuard::create(this);
511     restoreState( mainWindowState(objectName()) );
512     // NOTE: QWidget::isVisible() returns false if parent is not visible.
513     m_showItemPreview = !ui->dockWidgetItemPreview->isHidden();
514 
515     // Disable the show-preview option when the preview dock is closed.
516     connect( ui->dockWidgetItemPreview, &QDockWidget::visibilityChanged,
517              this, [this]() {
518                 if ( ui->dockWidgetItemPreview->isHidden() )
519                     setItemPreviewVisible(false);
520              } );
521 
522     updateIcon();
523 
524     updateFocusWindows();
525 
526     connect( m_trayMenu, &QMenu::aboutToShow,
527              this, &MainWindow::updateFocusWindows );
528     connect( m_trayMenu, &QMenu::aboutToShow,
529              this, &MainWindow::updateTrayMenuItemsTimeout );
530     connect( m_trayMenu, &QMenu::aboutToHide,
531              this, [this](){ m_timerRaiseLastWindowAfterMenuClosed.start(); } );
532     connect( m_trayMenu, &TrayMenu::searchRequest,
533              this, &MainWindow::filterTrayMenuItems );
534     connect( m_trayMenu, &TrayMenu::clipboardItemActionTriggered,
535              this, &MainWindow::onTrayActionTriggered );
536 
537     connect( m_menu, &QMenu::aboutToShow,
538              this, &MainWindow::updateFocusWindows );
539     connect( m_menu, &QMenu::aboutToHide,
540              this, [this](){ m_timerRaiseLastWindowAfterMenuClosed.start(); } );
541     connect( m_menu, &TrayMenu::searchRequest,
542              this, &MainWindow::filterMenuItems );
543     connect( m_menu, &TrayMenu::clipboardItemActionTriggered,
544              this, &MainWindow::onMenuActionTriggered );
545 
546     connect( ui->tabWidget, &TabWidget::currentChanged,
547              this, &MainWindow::tabChanged );
548     connect( ui->tabWidget, &TabWidget::tabMoved,
549              this, &MainWindow::saveTabPositions );
550     connect( ui->tabWidget, &TabWidget::tabsMoved,
551              this, &MainWindow::tabsMoved );
552     connect( ui->tabWidget, &TabWidget::tabBarMenuRequested,
553              this, &MainWindow::tabBarMenuRequested );
554     connect( ui->tabWidget, &TabWidget::tabTreeMenuRequested,
555              this, &MainWindow::tabTreeMenuRequested );
556     connect( ui->tabWidget, &TabWidget::tabRenamed,
557              this, &MainWindow::renameTab );
558     connect( ui->tabWidget, &TabWidget::tabCloseRequested,
559              this, &MainWindow::tabCloseRequested );
560     connect( ui->tabWidget, &TabWidget::dropItems,
561              this, &MainWindow::onTabWidgetDropItems);
562     connect( ui->searchBar, &Utils::FilterLineEdit::filterChanged,
563              this, &MainWindow::onFilterChanged );
564     connect( qApp, &QCoreApplication::aboutToQuit,
565              this, &MainWindow::onAboutToQuit );
566 
567     connect(m_sharedData->itemFactory, &ItemFactory::error,
568             this, &MainWindow::showError);
569     connect(m_sharedData->itemFactory, &ItemFactory::addCommands,
570             this, &MainWindow::addCommands);
571 
572     initSingleShotTimer( &m_timerUpdateFocusWindows, 100, this, &MainWindow::updateFocusWindows );
573     initSingleShotTimer( &m_timerUpdateContextMenu, 0, this, &MainWindow::updateContextMenuTimeout );
574     initSingleShotTimer( &m_timerUpdatePreview, 0, this, &MainWindow::updateItemPreviewTimeout );
575     initSingleShotTimer( &m_timerSaveTabPositions, 1000, this, &MainWindow::onSaveTabPositionsTimer );
576     initSingleShotTimer( &m_timerRaiseLastWindowAfterMenuClosed, 50, this, &MainWindow::raiseLastWindowAfterMenuClosed);
577     enableHideWindowOnUnfocus();
578 
579     m_trayMenu->setObjectName("TrayMenu");
580     m_menu->setObjectName("Menu");
581 
582     auto act = m_trayMenu->addAction( appIcon(), tr("&Show/Hide") );
583     connect(act, &QAction::triggered, this, &MainWindow::toggleVisible);
584     m_trayMenu->setDefaultAction(act);
585     addTrayAction(Actions::File_Preferences);
586     addTrayAction(Actions::File_ToggleClipboardStoring);
587     m_trayMenu->addSeparator();
588     addTrayAction(Actions::File_Exit);
589 }
590 
browseMode() const591 bool MainWindow::browseMode() const
592 {
593     return ui->searchBar->isHidden();
594 }
595 
exit()596 void MainWindow::exit()
597 {
598     // Check if not editing in any tab (show and try to close editors).
599     for ( int i = 0; i < ui->tabWidget->count(); ++i ) {
600         auto c = getPlaceholder(i)->browser();
601         if ( c && (c->isInternalEditorOpen() || c->isExternalEditorOpen()) ) {
602             setCurrentTab(i);
603             if ( !c->maybeCloseEditors() )
604                 return;
605         }
606     }
607 
608     int answer = QMessageBox::Yes;
609     if (m_options.confirmExit) {
610         showWindow();
611         answer = QMessageBox::question(
612                     this,
613                     tr("Exit?"),
614                     tr("Do you want to <strong>exit</strong> CopyQ?"),
615                     QMessageBox::Yes | QMessageBox::No,
616                     QMessageBox::Yes);
617     }
618 
619     if (answer == QMessageBox::Yes)
620         emit requestExit();
621 }
622 
closeEvent(QCloseEvent * event)623 void MainWindow::closeEvent(QCloseEvent *event)
624 {
625     event->ignore();
626     hideWindow();
627     COPYQ_LOG("Got main window close event.");
628 }
629 
focusNextPrevChild(bool next)630 bool MainWindow::focusNextPrevChild(bool next)
631 {
632     auto c = browser();
633     if (!c)
634         return false;
635 
636     // Fix tab order while searching in editor.
637     if (c->isInternalEditorOpen() && !browseMode()) {
638         if ( next && ui->searchBar->hasFocus() ) {
639             c->setFocus();
640             return true;
641         }
642 
643         if ( !next && c->hasFocus() ) {
644             ui->searchBar->setFocus();
645             return true;
646         }
647     }
648 
649     return QMainWindow::focusNextPrevChild(next);
650 }
651 
createMenu()652 void MainWindow::createMenu()
653 {
654     QMenuBar *menubar = menuBar();
655     QMenu *menu;
656     QAction *act;
657 
658     // Some action should not be triggered from tab widget or preview dock.
659     QWidget *actionParent = ui->tabWidget;
660 
661     menubar->clear();
662 
663     // File
664     menu = menubar->addMenu( tr("&File") );
665 
666     // - new
667     act = createAction( Actions::File_New, &MainWindow::editNewItem, menu );
668     disableActionWhenTabGroupSelected(act, this);
669 
670     // - import
671     createAction( Actions::File_Import, &MainWindow::importData, menu );
672 
673     // - export
674     createAction( Actions::File_Export, &MainWindow::exportData, menu );
675 
676     // - separator
677     menu->addSeparator();
678 
679     // - preferences
680     createAction( Actions::File_Preferences, &MainWindow::openPreferences, menu );
681 
682     // - commands
683     createAction( Actions::File_Commands, &MainWindow::openCommands, menu );
684 
685     // - separator
686     menu->addSeparator();
687 
688     // - show clipboard content
689     createAction( Actions::File_ShowClipboardContent, &MainWindow::showClipboardContent, menu );
690 
691     // - show preview
692     QAction *togglePreviewAction =
693             createAction( Actions::File_ShowPreview, &MainWindow::toggleItemPreviewVisible, menu );
694     togglePreviewAction->setCheckable(true);
695     togglePreviewAction->setChecked(m_showItemPreview);
696 
697     // - active commands
698     createAction( Actions::File_ProcessManager, &MainWindow::showProcessManagerDialog, menu );
699 
700     // - enable/disable
701     m_actionToggleClipboardStoring = createAction( Actions::File_ToggleClipboardStoring,
702                                                    &MainWindow::toggleClipboardStoring, menu );
703     updateMonitoringActions();
704 
705     // - separator
706     menu->addSeparator();
707 
708     // - exit
709     createAction( Actions::File_Exit, &MainWindow::exit, menu );
710 
711     // Edit
712     menu = menubar->addMenu( tr("&Edit") );
713 
714     // - find
715     createAction( Actions::Edit_FindItems, &MainWindow::findNextOrPrevious, menu, actionParent );
716 
717     // - separator
718     menu->addSeparator();
719 
720     // - sort
721     createAction( Actions::Edit_SortSelectedItems, &MainWindow::sortSelectedItems, menu, actionParent );
722 
723     // - reverse order
724     createAction( Actions::Edit_ReverseSelectedItems, &MainWindow::reverseSelectedItems, menu, actionParent );
725 
726     // - separator
727     menu->addSeparator();
728 
729     // - paste items
730     createAction( Actions::Edit_PasteItems, &MainWindow::pasteItems, menu, actionParent );
731 
732     // - copy items
733     createAction( Actions::Edit_CopySelectedItems, &MainWindow::copyItems, menu, actionParent );
734 
735     // Items
736     m_menuItem = menubar->addMenu( tr("&Item") );
737     disableActionWhenTabGroupSelected(m_menuItem, this);
738 
739     // Tabs
740     menu = menubar->addMenu(tr("&Tabs"));
741 
742     // - new tab
743     createAction( Actions::Tabs_NewTab, &MainWindow::openNewTabDialog, menu );
744 
745     // - rename tab
746     act = createAction( Actions::Tabs_RenameTab, &MainWindow::openRenameTabDialog, menu );
747     disableActionWhenTabGroupSelected(act, this);
748 
749     // - remove tab
750     act = createAction( Actions::Tabs_RemoveTab, &MainWindow::removeTab, menu );
751     disableActionWhenTabGroupSelected(act, this);
752 
753     createAction( Actions::Tabs_ChangeTabIcon, &MainWindow::setTabIcon, menu );
754 
755     // - separator
756     menu->addSeparator();
757 
758     // - next tab
759     createAction( Actions::Tabs_NextTab, &MainWindow::nextTab, menu );
760 
761     // - previous tab
762     createAction( Actions::Tabs_PreviousTab, &MainWindow::previousTab, menu );
763 
764     // Help
765     menu = menubar->addMenu(tr("&Help"));
766     createAction( Actions::Help_Help, &MainWindow::openHelp, menu );
767     createAction( Actions::Help_ShowLog, &MainWindow::openLogDialog, menu );
768     createAction( Actions::Help_About, &MainWindow::openAboutDialog, menu );
769 
770     // Open Item Menu
771     createAction( Actions::ItemMenu, &MainWindow::showContextMenu, nullptr );
772 
773     for (auto subMenu : menuBar()->findChildren<QMenu*>()) {
774         connect( subMenu, &QMenu::aboutToShow,
775                  this, &MainWindow::disableHideWindowOnUnfocus );
776         connect( subMenu, &QMenu::aboutToHide,
777                  this, &MainWindow::enableHideWindowOnUnfocus );
778     }
779 }
780 
popupTabBarMenu(QPoint pos,const QString & tab)781 void MainWindow::popupTabBarMenu(QPoint pos, const QString &tab)
782 {
783     QMenu menu(ui->tabWidget);
784 
785     const int tabIndex = ui->tabWidget->tabs().indexOf(tab);
786     bool hasTab = tabIndex != -1;
787     bool isGroup = ui->tabWidget->isTabGroup(tab);
788 
789     const QString quotedTab = quoteString(tab);
790     QAction *actNew = menu.addAction( iconTabNew(), tr("&New Tab") );
791     QAction *actRenameGroup =
792             isGroup ? menu.addAction( iconTabRename(), tr("Rename &Group %1").arg(quotedTab) ) : nullptr;
793     QAction *actRename =
794             hasTab ? menu.addAction( iconTabRename(), tr("Re&name Tab %1").arg(quotedTab) ) : nullptr;
795     QAction *actRemove =
796             hasTab ? menu.addAction( iconTabRemove(), tr("Re&move Tab %1").arg(quotedTab) ) : nullptr;
797     QAction *actRemoveGroup =
798             isGroup ? menu.addAction( iconTabRemove(), tr("Remove Group %1").arg(quotedTab) ) : nullptr;
799 
800     QAction *actIcon = menu.addAction( iconTabIcon(), tr("&Change Tab Icon") );
801 
802     QAction *act = menu.exec(pos);
803     if (act != nullptr) {
804         if (act == actNew)
805             openNewTabDialog(tab);
806         else if (act == actRenameGroup)
807             openRenameTabGroupDialog(tab);
808         else if (act == actRename)
809             openRenameTabDialog(tabIndex);
810         else if (act == actRemove)
811             removeTab(true, tabIndex);
812         else if (act == actRemoveGroup)
813             removeTabGroup(tab);
814         else if (act == actIcon)
815             setTabIcon(tab);
816     }
817 }
818 
updateContextMenu(int intervalMsec)819 void MainWindow::updateContextMenu(int intervalMsec)
820 {
821     interruptMenuCommandFilters(&m_itemMenuMatchCommands);
822 
823     // Omit tool bar flickering.
824     m_toolBar->setFrozen(true);
825 
826     clearActions(m_menuItem);
827 
828     m_timerUpdateContextMenu.start(intervalMsec);
829 }
830 
updateTrayMenuItems()831 void MainWindow::updateTrayMenuItems()
832 {
833     m_trayMenuDirty = true;
834 }
835 
updateTrayMenuCommands()836 void MainWindow::updateTrayMenuCommands()
837 {
838     m_trayMenu->clearCustomActions();
839 
840     if (!m_options.trayCommands)
841         return;
842 
843     const QString format = tr("&Clipboard: %1", "Tray menu clipboard item format");
844     const auto font = m_trayMenu->font();
845     const auto clipboardLabel = textLabelForData(m_clipboardData, font, format, true);
846 
847     QAction *clipboardAction = m_trayMenu->addAction( iconClipboard(), clipboardLabel );
848     connect(clipboardAction, &QAction::triggered,
849             this, &MainWindow::showClipboardContent);
850     m_trayMenu->addCustomAction(clipboardAction);
851 
852     m_trayMenu->markItemInClipboard(m_clipboardData);
853 
854     int i = m_trayMenu->actions().size();
855     addCommandsToTrayMenu(m_clipboardData);
856     QList<QAction *> actions = m_trayMenu->actions();
857     for ( ; i < actions.size(); ++i )
858         m_trayMenu->addCustomAction(actions[i]);
859 }
860 
updateIcon()861 void MainWindow::updateIcon()
862 {
863     const QIcon icon = appIcon();
864     setWindowIcon(icon);
865     if (m_tray)
866         m_tray->setIcon(icon);
867 }
868 
updateContextMenuTimeout()869 void MainWindow::updateContextMenuTimeout()
870 {
871     auto c = browserOrNull();
872     if ( ui->tabWidget->isTabGroupSelected() || !c || c->isInternalEditorOpen()) {
873         clearActions(m_toolBar);
874         m_toolBar->setFrozen(false);
875         return;
876     }
877 
878     addCommandsToItemMenu(c);
879 
880     m_menuItem->addSeparator();
881 
882     addItemAction( Actions::Item_MoveToClipboard, c, &ClipboardBrowser::moveToClipboard );
883     addItemAction( Actions::Item_ShowContent, this, &MainWindow::showItemContent );
884     addItemAction( Actions::Item_Remove, c, &ClipboardBrowser::remove );
885     addItemAction( Actions::Item_Edit, c, &ClipboardBrowser::editSelected );
886     addItemAction( Actions::Item_EditNotes, c, &ClipboardBrowser::editNotes );
887     addItemAction( Actions::Item_EditWithEditor, c, &ClipboardBrowser::openEditor );
888     addItemAction( Actions::Item_Action, this, &MainWindow::openActionDialog );
889 
890     m_menuItem->addSeparator();
891 
892     addItemAction( Actions::Item_MoveUp, this, &MainWindow::moveUp );
893     addItemAction( Actions::Item_MoveDown, this, &MainWindow::moveDown );
894     addItemAction( Actions::Item_MoveToTop, this, &MainWindow::moveToTop );
895     addItemAction( Actions::Item_MoveToBottom, this, &MainWindow::moveToBottom );
896 
897     updateToolBar();
898     updateActionShortcuts();
899 }
900 
updateItemPreviewAfterMs(int ms)901 void MainWindow::updateItemPreviewAfterMs(int ms)
902 {
903     m_timerUpdatePreview.start(ms);
904 }
905 
updateItemPreviewTimeout()906 void MainWindow::updateItemPreviewTimeout()
907 {
908     const bool showPreview = m_showItemPreview;
909 
910     auto c = browserOrNull();
911     if (c && c->length() > 0) {
912         ui->dockWidgetItemPreview->setVisible(m_showItemPreview && !c->isInternalEditorOpen());
913 
914         QWidget *w = ui->dockWidgetItemPreview->isVisible() && !ui->tabWidget->isTabGroupSelected()
915                 ? c->currentItemPreview(ui->scrollAreaItemPreview)
916                 : nullptr;
917 
918         ui->scrollAreaItemPreview->setVisible(w != nullptr);
919         ui->scrollAreaItemPreview->setWidget(w);
920         if (w) {
921             ui->dockWidgetItemPreview->setStyleSheet( c->styleSheet() );
922             w->show();
923         }
924     } else {
925         ui->dockWidgetItemPreview->hide();
926     }
927 
928     m_showItemPreview = showPreview;
929     m_timerUpdatePreview.stop();
930 }
931 
toggleItemPreviewVisible()932 void MainWindow::toggleItemPreviewVisible()
933 {
934     setItemPreviewVisible(!m_showItemPreview);
935 }
936 
setItemPreviewVisible(bool visible)937 void MainWindow::setItemPreviewVisible(bool visible)
938 {
939     if (m_showItemPreview == visible)
940         return;
941 
942     m_showItemPreview = visible;
943     updateItemPreviewAfterMs(0);
944 }
945 
isItemPreviewVisible() const946 bool MainWindow::isItemPreviewVisible() const
947 {
948     return m_showItemPreview;
949 }
950 
onAboutToQuit()951 void MainWindow::onAboutToQuit()
952 {
953     if (cm)
954         cm->disconnect();
955 
956     saveMainWindowState( objectName(), saveState() );
957     hideWindow();
958     if (m_tray)
959         m_tray->hide();
960 
961     stopMenuCommandFilters(&m_itemMenuMatchCommands);
962     stopMenuCommandFilters(&m_trayMenuMatchCommands);
963     terminateAction(&m_displayActionId);
964 }
965 
onSaveCommand(const Command & command)966 void MainWindow::onSaveCommand(const Command &command)
967 {
968     auto commands = loadAllCommands();
969     commands.append(command);
970     setCommands(commands);
971 }
972 
onItemCommandActionTriggered(CommandAction * commandAction,const QString & triggeredShortcut)973 void MainWindow::onItemCommandActionTriggered(CommandAction *commandAction, const QString &triggeredShortcut)
974 {
975     COPYQ_LOG( QString("Trigger: %1").arg(commandAction->text()) );
976     auto c = getPlaceholder()->createBrowser();
977     if (!c)
978         return;
979 
980     const QModelIndexList selected = c->selectionModel()->selectedIndexes();
981 
982     const auto command = commandAction->command();
983 
984     if ( !command.cmd.isEmpty() ) {
985         if (command.transform) {
986             for (const auto &index : selected) {
987                 const auto selection = QModelIndexList() << index;
988                 auto actionData = addSelectionData(*c, index, selection);
989                 if ( !triggeredShortcut.isEmpty() )
990                     actionData.insert(mimeShortcut, triggeredShortcut);
991                 action(actionData, command, index);
992             }
993         } else {
994             auto actionData = addSelectionData(*c);
995             if ( !triggeredShortcut.isEmpty() )
996                 actionData.insert(mimeShortcut, triggeredShortcut);
997             action(actionData, command, QModelIndex());
998         }
999     }
1000 
1001     if ( !command.tab.isEmpty() && command.tab != c->tabName() ) {
1002         auto c2 = tab(command.tab);
1003         if (c2) {
1004             for (int i = selected.size() - 1; i >= 0; --i) {
1005                 const auto data = c->copyIndex(selected[i]);
1006                 if ( !data.isEmpty() )
1007                     c2->addUnique(data, ClipboardMode::Clipboard);
1008             }
1009         }
1010     }
1011 
1012     if (command.remove) {
1013         const int lastRow = c->removeIndexes(selected);
1014         if (lastRow != -1)
1015             c->setCurrent(lastRow);
1016     }
1017 
1018     if (command.hideWindow)
1019         hideWindow();
1020 }
1021 
onClipboardCommandActionTriggered(CommandAction * commandAction,const QString & triggeredShortcut)1022 void MainWindow::onClipboardCommandActionTriggered(CommandAction *commandAction, const QString &triggeredShortcut)
1023 {
1024     const QMimeData *data = m_clipboard->mimeData(ClipboardMode::Clipboard);
1025     if (data == nullptr)
1026         return;
1027 
1028     auto actionData = cloneData(*data);
1029     if ( !triggeredShortcut.isEmpty() )
1030         actionData.insert(mimeShortcut, triggeredShortcut);
1031 
1032     auto command = commandAction->command();
1033 
1034     action( actionData, command, QModelIndex() );
1035 }
1036 
onTabWidgetDropItems(const QString & tabName,const QMimeData * data)1037 void MainWindow::onTabWidgetDropItems(const QString &tabName, const QMimeData *data)
1038 {
1039     auto browser = tab(tabName);
1040 
1041     if (browser) {
1042         const QVariantMap dataMap = data->hasFormat(mimeItems)
1043                 ? cloneData(*data, QStringList() << mimeItems) : cloneData(*data);
1044         browser->addAndSelect(dataMap, 0);
1045     }
1046 }
1047 
showContextMenuAt(QPoint position)1048 void MainWindow::showContextMenuAt(QPoint position)
1049 {
1050     // Restrict menu position to central widget.
1051     const auto localRect = centralWidget()->rect();
1052     const auto rect = QRect(
1053         centralWidget()->mapToGlobal(localRect.topLeft()),
1054         centralWidget()->mapToGlobal(localRect.bottomRight())
1055     );
1056     const QPoint positionInCentralWidget(
1057         qBound(rect.left(), position.x(), rect.right()),
1058         qBound(rect.top(), position.y(), rect.bottom())
1059     );
1060 
1061     m_menuItem->exec(positionInCentralWidget);
1062 }
1063 
showContextMenu()1064 void MainWindow::showContextMenu()
1065 {
1066     auto c = browser();
1067     if (!c)
1068         return;
1069 
1070     const auto index = c->currentIndex();
1071     if ( !index.isValid() )
1072         return;
1073 
1074     const auto itemRect = c->visualRect(index);
1075     const auto viewportPosition = itemRect.center();
1076     const auto position = c->mapToGlobal(viewportPosition);
1077     showContextMenuAt(position);
1078 }
1079 
moveUp()1080 void MainWindow::moveUp()
1081 {
1082     auto c = browser();
1083     if (c)
1084         c->move(Qt::Key_Up);
1085 }
1086 
moveDown()1087 void MainWindow::moveDown()
1088 {
1089     auto c = browser();
1090     if (c)
1091         c->move(Qt::Key_Down);
1092 }
1093 
moveToTop()1094 void MainWindow::moveToTop()
1095 {
1096     auto c = browser();
1097     if (c)
1098         c->move(Qt::Key_Home);
1099 }
1100 
moveToBottom()1101 void MainWindow::moveToBottom()
1102 {
1103     auto c = browser();
1104     if (c)
1105         c->move(Qt::Key_End);
1106 }
1107 
onBrowserCreated(ClipboardBrowser * browser)1108 void MainWindow::onBrowserCreated(ClipboardBrowser *browser)
1109 {
1110     connect( browser, &ClipboardBrowser::changeClipboard,
1111              this, &MainWindow::setClipboardAndSelection );
1112     connect( browser, &ClipboardBrowser::requestShow,
1113              this, &MainWindow::showBrowser );
1114     connect( browser, &ClipboardBrowser::error,
1115              this, &MainWindow::showError );
1116     connect( browser, &QAbstractItemView::clicked,
1117              this, &MainWindow::onItemClicked );
1118     connect( browser, &QAbstractItemView::doubleClicked,
1119              this, &MainWindow::onItemDoubleClicked );
1120     connect( browser, &ClipboardBrowser::itemCountChanged,
1121              ui->tabWidget, &TabWidget::setTabItemCount );
1122     connect( browser, &ClipboardBrowser::showContextMenu,
1123              this, &MainWindow::showContextMenuAt );
1124     connect( browser, &ClipboardBrowser::itemSelectionChanged,
1125              this, &MainWindow::onItemSelectionChanged );
1126     connect( browser, &ClipboardBrowser::itemsChanged,
1127              this, &MainWindow::onItemsChanged );
1128     connect( browser, &ClipboardBrowser::internalEditorStateChanged,
1129              this, &MainWindow::onInternalEditorStateChanged );
1130     connect( browser, &ClipboardBrowser::searchRequest,
1131              this, &MainWindow::findNextOrPrevious );
1132     connect( browser, &ClipboardBrowser::searchHideRequest,
1133              ui->searchBar, &Utils::FilterLineEdit::hide );
1134     connect( browser, &ClipboardBrowser::searchShowRequest,
1135              this, &MainWindow::onSearchShowRequest );
1136     connect( browser, &ClipboardBrowser::itemWidgetCreated,
1137              this, &MainWindow::onItemWidgetCreated );
1138 
1139     if (browserOrNull() == browser) {
1140         const int index = ui->tabWidget->currentIndex();
1141         tabChanged(index, index);
1142     }
1143 }
1144 
onBrowserDestroyed(ClipboardBrowserPlaceholder * placeholder)1145 void MainWindow::onBrowserDestroyed(ClipboardBrowserPlaceholder *placeholder)
1146 {
1147     if (placeholder == getPlaceholder()) {
1148         updateContextMenu(0);
1149         updateItemPreviewAfterMs(0);
1150     }
1151 }
1152 
onItemSelectionChanged(const ClipboardBrowser * browser)1153 void MainWindow::onItemSelectionChanged(const ClipboardBrowser *browser)
1154 {
1155     if (browser == browserOrNull()) {
1156         updateContextMenu(0);
1157         updateItemPreviewAfterMs(0);
1158     }
1159 }
1160 
onItemsChanged(const ClipboardBrowser * browser)1161 void MainWindow::onItemsChanged(const ClipboardBrowser *browser)
1162 {
1163     if (browser == browserOrNull()) {
1164         updateContextMenu(contextMenuUpdateIntervalMsec);
1165         updateItemPreviewAfterMs(itemPreviewUpdateIntervalMsec);
1166     }
1167 
1168     const ClipboardBrowserPlaceholder *placeholder = getPlaceholderForTrayMenu();
1169     if (placeholder && placeholder->browser() == browser)
1170         updateTrayMenuItems();
1171 }
1172 
onInternalEditorStateChanged(const ClipboardBrowser * browser)1173 void MainWindow::onInternalEditorStateChanged(const ClipboardBrowser *browser)
1174 {
1175     if (browser == browserOrNull()) {
1176         updateContextMenu(0);
1177         updateItemPreviewAfterMs(0);
1178     }
1179 }
1180 
onItemWidgetCreated(const PersistentDisplayItem & item)1181 void MainWindow::onItemWidgetCreated(const PersistentDisplayItem &item)
1182 {
1183     if ( m_displayCommands.isEmpty() )
1184         return;
1185 
1186     m_displayItemList.append(item);
1187     runDisplayCommands();
1188 }
1189 
onActionDialogAccepted(const Command & command,const QStringList & arguments,const QVariantMap & data)1190 void MainWindow::onActionDialogAccepted(const Command &command, const QStringList &arguments, const QVariantMap &data)
1191 {
1192     auto act = new Action();
1193     act->setCommand(command.cmd, arguments);
1194     act->setInputWithFormat(data, command.input);
1195     act->setName(command.name);
1196     act->setData(data);
1197 
1198     if ( !command.output.isEmpty() ) {
1199         if ( !command.sep.isEmpty() )
1200             actionOutput(this, act, command.output, command.outputTab, QRegularExpression(command.sep));
1201         else
1202             actionOutput(this, act, command.output, command.outputTab);
1203     }
1204 
1205     m_sharedData->actions->action(act);
1206 }
1207 
onSearchShowRequest(const QString & text)1208 void MainWindow::onSearchShowRequest(const QString &text)
1209 {
1210     enterSearchMode();
1211     if (m_options.viMode && text == "/")
1212         return;
1213 
1214     ui->searchBar->setText(text);
1215     ui->searchBar->end(false);
1216 }
1217 
runDisplayCommands()1218 void MainWindow::runDisplayCommands()
1219 {
1220     if ( m_displayItemList.isEmpty() )
1221         return;
1222 
1223     if ( !isInternalActionId(m_displayActionId) ) {
1224         m_currentDisplayItem = m_displayItemList.takeFirst();
1225         const auto action = runScript("runDisplayCommands()", m_currentDisplayItem.data());
1226         m_displayActionId = action->id();
1227     }
1228 
1229     emit sendActionData(m_displayActionId, QByteArray());
1230 }
1231 
clearHiddenDisplayData()1232 void MainWindow::clearHiddenDisplayData()
1233 {
1234     for (int i = m_displayItemList.size() - 1; i >= 0; --i) {
1235         auto &item = m_displayItemList[i];
1236         if ( !item.isValid() )
1237             m_displayItemList.removeAt(i);
1238     }
1239 }
1240 
reloadBrowsers()1241 void MainWindow::reloadBrowsers()
1242 {
1243     for( int i = 0; i < ui->tabWidget->count(); ++i )
1244         getPlaceholder(i)->reloadBrowser();
1245 }
1246 
findTabIndexExactMatch(const QString & name)1247 int MainWindow::findTabIndexExactMatch(const QString &name)
1248 {
1249     TabWidget *w = ui->tabWidget;
1250 
1251     for( int i = 0; i < w->count(); ++i ) {
1252         if ( name == w->tabName(i) )
1253             return i;
1254     }
1255 
1256     return -1;
1257 }
1258 
setClipboardData(const QVariantMap & data)1259 void MainWindow::setClipboardData(const QVariantMap &data)
1260 {
1261     m_clipboardData = data;
1262     updateContextMenu(contextMenuUpdateIntervalMsec);
1263     updateTrayMenuCommands();
1264 }
1265 
setFilter(const QString & text)1266 void MainWindow::setFilter(const QString &text)
1267 {
1268     if ( text.isEmpty() ) {
1269         enterBrowseMode();
1270     } else {
1271         enterSearchMode(text);
1272         getPlaceholder()->setFocus();
1273     }
1274 }
1275 
filter() const1276 QString MainWindow::filter() const
1277 {
1278     return ui->searchBar->isVisible() ? ui->searchBar->text() : QString();
1279 }
1280 
updateWindowTransparency(bool mouseOver)1281 void MainWindow::updateWindowTransparency(bool mouseOver)
1282 {
1283     int opacity = 100 - (mouseOver || isActiveWindow() ? m_options.transparencyFocused : m_options.transparency);
1284     setWindowOpacity(opacity / 100.0);
1285 }
1286 
updateMonitoringActions()1287 void MainWindow::updateMonitoringActions()
1288 {
1289     if ( !m_actionToggleClipboardStoring.isNull() ) {
1290         m_actionToggleClipboardStoring->setIcon(
1291                     getIcon("", m_clipboardStoringDisabled ? IconCheck : IconBan));
1292         m_actionToggleClipboardStoring->setText( m_clipboardStoringDisabled
1293                                                  ? tr("&Enable Clipboard Storing")
1294                                                  : tr("&Disable Clipboard Storing") );
1295     }
1296 }
1297 
getPlaceholder(int index) const1298 ClipboardBrowserPlaceholder *MainWindow::getPlaceholder(int index) const
1299 {
1300     return qobject_cast<ClipboardBrowserPlaceholder*>( ui->tabWidget->widget(index) );
1301 }
1302 
getPlaceholder(const QString & tabName) const1303 ClipboardBrowserPlaceholder *MainWindow::getPlaceholder(const QString &tabName) const
1304 {
1305     for (auto placeholder : findChildren<ClipboardBrowserPlaceholder*>()) {
1306         if ( placeholder->tabName() == tabName )
1307             return placeholder;
1308     }
1309     return nullptr;
1310 }
1311 
getPlaceholder() const1312 ClipboardBrowserPlaceholder *MainWindow::getPlaceholder() const
1313 {
1314     return qobject_cast<ClipboardBrowserPlaceholder*>( ui->tabWidget->currentWidget() );
1315 }
1316 
delayedUpdateForeignFocusWindows()1317 void MainWindow::delayedUpdateForeignFocusWindows()
1318 {
1319     if ( isActiveWindow() || m_trayMenu->isActiveWindow() || m_menu->isActiveWindow() )
1320         m_timerUpdateFocusWindows.stop();
1321     else
1322         m_timerUpdateFocusWindows.start();
1323 }
1324 
setHideTabs(bool hide)1325 void MainWindow::setHideTabs(bool hide)
1326 {
1327     ui->tabWidget->setTabBarHidden(hide);
1328 }
1329 
closeMinimizes() const1330 bool MainWindow::closeMinimizes() const
1331 {
1332     return !m_options.hideMainWindow
1333         && (!m_tray || !QSystemTrayIcon::isSystemTrayAvailable());
1334 }
1335 
createTab(const QString & name,TabNameMatching nameMatch,const Tabs & tabs)1336 ClipboardBrowserPlaceholder *MainWindow::createTab(const QString &name, TabNameMatching nameMatch, const Tabs &tabs)
1337 {
1338     if ( name.isEmpty() )
1339         return nullptr;
1340 
1341     const int i = nameMatch == MatchExactTabName
1342             ? findTabIndexExactMatch(name)
1343             : findTabIndex(name);
1344 
1345     ClipboardBrowserPlaceholder *placeholder = nullptr;
1346     if (i != -1) {
1347         placeholder = getPlaceholder(i);
1348     } else {
1349         placeholder = new ClipboardBrowserPlaceholder(name, m_sharedData, this);
1350         connect( placeholder, &ClipboardBrowserPlaceholder::browserCreated,
1351                  this, &MainWindow::onBrowserCreated );
1352         connect( placeholder, &ClipboardBrowserPlaceholder::browserDestroyed,
1353                  this, [this, placeholder]() { onBrowserDestroyed(placeholder); } );
1354 
1355         ui->tabWidget->addTab(placeholder, name);
1356         saveTabPositions();
1357     }
1358 
1359     const TabProperties tab = tabs.tabProperties(name);
1360     placeholder->setStoreItems(tab.storeItems);
1361 
1362     int maxItemCount = tab.maxItemCount;
1363     if (maxItemCount <= 0)
1364         maxItemCount = m_sharedData->maxItems;
1365     else if (maxItemCount > Config::maxItems)
1366         maxItemCount = Config::maxItems;
1367 
1368     placeholder->setMaxItemCount(maxItemCount);
1369 
1370     return placeholder;
1371 }
1372 
1373 template <typename SlotReturnType>
createAction(int id,MainWindowActionSlot<SlotReturnType> slot,QMenu * menu,QWidget * parent)1374 QAction *MainWindow::createAction(int id, MainWindowActionSlot<SlotReturnType> slot, QMenu *menu, QWidget *parent)
1375 {
1376     QAction *act = parent
1377         ? actionForMenuItem(id, parent, Qt::WidgetWithChildrenShortcut)
1378         : actionForMenuItem(id, this, Qt::WindowShortcut);
1379     connect(act, &QAction::triggered, this, slot, Qt::UniqueConnection);
1380     if (menu)
1381         menu->addAction(act);
1382     return act;
1383 }
1384 
addTrayAction(int id)1385 QAction *MainWindow::addTrayAction(int id)
1386 {
1387     QAction *act = actionForMenuItem(id, m_trayMenu, Qt::WindowShortcut);
1388     m_trayMenu->addAction(act);
1389     return act;
1390 }
1391 
updateTabIcon(const QString & newName,const QString & oldName)1392 void MainWindow::updateTabIcon(const QString &newName, const QString &oldName)
1393 {
1394     const QString icon = getIconNameForTabName(oldName);
1395     if ( !icon.isEmpty() )
1396         setIconNameForTabName(newName, icon);
1397 }
1398 
1399 template <typename Receiver, typename ReturnType>
addItemAction(int id,Receiver * receiver,ReturnType (Receiver::* slot)())1400 QAction *MainWindow::addItemAction(int id, Receiver *receiver, ReturnType (Receiver::* slot)())
1401 {
1402     QAction *act = actionForMenuItem(id, getPlaceholder(), Qt::WidgetWithChildrenShortcut);
1403     connect( act, &QAction::triggered, receiver, slot, Qt::UniqueConnection );
1404     m_menuItem->addAction(act);
1405     return act;
1406 }
1407 
commandsForMenu(const QVariantMap & data,const QString & tabName,const QVector<Command> & allCommands)1408 QVector<Command> MainWindow::commandsForMenu(const QVariantMap &data, const QString &tabName, const QVector<Command> &allCommands)
1409 {
1410     QVector<Command> commands;
1411     for (const auto &command : allCommands) {
1412         if ( canExecuteCommand(command, data, tabName) ) {
1413             Command cmd = command;
1414             if ( cmd.outputTab.isEmpty() )
1415                 cmd.outputTab = tabName;
1416             commands.append(cmd);
1417         }
1418     }
1419 
1420     return commands;
1421 }
1422 
addCommandsToItemMenu(ClipboardBrowser * c)1423 void MainWindow::addCommandsToItemMenu(ClipboardBrowser *c)
1424 {
1425     if ( m_menuCommands.isEmpty() ) {
1426         interruptMenuCommandFilters(&m_itemMenuMatchCommands);
1427         return;
1428     }
1429 
1430     auto data = addSelectionData(*c);
1431     const auto commands = commandsForMenu(data, c->tabName(), m_menuCommands);
1432 
1433     for (const auto &command : commands) {
1434         QString name = command.name;
1435         QMenu *currentMenu = createSubMenus(&name, m_menuItem);
1436         auto act = new CommandAction(command, name, currentMenu);
1437         c->addAction(act);
1438 
1439         addMenuMatchCommand(&m_itemMenuMatchCommands, command.matchCmd, act);
1440 
1441         connect(act, &CommandAction::triggerCommand,
1442                 this, &MainWindow::onItemCommandActionTriggered);
1443     }
1444 
1445     runMenuCommandFilters(&m_itemMenuMatchCommands, data);
1446 }
1447 
addCommandsToTrayMenu(const QVariantMap & clipboardData)1448 void MainWindow::addCommandsToTrayMenu(const QVariantMap &clipboardData)
1449 {
1450     if ( m_trayMenuCommands.isEmpty() ) {
1451         interruptMenuCommandFilters(&m_trayMenuMatchCommands);
1452         return;
1453     }
1454 
1455     ClipboardBrowserPlaceholder *placeholder = getPlaceholderForTrayMenu();
1456     if (!placeholder)
1457         return;
1458 
1459     // Pass current window title to commands in tray menu.
1460     auto data = clipboardData;
1461     if (m_lastWindow)
1462         data.insert( mimeWindowTitle, m_lastWindow->getTitle() );
1463 
1464     const auto commands = commandsForMenu(data, placeholder->tabName(), m_trayMenuCommands);
1465 
1466     for (const auto &command : commands) {
1467         QString name = command.name;
1468         QMenu *currentMenu = createSubMenus(&name, m_trayMenu);
1469         auto act = new CommandAction(command, name, currentMenu);
1470 
1471         addMenuMatchCommand(&m_trayMenuMatchCommands, command.matchCmd, act);
1472 
1473         connect(act, &CommandAction::triggerCommand,
1474                 this, &MainWindow::onClipboardCommandActionTriggered);
1475     }
1476 
1477     runMenuCommandFilters(&m_trayMenuMatchCommands, data);
1478 }
1479 
addMenuMatchCommand(MenuMatchCommands * menuMatchCommands,const QString & matchCommand,QAction * act)1480 void MainWindow::addMenuMatchCommand(MenuMatchCommands *menuMatchCommands, const QString &matchCommand, QAction *act)
1481 {
1482     if ( !matchCommand.isEmpty() ) {
1483         act->setDisabled(true);
1484         menuMatchCommands->matchCommands.append(matchCommand);
1485         menuMatchCommands->actions.append(act);
1486     }
1487 }
1488 
runMenuCommandFilters(MenuMatchCommands * menuMatchCommands,QVariantMap & data)1489 void MainWindow::runMenuCommandFilters(MenuMatchCommands *menuMatchCommands, QVariantMap &data)
1490 {
1491     if ( menuMatchCommands->actions.isEmpty() ) {
1492         interruptMenuCommandFilters(menuMatchCommands);
1493         return;
1494     }
1495 
1496     data[COPYQ_MIME_PREFIX "match-commands"] = menuMatchCommands->matchCommands;
1497 
1498     const bool isRunning = isInternalActionId(menuMatchCommands->actionId);
1499     if (isRunning) {
1500         m_sharedData->actions->setActionData(menuMatchCommands->actionId, data);
1501     } else {
1502         const auto act = runScript("runMenuCommandFilters()", data);
1503         menuMatchCommands->actionId = act->id();
1504     }
1505 
1506     const int currentRun = ++menuMatchCommands->currentRun;
1507     emit sendActionData(menuMatchCommands->actionId, QByteArray::number(currentRun));
1508 }
1509 
interruptMenuCommandFilters(MainWindow::MenuMatchCommands * menuMatchCommands)1510 void MainWindow::interruptMenuCommandFilters(MainWindow::MenuMatchCommands *menuMatchCommands)
1511 {
1512     ++menuMatchCommands->currentRun;
1513     menuMatchCommands->matchCommands.clear();
1514     menuMatchCommands->actions.clear();
1515 
1516     const bool isRunning = isInternalActionId(menuMatchCommands->actionId);
1517     if (isRunning)
1518         emit sendActionData(menuMatchCommands->actionId, QByteArray());
1519 }
1520 
stopMenuCommandFilters(MainWindow::MenuMatchCommands * menuMatchCommands)1521 void MainWindow::stopMenuCommandFilters(MainWindow::MenuMatchCommands *menuMatchCommands)
1522 {
1523     ++menuMatchCommands->currentRun;
1524     menuMatchCommands->matchCommands.clear();
1525     menuMatchCommands->actions.clear();
1526     terminateAction(&menuMatchCommands->actionId);
1527 }
1528 
terminateAction(int * actionId)1529 void MainWindow::terminateAction(int *actionId)
1530 {
1531     if (*actionId == -1)
1532         return;
1533 
1534     const int id = *actionId;
1535     *actionId = -1;
1536     emit sendActionData(id, "ABORT");
1537 }
1538 
isItemMenuDefaultActionValid() const1539 bool MainWindow::isItemMenuDefaultActionValid() const
1540 {
1541     const auto defaultAction = m_menuItem->defaultAction();
1542     return defaultAction != nullptr && defaultAction->isEnabled();
1543 }
1544 
updateToolBar()1545 void MainWindow::updateToolBar()
1546 {
1547     clearActions(m_toolBar);
1548 
1549     if ( m_toolBar->isHidden() )
1550         return;
1551 
1552     QAction *act = actionForMenuItem(Actions::File_New, this, Qt::WindowShortcut);
1553     m_toolBar->addAction(act);
1554 
1555     for ( auto action : m_menuItem->actions() ) {
1556         if ( action->isSeparator() ) {
1557             m_toolBar->addSeparator();
1558         } else if ( !action->icon().isNull() ) {
1559             act = m_toolBar->addAction(QString());
1560 
1561             const auto update = [=]() {
1562                 const QIcon icon = action->icon();
1563                 act->setIcon(icon);
1564 
1565                 const QString text = action->text().remove("&");
1566                 const QString shortcut = action->shortcut().toString(QKeySequence::NativeText);
1567                 const QString label = text + (shortcut.isEmpty() ? QString() : "\n[" + shortcut + "]");
1568                 act->setText(label);
1569 
1570                 const QString tooltip = "<center>" + escapeHtml(text)
1571                         + (shortcut.isEmpty() ? QString() : "<br /><b>" + escapeHtml(shortcut) + "</b>") + "</center>";
1572                 act->setToolTip(tooltip);
1573                 act->setEnabled(action->isEnabled());
1574 
1575                 if ( action->isCheckable() ) {
1576                     act->setCheckable(true);
1577                     act->setChecked(action->isChecked());
1578                 }
1579             };
1580 
1581             connect(act, &QAction::triggered, action, &QAction::triggered);
1582             connect(action, &QAction::changed, act, update);
1583             update();
1584         }
1585     }
1586 
1587     m_toolBar->setFrozen(false);
1588 }
1589 
setTrayEnabled(bool enable)1590 void MainWindow::setTrayEnabled(bool enable)
1591 {
1592     const bool trayAlreadyEnabled = m_tray != nullptr;
1593     if (enable == trayAlreadyEnabled)
1594         return;
1595 
1596     if (enable) {
1597         m_tray = new QSystemTrayIcon(this);
1598         connect( m_tray, &QSystemTrayIcon::activated,
1599                  this, &MainWindow::trayActivated );
1600         updateIcon();
1601 
1602         // On macOS, avoid showing tray menu with the main window.
1603 #ifndef Q_OS_MAC
1604         m_tray->setContextMenu(m_trayMenu);
1605 #endif
1606 
1607         m_tray->show();
1608 
1609         if ( isMinimized() )
1610             hideWindow();
1611     } else {
1612         // Hide tray on Ubuntu (buggy sni-qt) before disabling.
1613         m_tray->hide();
1614 
1615         delete m_tray;
1616         m_tray = nullptr;
1617 
1618         if ( isHidden() && !isMinimized() )
1619             minimizeWindow();
1620     }
1621 }
1622 
isWindowVisible() const1623 bool MainWindow::isWindowVisible() const
1624 {
1625     return !isMinimized() && isVisible() && m_isActiveWindow;
1626 }
1627 
onEscape()1628 void MainWindow::onEscape()
1629 {
1630     if ( browseMode() ) {
1631         auto c = browser();
1632         if (c && !c->hasFocus()) {
1633             enterBrowseMode();
1634             return;
1635         }
1636 
1637         hideWindow();
1638         if (c)
1639             c->setCurrent(0);
1640     } else {
1641         enterBrowseMode();
1642     }
1643 }
1644 
updateActionShortcuts()1645 void MainWindow::updateActionShortcuts()
1646 {
1647     QList<QKeySequence> usedShortcuts;
1648 
1649     for (auto act : m_menuItem->findChildren<CommandAction*>()) {
1650         if (!act->isEnabled() && !act->isVisible())
1651             continue;
1652 
1653         if ( act->property(propertyActionFilterCommandFailed).toBool() )
1654             continue;
1655 
1656         const Command &command = act->command();
1657         QList<QKeySequence> uniqueShortcuts;
1658 
1659         for (const auto &shortcutText : command.shortcuts) {
1660             const QKeySequence shortcut(shortcutText, QKeySequence::PortableText);
1661             if ( !shortcut.isEmpty() && !usedShortcuts.contains(shortcut) ) {
1662                 usedShortcuts.append(shortcut);
1663                 uniqueShortcuts.append(shortcut);
1664 
1665                 if ( !isItemMenuDefaultActionValid() && isItemActivationShortcut(shortcut) )
1666                     m_menuItem->setDefaultAction(act);
1667             }
1668         }
1669 
1670         if (!uniqueShortcuts.isEmpty())
1671             act->setShortcuts(uniqueShortcuts);
1672     }
1673 
1674     for (int id = 0; id < m_actions.size(); ++id) {
1675         QAction *action = m_actions[id];
1676         if (!action)
1677             continue;
1678 
1679         QList<QKeySequence> shortcuts = m_menuItems[id].shortcuts;
1680         for (const auto &shortcut : usedShortcuts)
1681             shortcuts.removeAll(shortcut);
1682 
1683         action->setShortcuts(shortcuts);
1684     }
1685 }
1686 
actionForMenuItem(int id,QWidget * parent,Qt::ShortcutContext context)1687 QAction *MainWindow::actionForMenuItem(int id, QWidget *parent, Qt::ShortcutContext context)
1688 {
1689     Q_ASSERT(id < m_menuItems.size());
1690 
1691     m_actions.resize(m_menuItems.size());
1692 
1693     QPointer<QAction> &action = m_actions[id];
1694     if (action && !action->isEnabled() && !action->isVisible()) {
1695         action->deleteLater();
1696         action = nullptr;
1697     }
1698 
1699     const MenuItem &item = m_menuItems[id];
1700 
1701     if (!action) {
1702         action = new QAction(item.text, parent);
1703         action->setShortcutContext(context);
1704         parent->addAction(action);
1705     }
1706 
1707     action->setIcon( getIcon(item.iconName, item.iconId) );
1708 
1709     return action;
1710 }
1711 
addMenuItems(TrayMenu * menu,ClipboardBrowserPlaceholder * placeholder,int maxItemCount,const QString & searchText)1712 void MainWindow::addMenuItems(TrayMenu *menu, ClipboardBrowserPlaceholder *placeholder, int maxItemCount, const QString &searchText)
1713 {
1714     WidgetSizeGuard sizeGuard(menu);
1715     menu->clearClipboardItems();
1716 
1717     if (maxItemCount <= 0)
1718         return;
1719 
1720     if (!placeholder)
1721         return;
1722 
1723     const ClipboardBrowser *c = placeholder->createBrowser();
1724     if (!c)
1725         return;
1726 
1727     int itemCount = 0;
1728     for ( int i = 0; i < c->length() && itemCount < maxItemCount; ++i ) {
1729         const QModelIndex index = c->model()->index(i, 0);
1730         if ( !searchText.isEmpty() ) {
1731             const QString itemText = index.data(contentType::text).toString().toLower();
1732             if ( !itemText.contains(searchText.toLower()) )
1733                 continue;
1734         }
1735         const QVariantMap data = index.data(contentType::data).toMap();
1736         menu->addClipboardItemAction(data, m_options.trayImages);
1737         ++itemCount;
1738     }
1739 }
1740 
activateMenuItem(ClipboardBrowserPlaceholder * placeholder,const QVariantMap & data,bool omitPaste)1741 void MainWindow::activateMenuItem(ClipboardBrowserPlaceholder *placeholder, const QVariantMap &data, bool omitPaste)
1742 {
1743     if ( m_sharedData->moveItemOnReturnKey ) {
1744         const auto itemHash = ::hash(data);
1745         if (placeholder) {
1746             ClipboardBrowser *c = placeholder->createBrowser();
1747             if (c)
1748                 c->moveToTop(itemHash);
1749         }
1750     }
1751 
1752     if ( QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier) )
1753         setClipboard( createDataMap(mimeText, data.value(mimeText) ) );
1754     else
1755         setClipboard(data);
1756 
1757     if (!m_lastWindow)
1758         updateFocusWindows();
1759 
1760     PlatformWindowPtr lastWindow = m_lastWindow;
1761 
1762     if ( m_options.trayItemPaste && lastWindow && !omitPaste && canPaste() ) {
1763         COPYQ_LOG( QString("Pasting item from tray menu to \"%1\".")
1764                    .arg(lastWindow->getTitle()) );
1765         lastWindow->pasteClipboard();
1766     }
1767 }
1768 
toggleMenu(TrayMenu * menu,QPoint pos)1769 bool MainWindow::toggleMenu(TrayMenu *menu, QPoint pos)
1770 {
1771     if ( menu->isVisible() ) {
1772         menu->close();
1773         return false;
1774     }
1775 
1776     menu->popup( toScreen(pos, menu) );
1777 
1778     menu->raise();
1779     menu->activateWindow();
1780     QApplication::setActiveWindow(menu);
1781     QApplication::processEvents();
1782     stealFocus(*menu);
1783 
1784     return true;
1785 }
1786 
toggleMenu(TrayMenu * menu)1787 bool MainWindow::toggleMenu(TrayMenu *menu)
1788 {
1789     return toggleMenu(menu, QCursor::pos());
1790 }
1791 
exportDataFrom(const QString & fileName,const QStringList & tabs,bool exportConfiguration,bool exportCommands)1792 bool MainWindow::exportDataFrom(const QString &fileName, const QStringList &tabs, bool exportConfiguration, bool exportCommands)
1793 {
1794     QTemporaryFile file(fileName + ".XXXXXX.part");
1795     if ( !file.open() ) {
1796         log( QString("Failed to open temporary file: %1")
1797              .arg(file.errorString()), LogError );
1798         return false;
1799     }
1800 
1801     QDataStream out(&file);
1802     if ( !exportDataV4(&out, tabs, exportConfiguration, exportCommands) )
1803         return false;
1804 
1805     if ( !file.flush() ) {
1806         log( QString("Failed to flush temporary file %1: %2")
1807              .arg(file.fileName(), file.errorString()), LogError );
1808         return false;
1809     }
1810 
1811     QFile originalFile(fileName);
1812     if ( originalFile.exists() && !originalFile.remove(fileName) ) {
1813         log( QString("Failed to remove original file %1: %2")
1814              .arg(fileName, originalFile.errorString()), LogError );
1815         return false;
1816     }
1817 
1818     file.setAutoRemove(false);
1819     if ( !file.rename(fileName) ) {
1820         log( QString("Failed to move temporary file %1: %2")
1821              .arg(file.fileName(), file.errorString()), LogError );
1822         return false;
1823     }
1824 
1825     return true;
1826 }
1827 
exportDataV4(QDataStream * out,const QStringList & tabs,bool exportConfiguration,bool exportCommands)1828 bool MainWindow::exportDataV4(QDataStream *out, const QStringList &tabs, bool exportConfiguration, bool exportCommands)
1829 {
1830     out->setVersion(QDataStream::Qt_4_7);
1831     (*out) << QByteArray("CopyQ v4");
1832 
1833     QVariantMap settingsMap;
1834     if (exportConfiguration) {
1835         const QSettings settings;
1836 
1837         for (const auto &key : settings.allKeys()) {
1838             if ( !key.startsWith("Commands/") )
1839                 settingsMap[key] = serializableValue(settings, key);
1840         }
1841     }
1842 
1843     QVariantList commandsList;
1844     if (exportCommands) {
1845         Settings settings(getConfigurationFilePath("-commands.ini"));
1846 
1847         const int commandCount = settings.beginReadArray("Commands");
1848         commandsList.reserve(commandCount);
1849         for (int i = 0; i < commandCount; ++i) {
1850             settings.setArrayIndex(i);
1851 
1852             QVariantMap commandMap;
1853             for ( const auto &key : settings.allKeys() )
1854                 commandMap[key] = serializableValue(settings.constSettingsData(), key);
1855 
1856             commandsList.append(commandMap);
1857         }
1858 
1859         settings.endArray();
1860     }
1861 
1862     QVariantMap data;
1863     if ( !tabs.isEmpty() )
1864         data["tabs"] = tabs;
1865     if ( !settingsMap.isEmpty() )
1866         data["settings"] = settingsMap;
1867     if ( !commandsList.isEmpty() )
1868         data["commands"] = commandsList;
1869 
1870     (*out) << data;
1871 
1872     for (const auto &tab : tabs) {
1873         const auto i = findTabIndex(tab);
1874         if (i == -1)
1875             continue;
1876 
1877         auto placeholder = getPlaceholder(i);
1878         const bool wasLoaded = placeholder->isDataLoaded();
1879         auto c = placeholder->createBrowserAgain();
1880         if (!c) {
1881             log(QString("Failed to open tab \"%s\" for export").arg(tab), LogError);
1882             return false;
1883         }
1884 
1885         const auto &tabName = c->tabName();
1886 
1887         bool saved = false;
1888         QByteArray tabBytes;
1889         {
1890             QDataStream tabOut(&tabBytes, QIODevice::WriteOnly);
1891             tabOut.setVersion(QDataStream::Qt_4_7);
1892             saved = serializeData(*c->model(), &tabOut);
1893         }
1894 
1895         if (!wasLoaded)
1896             placeholder->expire();
1897 
1898         if (!saved) {
1899             log(QString("Failed to export tab \"%s\"").arg(tab), LogError);
1900             return false;
1901         }
1902 
1903         const auto iconName = getIconNameForTabName(tabName);
1904 
1905         QVariantMap tabMap;
1906         tabMap["name"] = tabName;
1907         tabMap["data"] = tabBytes;
1908         if ( !iconName.isEmpty() )
1909             tabMap["icon"] = iconName;
1910 
1911         (*out) << tabMap;
1912     }
1913 
1914     return out->status() == QDataStream::Ok;
1915 }
1916 
importDataV3(QDataStream * in,ImportOptions options)1917 bool MainWindow::importDataV3(QDataStream *in, ImportOptions options)
1918 {
1919     QByteArray header;
1920     (*in) >> header;
1921     if ( !header.startsWith("CopyQ v3") )
1922         return false;
1923 
1924     QVariantMap data;
1925     (*in) >> data;
1926     if ( in->status() != QDataStream::Ok )
1927         return false;
1928 
1929     const auto tabsList = data.value("tabs").toList();
1930 
1931     QStringList tabs;
1932     tabs.reserve( tabsList.size() );
1933     for (const auto &tabMapValue : tabsList) {
1934         const auto tabMap = tabMapValue.toMap();
1935         const auto oldTabName = tabMap["name"].toString();
1936         tabs.append(oldTabName);
1937     }
1938 
1939     const auto settingsMap = data.value("settings").toMap();
1940     const auto commandsList = data.value("commands").toList();
1941 
1942     bool importConfiguration = true;
1943     bool importCommands = true;
1944 
1945     if (options == ImportOptions::Select) {
1946         ImportExportDialog importDialog(this);
1947         importDialog.setWindowTitle( tr("Options for Import") );
1948         importDialog.setTabs(tabs);
1949         importDialog.setHasConfiguration( !settingsMap.isEmpty() );
1950         importDialog.setHasCommands( !commandsList.isEmpty() );
1951         importDialog.setConfigurationEnabled(true);
1952         importDialog.setCommandsEnabled(true);
1953         if ( importDialog.exec() != QDialog::Accepted )
1954             return true;
1955 
1956         tabs = importDialog.selectedTabs();
1957         importConfiguration = importDialog.isConfigurationEnabled();
1958         importCommands = importDialog.isCommandsEnabled();
1959     }
1960 
1961     const Tabs tabProps;
1962     for (const auto &tabMapValue : tabsList) {
1963         const auto tabMap = tabMapValue.toMap();
1964         const auto oldTabName = tabMap["name"].toString();
1965         if ( !tabs.contains(oldTabName) )
1966             continue;
1967 
1968         auto tabName = oldTabName;
1969         renameToUnique( &tabName, ui->tabWidget->tabs() );
1970 
1971         const auto iconName = tabMap.value("icon").toString();
1972         if ( !iconName.isEmpty() )
1973             setIconNameForTabName(tabName, iconName);
1974 
1975         auto c = createTab(tabName, MatchExactTabName, tabProps)->createBrowser();
1976         if (!c) {
1977             log(QString("Failed to create tab \"%s\" for import").arg(tabName), LogError);
1978             return false;
1979         }
1980 
1981         const auto tabBytes = tabMap.value("data").toByteArray();
1982         QDataStream tabIn(tabBytes);
1983         tabIn.setVersion(QDataStream::Qt_4_7);
1984 
1985         // Don't read items based on current value of "maxitems" option since
1986         // the option can be later also imported.
1987         if ( !deserializeData( c->model(), &tabIn, Config::maxItems ) ) {
1988             log(QString("Failed to import tab \"%s\"").arg(tabName), LogError);
1989             return false;
1990         }
1991 
1992         const auto i = findTabIndex(tabName);
1993         if (i != -1)
1994             getPlaceholder(i)->expire();
1995     }
1996 
1997     if (importConfiguration) {
1998         // Configuration dialog shouldn't be open.
1999         if (cm) {
2000             log("Failed to import configuration while configuration dialog is open", LogError);
2001             return false;
2002         }
2003 
2004         Settings settings;
2005 
2006         for (auto it = settingsMap.constBegin(); it != settingsMap.constEnd(); ++it)
2007             settings.setValue( it.key(), it.value() );
2008 
2009         AppConfig appConfig;
2010         emit configurationChanged(&appConfig);
2011     }
2012 
2013     if (importCommands) {
2014         // Close command dialog.
2015         if ( !maybeCloseCommandDialog() ) {
2016             log("Failed to import command while command dialog is open", LogError);
2017             return false;
2018         }
2019 
2020         // Re-create command dialog again later.
2021         if (m_commandDialog) {
2022             m_commandDialog->deleteLater();
2023             m_commandDialog = nullptr;
2024         }
2025 
2026         Settings settings;
2027 
2028         int i = settings.beginReadArray("Commands");
2029         settings.endArray();
2030 
2031         settings.beginWriteArray("Commands");
2032 
2033         for ( const auto &commandDataValue : commandsList ) {
2034             settings.setArrayIndex(i++);
2035             const auto commandMap = commandDataValue.toMap();
2036             for (auto it = commandMap.constBegin(); it != commandMap.constEnd(); ++it)
2037                 settings.setValue( it.key(), it.value() );
2038         }
2039 
2040         settings.endArray();
2041 
2042         updateEnabledCommands();
2043     }
2044 
2045     return in->status() == QDataStream::Ok;
2046 }
2047 
importDataV4(QDataStream * in,ImportOptions options)2048 bool MainWindow::importDataV4(QDataStream *in, ImportOptions options)
2049 {
2050     QByteArray header;
2051     (*in) >> header;
2052     if ( !header.startsWith("CopyQ v4") )
2053         return false;
2054 
2055     QVariantMap data;
2056     (*in) >> data;
2057     if ( in->status() != QDataStream::Ok )
2058         return false;
2059 
2060     QStringList tabs = data.value("tabs").toStringList();
2061     const auto settingsMap = data.value("settings").toMap();
2062     const auto commandsList = data.value("commands").toList();
2063 
2064     bool importConfiguration = true;
2065     bool importCommands = true;
2066 
2067     if (options == ImportOptions::Select) {
2068         ImportExportDialog importDialog(this);
2069         importDialog.setWindowTitle( tr("Options for Import") );
2070         importDialog.setTabs(tabs);
2071         importDialog.setHasConfiguration( !settingsMap.isEmpty() );
2072         importDialog.setHasCommands( !commandsList.isEmpty() );
2073         importDialog.setConfigurationEnabled(true);
2074         importDialog.setCommandsEnabled(true);
2075         if ( importDialog.exec() != QDialog::Accepted )
2076             return true;
2077 
2078         tabs = importDialog.selectedTabs();
2079         importConfiguration = importDialog.isConfigurationEnabled();
2080         importCommands = importDialog.isCommandsEnabled();
2081     }
2082 
2083     const Tabs tabProps;
2084     while ( !in->atEnd() ) {
2085         QVariantMap tabMap;
2086         (*in) >> tabMap;
2087         if ( in->status() != QDataStream::Ok )
2088             return false;
2089 
2090         const auto oldTabName = tabMap["name"].toString();
2091         if ( !tabs.contains(oldTabName) )
2092             continue;
2093 
2094         auto tabName = oldTabName;
2095         renameToUnique( &tabName, ui->tabWidget->tabs() );
2096 
2097         const auto iconName = tabMap.value("icon").toString();
2098         if ( !iconName.isEmpty() )
2099             setIconNameForTabName(tabName, iconName);
2100 
2101         auto c = createTab(tabName, MatchExactTabName, tabProps)->createBrowser();
2102         if (!c) {
2103             log(QString("Failed to create tab \"%s\" for import").arg(tabName), LogError);
2104             return false;
2105         }
2106 
2107         const auto tabBytes = tabMap.value("data").toByteArray();
2108         QDataStream tabIn(tabBytes);
2109         tabIn.setVersion(QDataStream::Qt_4_7);
2110 
2111         // Don't read items based on current value of "maxitems" option since
2112         // the option can be later also imported.
2113         const int maxItems = importConfiguration ? Config::maxItems : m_sharedData->maxItems;
2114         if ( !deserializeData( c->model(), &tabIn, maxItems ) ) {
2115             log(QString("Failed to import tab \"%s\"").arg(tabName), LogError);
2116             return false;
2117         }
2118 
2119         const auto i = findTabIndex(tabName);
2120         if (i != -1)
2121             getPlaceholder(i)->expire();
2122     }
2123 
2124     if (importConfiguration) {
2125         // Configuration dialog shouldn't be open.
2126         if (cm) {
2127             log("Failed to import configuration while configuration dialog is open", LogError);
2128             return false;
2129         }
2130 
2131         Settings settings;
2132 
2133         for (auto it = settingsMap.constBegin(); it != settingsMap.constEnd(); ++it)
2134             settings.setValue( it.key(), it.value() );
2135 
2136         AppConfig appConfig;
2137         emit configurationChanged(&appConfig);
2138     }
2139 
2140     if (importCommands) {
2141         // Close command dialog.
2142         if ( !maybeCloseCommandDialog() ) {
2143             log("Failed to import command while command dialog is open", LogError);
2144             return false;
2145         }
2146 
2147         // Re-create command dialog again later.
2148         if (m_commandDialog) {
2149             m_commandDialog->deleteLater();
2150             m_commandDialog = nullptr;
2151         }
2152 
2153         Settings settings(getConfigurationFilePath("-commands.ini"));
2154 
2155         int i = settings.beginReadArray("Commands");
2156         settings.endArray();
2157 
2158         settings.beginWriteArray("Commands");
2159 
2160         for ( const auto &commandDataValue : commandsList ) {
2161             settings.setArrayIndex(i++);
2162             const auto commandMap = commandDataValue.toMap();
2163             for (auto it = commandMap.constBegin(); it != commandMap.constEnd(); ++it)
2164                 settings.setValue( it.key(), it.value() );
2165         }
2166 
2167         settings.endArray();
2168 
2169         updateEnabledCommands();
2170     }
2171 
2172     return in->status() == QDataStream::Ok;
2173 }
2174 
updateEnabledCommands()2175 void MainWindow::updateEnabledCommands()
2176 {
2177     updateCommands(loadAllCommands(), false);
2178 }
2179 
updateCommands(QVector<Command> allCommands,bool forceSave)2180 void MainWindow::updateCommands(QVector<Command> allCommands, bool forceSave)
2181 {
2182     m_automaticCommands.clear();
2183     m_menuCommands.clear();
2184     m_scriptCommands.clear();
2185     m_trayMenuCommands.clear();
2186 
2187     QVector<Command> displayCommands;
2188 
2189     if ( addPluginCommands(&allCommands) || forceSave )
2190         saveCommands(allCommands);
2191 
2192     const auto disabledPluginCommands = m_sharedData->itemFactory->commands(false);
2193     Commands commands;
2194     for (const auto &command : allCommands) {
2195         if ( command.enable && !hasCommandFuzzy(disabledPluginCommands, command) )
2196             commands.append(command);
2197     }
2198 
2199     for (const auto &command : commands) {
2200         const auto type = command.type();
2201 
2202         if (type & CommandType::Automatic)
2203             m_automaticCommands.append(command);
2204 
2205         if (type & CommandType::Display)
2206             displayCommands.append(command);
2207 
2208         if (type & CommandType::Menu)
2209             m_menuCommands.append(command);
2210 
2211         if (m_options.trayCommands && type & CommandType::GlobalShortcut)
2212             m_trayMenuCommands.append(command);
2213 
2214         if (type & CommandType::Script)
2215             m_scriptCommands.append(command);
2216     }
2217 
2218     if (m_displayCommands != displayCommands) {
2219         m_displayItemList.clear();
2220         m_displayCommands = displayCommands;
2221         reloadBrowsers();
2222     }
2223 
2224     updateContextMenu(contextMenuUpdateIntervalMsec);
2225     updateTrayMenuCommands();
2226     emit commandsSaved(commands);
2227 }
2228 
addPluginCommands(QVector<Command> * allCommands)2229 bool MainWindow::addPluginCommands(QVector<Command> *allCommands)
2230 {
2231     const auto oldSize = allCommands->size();
2232 
2233     for ( const auto &command : m_sharedData->itemFactory->commands() ) {
2234         if ( !hasCommandFuzzy(*allCommands, command) )
2235             allCommands->append(command);
2236     }
2237 
2238     return oldSize != allCommands->size();
2239 }
2240 
disableHideWindowOnUnfocus()2241 void MainWindow::disableHideWindowOnUnfocus()
2242 {
2243     m_timerHideWindowIfNotActive.disconnect();
2244 }
2245 
enableHideWindowOnUnfocus()2246 void MainWindow::enableHideWindowOnUnfocus()
2247 {
2248     initSingleShotTimer( &m_timerHideWindowIfNotActive, 100, this, &MainWindow::hideWindowIfNotActive );
2249 }
2250 
hideWindowIfNotActive()2251 void MainWindow::hideWindowIfNotActive()
2252 {
2253     if ( isVisible() && !hasDialogOpen(this) && !isAnyApplicationWindowActive() ) {
2254         COPYQ_LOG("Auto-hiding unfocused main window");
2255         hideWindow();
2256     }
2257 }
2258 
theme() const2259 const Theme &MainWindow::theme() const
2260 {
2261     return m_sharedData->theme;
2262 }
2263 
runScript(const QString & script,const QVariantMap & data)2264 Action *MainWindow::runScript(const QString &script, const QVariantMap &data)
2265 {
2266     auto act = new Action();
2267     act->setCommand(QStringList() << "copyq" << "eval" << "--" << script);
2268     act->setData(data);
2269     runInternalAction(act);
2270     return act;
2271 }
2272 
findTabIndex(const QString & name)2273 int MainWindow::findTabIndex(const QString &name)
2274 {
2275     TabWidget *w = ui->tabWidget;
2276 
2277     const int found = findTabIndexExactMatch(name);
2278     if (found != -1)
2279         return found;
2280 
2281     // Ignore key hints ('&').
2282     if ( !hasKeyHint(name) ) {
2283         for( int i = 0; i < w->count(); ++i ) {
2284             QString tabName = w->tabName(i);
2285             if ( name == removeKeyHint(&tabName) )
2286                 return i;
2287         }
2288     }
2289 
2290     return -1;
2291 }
2292 
tab(const QString & name)2293 ClipboardBrowser *MainWindow::tab(const QString &name)
2294 {
2295     return createTab(name, MatchSimilarTabName, Tabs())->createBrowser();
2296 }
2297 
maybeCloseCommandDialog()2298 bool MainWindow::maybeCloseCommandDialog()
2299 {
2300     return !m_commandDialog || m_commandDialog->maybeClose(this);
2301 }
2302 
showError(const QString & msg)2303 void MainWindow::showError(const QString &msg)
2304 {
2305     const auto notificationId = qHash(msg);
2306     auto notification = createNotification( QString::number(notificationId) );
2307     notification->setTitle( tr("CopyQ Error", "Notification error message title") );
2308     notification->setMessage(msg);
2309     notification->setIcon(IconTimesCircle);
2310 }
2311 
createNotification(const QString & id)2312 Notification *MainWindow::createNotification(const QString &id)
2313 {
2314     return m_sharedData->notifications->createNotification(id);
2315 }
2316 
addCommands(const QVector<Command> & commands)2317 void MainWindow::addCommands(const QVector<Command> &commands)
2318 {
2319     openCommands();
2320     if (m_commandDialog)
2321         m_commandDialog->addCommands(commands);
2322 }
2323 
keyPressEvent(QKeyEvent * event)2324 void MainWindow::keyPressEvent(QKeyEvent *event)
2325 {
2326     const int key = event->key();
2327     const Qt::KeyboardModifiers modifiers = event->modifiers();
2328 
2329     // Search items or text in editor (search previous when Shift is pressed).
2330     if ( key == Qt::Key_F3
2331          || (modifiers.testFlag(Qt::ControlModifier) && (key == Qt::Key_F || key == Qt::Key_G)) )
2332     {
2333         findNextOrPrevious();
2334         return;
2335     }
2336 
2337     auto c = browser();
2338     if (c && c->isInternalEditorOpen())
2339         return;
2340 
2341     if (m_options.hideTabs && key == Qt::Key_Alt)
2342         setHideTabs(false);
2343 
2344     if (m_options.viMode) {
2345         if (modifiers == Qt::ControlModifier && key == Qt::Key_BracketLeft) {
2346             onEscape();
2347             return;
2348         }
2349 
2350         if (browseMode()) {
2351             if (c && handleViKey(event, c))
2352                 return;
2353 
2354             switch(key) {
2355             case Qt::Key_Slash:
2356                 enterSearchMode();
2357                 event->accept();
2358                 return;
2359             case Qt::Key_H:
2360                 previousTab();
2361                 event->accept();
2362                 return;
2363             case Qt::Key_L:
2364                 nextTab();
2365                 event->accept();
2366                 return;
2367             }
2368         }
2369     }
2370 
2371     if ( event->matches(QKeySequence::NextChild) ) {
2372          nextTab();
2373          return;
2374     }
2375 
2376     if ( event->matches(QKeySequence::PreviousChild) ) {
2377          previousTab();
2378          return;
2379     }
2380 
2381     // Ctrl/Alt+0 to Ctrl/Alt+9 to focus tabs (0 to focus the last, 1 to focus the first and so on).
2382     if (modifiers == Qt::ControlModifier || modifiers == Qt::AltModifier) {
2383         if (key >= Qt::Key_0 && key <= Qt::Key_9) {
2384             const int index = (key == Qt::Key_0) ? ui->tabWidget->count() - 1
2385                                                  : key - Qt::Key_1;
2386             ui->tabWidget->setCurrentIndex(index);
2387             return;
2388         }
2389     }
2390 
2391     if (modifiers == Qt::ControlModifier) {
2392         switch(key) {
2393             case Qt::Key_Return:
2394             case Qt::Key_Enter:
2395                 if (c)
2396                     activateCurrentItem();
2397                 return;
2398             default:
2399                 QMainWindow::keyPressEvent(event);
2400                 break;
2401         }
2402         return;
2403     }
2404 
2405     if (modifiers == Qt::AltModifier)
2406         return;
2407 
2408     switch(key) {
2409         case Qt::Key_Down:
2410         case Qt::Key_Up:
2411         case Qt::Key_PageDown:
2412         case Qt::Key_PageUp:
2413             if ( c && !c->hasFocus() )
2414                 c->setFocus();
2415             break;
2416 
2417         case Qt::Key_Return:
2418         case Qt::Key_Enter:
2419             if (c)
2420                 activateCurrentItem();
2421             else
2422                 getPlaceholder()->createBrowser();
2423             break;
2424 
2425         case Qt::Key_Tab:
2426             QMainWindow::keyPressEvent(event);
2427             break;
2428 
2429 #ifndef Q_OS_MAC
2430         case Qt::Key_Backspace:
2431             // fallthrough
2432 #endif // Q_OS_MAC
2433         case Qt::Key_Escape:
2434             onEscape();
2435             break;
2436 
2437         default:
2438             QMainWindow::keyPressEvent(event);
2439             break;
2440     }
2441 }
2442 
keyReleaseEvent(QKeyEvent * event)2443 void MainWindow::keyReleaseEvent(QKeyEvent *event)
2444 {
2445     if (m_options.hideTabs && event->key() == Qt::Key_Alt)
2446         setHideTabs(true);
2447 
2448     QMainWindow::keyReleaseEvent(event);
2449 }
2450 
event(QEvent * event)2451 bool MainWindow::event(QEvent *event)
2452 {
2453     QEvent::Type type = event->type();
2454 
2455     if (type == QEvent::Enter) {
2456         if ( !isActiveWindow() )
2457             updateFocusWindows();
2458         updateWindowTransparency(true);
2459     } else if (type == QEvent::Leave) {
2460         updateWindowTransparency(false);
2461         setHideTabs(m_options.hideTabs);
2462     } else if (type == QEvent::WindowActivate) {
2463         m_isActiveWindow = true;
2464         if ( !isActiveWindow() )
2465             updateFocusWindows();
2466         updateWindowTransparency();
2467         enableHideWindowOnUnfocus();
2468     } else if (type == QEvent::WindowDeactivate) {
2469         updateWindowTransparency();
2470         setHideTabs(m_options.hideTabs);
2471         m_timerUpdateFocusWindows.start();
2472     } else if (type == QEvent::Hide) {
2473         m_wasMaximized = isMaximized();
2474     }
2475 
2476     return QMainWindow::event(event);
2477 }
2478 
nativeEvent(const QByteArray & eventType,void * message,long * result)2479 bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
2480 {
2481     delayedUpdateForeignFocusWindows();
2482     return QMainWindow::nativeEvent(eventType, message, result);
2483 }
2484 
loadSettings(QSettings & settings,AppConfig * appConfig)2485 void MainWindow::loadSettings(QSettings &settings, AppConfig *appConfig)
2486 {
2487     stopMenuCommandFilters(&m_itemMenuMatchCommands);
2488     stopMenuCommandFilters(&m_trayMenuMatchCommands);
2489     terminateAction(&m_displayActionId);
2490 
2491     theme().decorateMainWindow(this);
2492     ui->scrollAreaItemPreview->setObjectName("ClipboardBrowser");
2493     theme().decorateItemPreview(ui->scrollAreaItemPreview);
2494 
2495     setUseSystemIcons( theme().useSystemIcons() );
2496 
2497     m_options.confirmExit = appConfig->option<Config::confirm_exit>();
2498 
2499     // always on top window hint
2500     bool alwaysOnTop = appConfig->option<Config::always_on_top>();
2501     setAlwaysOnTop(this, alwaysOnTop);
2502     setAlwaysOnTop(m_commandDialog.data(), alwaysOnTop);
2503 
2504     // Vi mode
2505     m_options.viMode = appConfig->option<Config::vi>();
2506     m_trayMenu->setViModeEnabled(m_options.viMode);
2507     m_menu->setViModeEnabled(m_options.viMode);
2508 
2509     // Number search
2510     m_trayMenu->setNumberSearchEnabled(m_sharedData->numberSearch);
2511     m_menu->setNumberSearchEnabled(m_sharedData->numberSearch);
2512 
2513     m_trayMenu->setRowIndexFromOne(m_sharedData->rowIndexFromOne);
2514     m_menu->setRowIndexFromOne(m_sharedData->rowIndexFromOne);
2515 
2516     m_options.transparency = appConfig->option<Config::transparency>();
2517     m_options.transparencyFocused = appConfig->option<Config::transparency_focused>();
2518     updateWindowTransparency();
2519 
2520     // save unsaved tab data
2521     if ( ui->tabWidget->count() != 0 ) {
2522         if ( m_timerSaveTabPositions.isActive() )
2523             doSaveTabPositions(appConfig);
2524         ui->tabWidget->saveTabInfo();
2525     }
2526 
2527     const QStringList tabNames = savedTabs();
2528 
2529     // tab bar position
2530     const bool tabTreeEnabled = appConfig->option<Config::tab_tree>();
2531     ui->tabWidget->setTreeModeEnabled(tabTreeEnabled);
2532     ui->tabWidget->setTabItemCountVisible(appConfig->option<Config::show_tab_item_count>());
2533     for ( auto scrollArea : ui->tabWidget->toolBar()->findChildren<QAbstractScrollArea*>() )
2534         theme().decorateScrollArea(scrollArea);
2535 
2536     // create tabs
2537     const Tabs tabs;
2538     for (const auto &name : tabNames)
2539         createTab(name, MatchExactTabName, tabs);
2540 
2541     ui->tabWidget->setTabsOrder(tabNames);
2542 
2543     m_options.hideTabs = appConfig->option<Config::hide_tabs>();
2544     setHideTabs(m_options.hideTabs);
2545 
2546     bool hideToolbar = appConfig->option<Config::hide_toolbar>();
2547     clearActions(m_toolBar);
2548     m_toolBar->setHidden(hideToolbar);
2549     bool hideToolBarLabels = appConfig->option<Config::hide_toolbar_labels>();
2550     m_toolBar->setToolButtonStyle(hideToolBarLabels ? Qt::ToolButtonIconOnly
2551                                                       : Qt::ToolButtonTextUnderIcon);
2552 
2553     m_options.hideMainWindow = appConfig->option<Config::hide_main_window>();
2554     m_options.closeOnUnfocus = appConfig->option<Config::close_on_unfocus>();
2555 
2556     const bool hideInTaskBar = appConfig->option<Config::hide_main_window_in_task_bar>();
2557     setHideInTaskBar(this, hideInTaskBar);
2558 
2559     Q_ASSERT( ui->tabWidget->count() > 0 );
2560 
2561     // Save any tabs loaded from new tab files.
2562     appConfig->setOption("tabs", tabNames);
2563 
2564     reloadBrowsers();
2565 
2566     ui->tabWidget->updateTabs();
2567 
2568     m_timerSaveTabPositions.stop();
2569 
2570     updateContextMenu(contextMenuUpdateIntervalMsec);
2571     updateItemPreviewAfterMs(itemPreviewUpdateIntervalMsec);
2572 
2573     m_options.itemActivationCommands = ActivateNoCommand;
2574     if ( appConfig->option<Config::activate_closes>() )
2575         m_options.itemActivationCommands |= ActivateCloses;
2576     if ( appConfig->option<Config::activate_focuses>() )
2577         m_options.itemActivationCommands |= ActivateFocuses;
2578     if ( appConfig->option<Config::activate_pastes>() )
2579         m_options.itemActivationCommands |= ActivatePastes;
2580 
2581     m_options.trayItems = appConfig->option<Config::tray_items>();
2582     m_options.trayItemPaste = appConfig->option<Config::tray_item_paste>();
2583     m_options.trayCommands = appConfig->option<Config::tray_commands>();
2584     m_options.trayCurrentTab = appConfig->option<Config::tray_tab_is_current>();
2585     m_options.trayTabName = appConfig->option<Config::tray_tab>();
2586     m_options.trayImages = appConfig->option<Config::tray_images>();
2587     m_options.trayMenuOpenOnLeftClick = appConfig->option<Config::tray_menu_open_on_left_click>();
2588     m_options.clipboardTab = appConfig->option<Config::clipboard_tab>();
2589 
2590     m_singleClickActivate = appConfig->option<Config::activate_item_with_single_click>();
2591 
2592     const auto toolTipStyleSheet = theme().getToolTipStyleSheet();
2593     m_trayMenu->setStyleSheet(toolTipStyleSheet);
2594     m_menu->setStyleSheet(toolTipStyleSheet);
2595 
2596     setTrayEnabled( !appConfig->option<Config::disable_tray>() );
2597     updateTrayMenuItems();
2598 
2599     updateIcon();
2600 
2601     menuBar()->setNativeMenuBar( appConfig->option<Config::native_menu_bar>() );
2602 
2603     ui->searchBar->loadSettings();
2604 
2605     settings.beginGroup("Shortcuts");
2606     loadShortcuts(&m_menuItems, settings);
2607     updateActionShortcuts();
2608     settings.endGroup();
2609 
2610     enterBrowseMode();
2611 
2612     updateEnabledCommands();
2613 
2614     m_sharedData->notifications->setIconColor( theme().color("notification_fg") );
2615 }
2616 
loadTheme(const QSettings & themeSettings)2617 void MainWindow::loadTheme(const QSettings &themeSettings)
2618 {
2619     m_sharedData->theme.loadTheme(themeSettings);
2620     if (themeSettings.status() != QSettings::NoError)
2621         return;
2622 
2623     {
2624         Settings settings;
2625         settings.beginGroup("Theme");
2626         m_sharedData->theme.saveTheme(settings.settingsData());
2627         settings.endGroup();
2628     }
2629 
2630     AppConfig appConfig;
2631     emit configurationChanged(&appConfig);
2632 }
2633 
openHelp()2634 void MainWindow::openHelp()
2635 {
2636     QDesktopServices::openUrl( QUrl("https://copyq.readthedocs.io") );
2637 }
2638 
showWindow()2639 void MainWindow::showWindow()
2640 {
2641     if ( isWindowVisible() )
2642         return;
2643 
2644     m_trayMenu->close();
2645     m_menu->close();
2646 
2647     updateFocusWindows();
2648 
2649     moveToCurrentWorkspace(this);
2650 
2651     if (m_wasMaximized || isMaximized())
2652         showMaximized();
2653     else
2654         showNormal();
2655     raise();
2656     activateWindow();
2657 
2658     auto c = browser();
2659     if (c) {
2660         if ( !c->isInternalEditorOpen() )
2661             c->scrollTo( c->currentIndex() );
2662         c->setFocus();
2663     }
2664 
2665     QApplication::setActiveWindow(this);
2666 
2667     stealFocus(*this);
2668 }
2669 
hideWindow()2670 void MainWindow::hideWindow()
2671 {
2672     if ( closeMinimizes() )
2673         minimizeWindow();
2674     else
2675         hide();
2676 
2677     // It can be unexpected to have search active or random items selected when
2678     // reopening main window. This resets search and selection after the window
2679     // is closed.
2680     if ( !browseMode() ) {
2681         enterBrowseMode();
2682         auto c = browser();
2683         if (c)
2684             c->setCurrent(0);
2685     }
2686 }
2687 
minimizeWindow()2688 void MainWindow::minimizeWindow()
2689 {
2690     if (m_options.hideMainWindow)
2691         hide();
2692     else
2693         showMinimized();
2694 }
2695 
toggleVisible()2696 bool MainWindow::toggleVisible()
2697 {
2698     if ( isWindowVisible() ) {
2699         hideWindow();
2700         return false;
2701     }
2702 
2703     showWindow();
2704     return true;
2705 }
2706 
showBrowser(const ClipboardBrowser * browser)2707 void MainWindow::showBrowser(const ClipboardBrowser *browser)
2708 {
2709     int i = 0;
2710     for( ; i < ui->tabWidget->count() && getPlaceholder(i)->browser() != browser; ++i ) {}
2711     setCurrentTab(i);
2712     showWindow();
2713 }
2714 
setCurrentTab(int index)2715 bool MainWindow::setCurrentTab(int index)
2716 {
2717     if ( index < 0 || ui->tabWidget->count() <= index )
2718         return false;
2719 
2720     ui->tabWidget->setCurrentIndex(index);
2721     return true;
2722 }
2723 
focusPrevious()2724 bool MainWindow::focusPrevious()
2725 {
2726     if ( !m_lastWindow )
2727         return false;
2728 
2729     m_lastWindow->raise();
2730     return true;
2731 }
2732 
onMenuActionTriggered(const QVariantMap & data,bool omitPaste)2733 void MainWindow::onMenuActionTriggered(const QVariantMap &data, bool omitPaste)
2734 {
2735     m_menu->close();
2736     activateMenuItem( getPlaceholderForMenu(), data, omitPaste );
2737 }
2738 
onTrayActionTriggered(const QVariantMap & data,bool omitPaste)2739 void MainWindow::onTrayActionTriggered(const QVariantMap &data, bool omitPaste)
2740 {
2741     m_trayMenu->close();
2742     activateMenuItem( getPlaceholderForTrayMenu(), data, omitPaste );
2743 }
2744 
trayActivated(QSystemTrayIcon::ActivationReason reason)2745 void MainWindow::trayActivated(QSystemTrayIcon::ActivationReason reason)
2746 {
2747     if ( reason == QSystemTrayIcon::MiddleClick
2748          || (m_options.trayMenuOpenOnLeftClick && reason == QSystemTrayIcon::Trigger) )
2749     {
2750         toggleMenu();
2751     } else if ( reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick ) {
2752         // Like toggleVisible() but hide window if visible and not focused
2753         // (this seems better behavior when using mouse).
2754         if (!isMinimized() && isVisible())
2755             hideWindow();
2756         else
2757             showWindow();
2758 
2759     }
2760 }
2761 
toggleMenu()2762 bool MainWindow::toggleMenu()
2763 {
2764     m_trayMenu->search(QString());
2765 
2766     if ( !m_trayMenu->isVisible() )
2767         updateTrayMenuItemsTimeout();
2768 
2769     return toggleMenu(m_trayMenu);
2770 }
2771 
toggleMenu(const QString & tabName,int itemCount,QPoint position)2772 bool MainWindow::toggleMenu(const QString &tabName, int itemCount, QPoint position)
2773 {
2774     // Just close the previously opened menu if parameters are the same.
2775     if ( m_menu->isVisible()
2776          && (m_menuTabName == tabName && m_menuMaxItemCount == itemCount) )
2777     {
2778         m_menu->close();
2779         return false;
2780     }
2781 
2782     WidgetSizeGuard sizeGuard(m_menu);
2783 
2784     m_menuTabName = tabName;
2785     m_menuMaxItemCount = itemCount;
2786     if (m_menuMaxItemCount < 0)
2787         m_menuMaxItemCount = m_options.trayItems > 0 ? m_options.trayItems : 10;
2788 
2789     m_menu->clearAllActions();
2790     filterMenuItems(QString());
2791 
2792     if ( m_menu->isVisible() )
2793         m_menu->close();
2794 
2795     if ( m_menu->isEmpty() )
2796         return false;
2797 
2798     if (position.x() >= 0 && position.y() >= 0)
2799         return toggleMenu(m_menu, position);
2800 
2801     return toggleMenu(m_menu);
2802 }
2803 
tabChanged(int current,int)2804 void MainWindow::tabChanged(int current, int)
2805 {
2806     bool currentIsTabGroup = current == -1;
2807 
2808     emit tabGroupSelected(currentIsTabGroup);
2809 
2810     if (!currentIsTabGroup) {
2811         // update item menu (necessary for keyboard shortcuts to work)
2812         auto c = browserOrNull();
2813         if (c) {
2814             c->filterItems( browseMode() ? nullptr : ui->searchBar->filter() );
2815 
2816             if ( current >= 0 ) {
2817                 if( !c->currentIndex().isValid() && isVisible() ) {
2818                     c->setCurrent(0);
2819                 }
2820             }
2821 
2822             setTabOrder(ui->searchBar, c);
2823         }
2824     }
2825 
2826     updateContextMenu(0);
2827     updateItemPreviewAfterMs(0);
2828 
2829     if (m_options.trayCurrentTab)
2830         updateTrayMenuItems();
2831 }
2832 
saveTabPositions()2833 void MainWindow::saveTabPositions()
2834 {
2835     m_timerSaveTabPositions.start();
2836 }
2837 
onSaveTabPositionsTimer()2838 void MainWindow::onSaveTabPositionsTimer()
2839 {
2840     AppConfig appConfig;
2841     doSaveTabPositions(&appConfig);
2842 }
2843 
doSaveTabPositions(AppConfig * appConfig)2844 void MainWindow::doSaveTabPositions(AppConfig *appConfig)
2845 {
2846     m_timerSaveTabPositions.stop();
2847     const QStringList tabs = ui->tabWidget->tabs();
2848     appConfig->setOption("tabs", tabs);
2849 }
2850 
tabsMoved(const QString & oldPrefix,const QString & newPrefix)2851 void MainWindow::tabsMoved(const QString &oldPrefix, const QString &newPrefix)
2852 {
2853     const QStringList tabs = ui->tabWidget->tabs();
2854     Q_ASSERT( oldPrefix == newPrefix || !tabs.contains(oldPrefix) );
2855     Q_ASSERT( !tabs.contains(QString()) );
2856 
2857     const QString prefix = oldPrefix + '/';
2858 
2859     // Rename tabs if needed.
2860     for (int i = 0 ; i < tabs.size(); ++i) {
2861         auto placeholder = getPlaceholder(i);
2862         const QString oldTabName = placeholder->tabName();
2863 
2864         if ( (oldTabName == oldPrefix || oldTabName.startsWith(prefix)) && newPrefix != oldPrefix) {
2865             const QString newName = newPrefix + oldTabName.mid(oldPrefix.size());
2866             updateTabIcon(newName, placeholder->tabName());
2867             if ( placeholder->setTabName(newName) ) {
2868                 auto c = placeholder->browser();
2869                 if (c)
2870                     ui->tabWidget->setTabItemCount( newName, c->length() );
2871             }
2872         }
2873     }
2874 
2875     saveTabPositions();
2876 }
2877 
tabBarMenuRequested(QPoint pos,int tab)2878 void MainWindow::tabBarMenuRequested(QPoint pos, int tab)
2879 {
2880     auto placeholder = getPlaceholder(tab);
2881     if (placeholder == nullptr)
2882         return;
2883     const QString tabName = placeholder->tabName();
2884     popupTabBarMenu(pos, tabName);
2885 }
2886 
tabTreeMenuRequested(QPoint pos,const QString & groupPath)2887 void MainWindow::tabTreeMenuRequested(QPoint pos, const QString &groupPath)
2888 {
2889     popupTabBarMenu(pos, groupPath);
2890 }
2891 
tabCloseRequested(int tab)2892 void MainWindow::tabCloseRequested(int tab)
2893 {
2894     removeTab(true, tab);
2895 }
2896 
config(const QVariantList & nameValue)2897 QVariant MainWindow::config(const QVariantList &nameValue)
2898 {
2899     AppConfig appConfig;
2900 
2901     if ( m_timerSaveTabPositions.isActive() )
2902         doSaveTabPositions(&appConfig);
2903 
2904     ConfigurationManager configurationManager;
2905 
2906     QStringList unknownOptions;
2907     const auto validOptions = configurationManager.options();
2908 
2909     // Check if option names are valid.
2910     for (int i = 0; i < nameValue.size(); i += 2) {
2911         const QString name = nameValue[i].toString();
2912         if ( !validOptions.contains(name) )
2913             unknownOptions.append(name);
2914     }
2915 
2916     if ( !unknownOptions.isEmpty() )
2917         return unknownOptions;
2918 
2919     configurationManager.loadSettings(&appConfig);
2920 
2921     QVariantMap result;
2922     bool emitConfigurationChanged = false;
2923     for (int i = 0; i < nameValue.size(); i += 2) {
2924         const QString name = nameValue[i].toString();
2925         const QVariant value = nameValue.value(i + 1);
2926         if ( i + 1 < nameValue.size() && configurationManager.setOptionValue(name, value, &appConfig) )
2927             emitConfigurationChanged = true;
2928 
2929         result.insert( name, configurationManager.optionValue(name) );
2930     }
2931 
2932     if (emitConfigurationChanged) {
2933         configurationManager.setAutostartEnable(&appConfig);
2934         emit configurationChanged(&appConfig);
2935     }
2936 
2937     return result;
2938 }
2939 
configDescription()2940 QString MainWindow::configDescription()
2941 {
2942     ConfigurationManager configurationManager;
2943     QStringList options = configurationManager.options();
2944     options.sort();
2945     QString opts;
2946     for (const auto &option : options)
2947         opts.append( option + "\n  " + configurationManager.optionToolTip(option).replace('\n', "\n  ") + '\n' );
2948     return opts;
2949 }
2950 
actionData(int id) const2951 QVariantMap MainWindow::actionData(int id) const
2952 {
2953     return m_sharedData->actions->actionData(id);
2954 }
2955 
setActionData(int id,const QVariantMap & data)2956 void MainWindow::setActionData(int id, const QVariantMap &data)
2957 {
2958     m_sharedData->actions->setActionData(id, data);
2959 }
2960 
setCommands(const QVector<Command> & commands)2961 void MainWindow::setCommands(const QVector<Command> &commands)
2962 {
2963     if ( !maybeCloseCommandDialog() )
2964         return;
2965 
2966     updateCommands(commands, true);
2967 }
2968 
setSessionIconColor(QColor color)2969 void MainWindow::setSessionIconColor(QColor color)
2970 {
2971     ::setSessionIconColor(color);
2972     updateIcon();
2973 }
2974 
setSessionIconTag(const QString & tag)2975 void MainWindow::setSessionIconTag(const QString &tag)
2976 {
2977     ::setSessionIconTag(tag);
2978     updateIcon();
2979 }
2980 
setSessionIconTagColor(QColor color)2981 void MainWindow::setSessionIconTagColor(QColor color)
2982 {
2983     ::setSessionIconTagColor(color);
2984     updateIcon();
2985 }
2986 
sessionIconColor() const2987 QColor MainWindow::sessionIconColor() const
2988 {
2989     return ::sessionIconColor();
2990 }
2991 
sessionIconTag() const2992 QString MainWindow::sessionIconTag() const
2993 {
2994     return ::sessionIconTag();
2995 }
2996 
sessionIconTagColor() const2997 QColor MainWindow::sessionIconTagColor() const
2998 {
2999     return ::sessionIconTagColor();
3000 }
3001 
setTrayTooltip(const QString & tooltip)3002 void MainWindow::setTrayTooltip(const QString &tooltip)
3003 {
3004     if (m_tray)
3005         m_tray->setToolTip(tooltip);
3006 }
3007 
setMenuItemEnabled(int actionId,int currentRun,int menuItemMatchCommandIndex,const QVariantMap & menuItem)3008 bool MainWindow::setMenuItemEnabled(int actionId, int currentRun, int menuItemMatchCommandIndex, const QVariantMap &menuItem)
3009 {
3010     if (actionId != m_trayMenuMatchCommands.actionId && actionId != m_itemMenuMatchCommands.actionId)
3011         return false;
3012 
3013     const auto &menuMatchCommands = actionId == m_trayMenuMatchCommands.actionId
3014             ? m_trayMenuMatchCommands
3015             : m_itemMenuMatchCommands;
3016 
3017     if (currentRun != menuMatchCommands.currentRun)
3018         return false;
3019 
3020     if (menuMatchCommands.actions.size() <= menuItemMatchCommandIndex)
3021         return false;
3022 
3023     auto action = menuMatchCommands.actions[menuItemMatchCommandIndex];
3024     if (!action)
3025         return true;
3026 
3027     for (auto it = menuItem.constBegin(); it != menuItem.constEnd(); ++it) {
3028         const auto &key = it.key();
3029         if (key == menuItemKeyColor || key == menuItemKeyIcon || key == menuItemKeyTag)
3030             continue;
3031 
3032         const auto value = it.value();
3033         action->setProperty(key.toLatin1(), value);
3034     }
3035 
3036     if ( menuItem.contains(menuItemKeyTag) || menuItem.contains(menuItemKeyIcon) ) {
3037         QString icon = menuItem.value(menuItemKeyIcon).toString();
3038         if (icon.isEmpty()) {
3039             const auto commandAction = qobject_cast<CommandAction*>(action);
3040             if (commandAction)
3041                 icon = commandAction->command().icon;
3042         }
3043         const QString colorName = menuItem.value(menuItemKeyColor).toString();
3044         const QColor color = colorName.isEmpty() ? getDefaultIconColor(*this) : deserializeColor(colorName);
3045         const QString tag = menuItem.value(menuItemKeyTag).toString();
3046         action->setIcon( iconFromFile(icon, tag, color) );
3047     }
3048 
3049     const bool enabled = action->isEnabled();
3050     action->setProperty(propertyActionFilterCommandFailed, !enabled);
3051 
3052     const auto shortcuts = action->shortcuts();
3053 
3054     if ( !enabled && (actionId == m_trayMenuMatchCommands.actionId || !m_menuItem->isVisible()) )
3055         action->deleteLater();
3056 
3057     if ( !shortcuts.isEmpty() )
3058         updateActionShortcuts();
3059 
3060     return true;
3061 }
3062 
setDisplayData(int actionId,const QVariantMap & data)3063 QVariantMap MainWindow::setDisplayData(int actionId, const QVariantMap &data)
3064 {
3065     if (m_displayActionId != actionId)
3066         return QVariantMap();
3067 
3068     m_currentDisplayItem.setData(data);
3069 
3070     clearHiddenDisplayData();
3071 
3072     if ( m_displayItemList.isEmpty() )
3073         return QVariantMap();
3074 
3075     m_currentDisplayItem = m_displayItemList.takeFirst();
3076     m_sharedData->actions->setActionData(actionId, m_currentDisplayItem.data());
3077     return m_currentDisplayItem.data();
3078 }
3079 
nextTab()3080 void MainWindow::nextTab()
3081 {
3082     ui->tabWidget->nextTab();
3083 }
3084 
previousTab()3085 void MainWindow::previousTab()
3086 {
3087     ui->tabWidget->previousTab();
3088 }
3089 
setClipboard(const QVariantMap & data)3090 void MainWindow::setClipboard(const QVariantMap &data)
3091 {
3092     setClipboard(data, ClipboardMode::Clipboard);
3093 #ifdef HAS_MOUSE_SELECTIONS
3094     setClipboard(data, ClipboardMode::Selection);
3095 #endif
3096 }
3097 
setClipboard(const QVariantMap & data,ClipboardMode mode)3098 void MainWindow::setClipboard(const QVariantMap &data, ClipboardMode mode)
3099 {
3100     m_clipboard->setData(mode, data);
3101 }
3102 
setClipboardAndSelection(const QVariantMap & data)3103 void MainWindow::setClipboardAndSelection(const QVariantMap &data)
3104 {
3105     setClipboard(data);
3106 }
3107 
moveToClipboard(ClipboardBrowser * c,int row)3108 void MainWindow::moveToClipboard(ClipboardBrowser *c, int row)
3109 {
3110     const auto index = c ? c->index(row) : QModelIndex();
3111     if ( index.isValid() )
3112         c->moveToClipboard(index);
3113     else
3114         setClipboard(QVariantMap());
3115 }
3116 
getClipboardData(ClipboardMode mode)3117 const QMimeData *MainWindow::getClipboardData(ClipboardMode mode)
3118 {
3119     return m_clipboard->mimeData(mode);
3120 }
3121 
activateCurrentItem()3122 void MainWindow::activateCurrentItem()
3123 {
3124     // Omit activating item multiple times in quick succession.
3125     if (m_activatingItem)
3126         return;
3127 
3128     m_activatingItem = true;
3129     activateCurrentItemHelper();
3130     m_activatingItem = false;
3131 }
3132 
activateCurrentItemHelper()3133 void MainWindow::activateCurrentItemHelper()
3134 {
3135     if ( QApplication::queryKeyboardModifiers() == Qt::NoModifier
3136          && isItemMenuDefaultActionValid() )
3137     {
3138         m_menuItem->defaultAction()->trigger();
3139         return;
3140     }
3141 
3142     auto c = browser();
3143     if (!c)
3144         return;
3145 
3146     // Perform custom actions on item activation.
3147     PlatformWindowPtr lastWindow = m_lastWindow;
3148     const bool paste = lastWindow && m_options.activatePastes() && canPaste();
3149     const bool activateWindow = paste || (lastWindow && m_options.activateFocuses());
3150 
3151     // Copy current item or selection to clipboard.
3152     // While clipboard is being set (in separate process)
3153     // activate target window for pasting.
3154     c->moveToClipboard();
3155 
3156     if ( m_options.activateCloses() )
3157         hideWindow();
3158 
3159     if (activateWindow)
3160         lastWindow->raise();
3161 
3162     enterBrowseMode();
3163 
3164     if (paste) {
3165         COPYQ_LOG( QString("Pasting item from main window to \"%1\".")
3166                    .arg(lastWindow->getTitle()) );
3167         lastWindow->pasteClipboard();
3168     }
3169 }
3170 
onItemClicked()3171 void MainWindow::onItemClicked()
3172 {
3173     if (m_singleClickActivate)
3174         activateCurrentItem();
3175 }
3176 
onItemDoubleClicked()3177 void MainWindow::onItemDoubleClicked()
3178 {
3179     if (!m_singleClickActivate)
3180         activateCurrentItem();
3181 }
3182 
disableClipboardStoring(bool disable)3183 void MainWindow::disableClipboardStoring(bool disable)
3184 {
3185     if (m_clipboardStoringDisabled == disable)
3186         return;
3187 
3188     m_clipboardStoringDisabled = disable;
3189     emit disableClipboardStoringRequest(disable);
3190 
3191     updateMonitoringActions();
3192 
3193     ::setSessionIconEnabled(!disable);
3194 
3195     updateIcon();
3196 
3197     if (m_clipboardStoringDisabled)
3198         runScript("setTitle(); showDataNotification()");
3199 
3200     COPYQ_LOG( QString("Clipboard monitoring %1.")
3201                .arg(m_clipboardStoringDisabled ? "disabled" : "enabled") );
3202 }
3203 
isMonitoringEnabled() const3204 bool MainWindow::isMonitoringEnabled() const
3205 {
3206     return !m_clipboardStoringDisabled;
3207 }
3208 
toggleClipboardStoring()3209 void MainWindow::toggleClipboardStoring()
3210 {
3211     disableClipboardStoring(!m_clipboardStoringDisabled);
3212 }
3213 
tabs() const3214 QStringList MainWindow::tabs() const
3215 {
3216     return ui->tabWidget->tabs();
3217 }
3218 
getPlaceholderForMenu()3219 ClipboardBrowserPlaceholder *MainWindow::getPlaceholderForMenu()
3220 {
3221     const auto i = findTabIndex(m_menuTabName);
3222     return i != -1 ? getPlaceholder(i) : nullptr;
3223 }
3224 
getPlaceholderForTrayMenu()3225 ClipboardBrowserPlaceholder *MainWindow::getPlaceholderForTrayMenu()
3226 {
3227     if (m_options.trayCurrentTab)
3228         return getPlaceholder();
3229 
3230     if ( m_options.trayTabName.isEmpty() )
3231         return m_options.clipboardTab.isEmpty() ? nullptr : getPlaceholder(m_options.clipboardTab);
3232 
3233     int i = findTabIndex(m_options.trayTabName);
3234     return i != -1 ? getPlaceholder(i) : nullptr;
3235 }
3236 
onFilterChanged()3237 void MainWindow::onFilterChanged()
3238 {
3239     ItemFilterPtr filter = ui->searchBar->filter();
3240     if ( filter->matchesAll() )
3241         enterBrowseMode();
3242     else if ( browseMode() )
3243         enterSearchMode();
3244 
3245     auto c = browser();
3246     if (c)
3247         c->filterItems(filter);
3248     updateItemPreviewAfterMs(2 * itemPreviewUpdateIntervalMsec);
3249 }
3250 
raiseLastWindowAfterMenuClosed()3251 void MainWindow::raiseLastWindowAfterMenuClosed()
3252 {
3253     if ( m_lastWindow && !isAnyApplicationWindowActive() )
3254         m_lastWindow->raise();
3255 }
3256 
updateFocusWindows()3257 void MainWindow::updateFocusWindows()
3258 {
3259     m_isActiveWindow = isActiveWindow();
3260 
3261     if ( QApplication::activePopupWidget() )
3262         return;
3263 
3264     auto platform = platformNativeInterface();
3265     PlatformWindowPtr lastWindow = platform->getCurrentWindow();
3266     if (lastWindow) {
3267         const QString title = lastWindow->getTitle();
3268         const QWidget *activeWindow = qApp->activeWindow();
3269         if (activeWindow) {
3270             if (activeWindow == m_trayMenu || activeWindow == m_menu) {
3271                 COPYQ_LOG(QString("Focus window is \"%1\" - tray menu - ignoring").arg(title) );
3272             } else {
3273                 COPYQ_LOG(QString("Focus window is \"%1\": [%2] %3").arg(
3274                     title,
3275                     QLatin1String(activeWindow->metaObject()->className()),
3276                     activeWindow->windowTitle()
3277                 ));
3278                 m_lastWindow = lastWindow;
3279             }
3280         } else {
3281             COPYQ_LOG( QString("Focus window is \"%1\"").arg(title) );
3282             m_lastWindow = lastWindow;
3283         }
3284     }
3285 
3286     if (m_options.closeOnUnfocus)
3287         m_timerHideWindowIfNotActive.start();
3288 }
3289 
updateShortcuts()3290 void MainWindow::updateShortcuts()
3291 {
3292     if ( m_timerUpdateContextMenu.isActive() ) {
3293         m_timerUpdateContextMenu.stop();
3294         updateContextMenuTimeout();
3295     }
3296 }
3297 
findNextOrPrevious()3298 void MainWindow::findNextOrPrevious()
3299 {
3300     if (browseMode()) {
3301         enterSearchMode();
3302     } else {
3303         auto c = browser();
3304         if (!c)
3305             return;
3306 
3307         const bool next = !QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier);
3308         if ( c->isInternalEditorOpen() ) {
3309             ui->searchBar->setFocus(Qt::ShortcutFocusReason);
3310             if (next)
3311                 c->findNext();
3312             else
3313                 c->findPrevious();
3314         } else {
3315             c->setFocus();
3316             c->setCurrent( c->currentIndex().row() + (next ? 1 : -1) );
3317         }
3318     }
3319 }
3320 
enterBrowseMode()3321 void MainWindow::enterBrowseMode()
3322 {
3323     getPlaceholder()->setFocus();
3324     ui->searchBar->hide();
3325 
3326     auto c = browserOrNull();
3327     if (c)
3328         c->filterItems(nullptr);
3329 }
3330 
enterSearchMode()3331 void MainWindow::enterSearchMode()
3332 {
3333     ui->searchBar->show();
3334     ui->searchBar->setFocus(Qt::ShortcutFocusReason);
3335 
3336     if ( !ui->searchBar->text().isEmpty() ) {
3337         auto c = browserOrNull();
3338         if (c) {
3339             const int currentRow = c->currentIndex().row();
3340             c->filterItems( ui->searchBar->filter() );
3341             c->setCurrent(currentRow);
3342         }
3343     }
3344 }
3345 
enterSearchMode(const QString & txt)3346 void MainWindow::enterSearchMode(const QString &txt)
3347 {
3348     const bool searchModeActivated = !ui->searchBar->isVisible();
3349 
3350     ui->searchBar->show();
3351     ui->searchBar->setFocus(Qt::ShortcutFocusReason);
3352 
3353     if (searchModeActivated)
3354         ui->searchBar->setText(txt);
3355     else
3356         ui->searchBar->setText( ui->searchBar->text() + txt );
3357 
3358     auto c = browser();
3359     if (c)
3360         c->filterItems( ui->searchBar->filter() );
3361 }
3362 
updateTrayMenuItemsTimeout()3363 void MainWindow::updateTrayMenuItemsTimeout()
3364 {
3365     if (!m_trayMenuDirty)
3366         return;
3367 
3368     // Update tray only if not currently visible.
3369     if ( m_trayMenu->isVisible() ) {
3370         updateTrayMenuItems();
3371         return;
3372     }
3373 
3374     m_trayMenuDirty = false;
3375 
3376     COPYQ_LOG("Updating tray menu");
3377 
3378     WidgetSizeGuard sizeGuard(m_trayMenu);
3379 
3380     interruptMenuCommandFilters(&m_trayMenuMatchCommands);
3381     m_trayMenu->clearClipboardItems();
3382 
3383     filterTrayMenuItems(QString());
3384 }
3385 
filterMenuItems(const QString & searchText)3386 void MainWindow::filterMenuItems(const QString &searchText)
3387 {
3388     addMenuItems(m_menu, getPlaceholderForMenu(), m_menuMaxItemCount, searchText);
3389 }
3390 
filterTrayMenuItems(const QString & searchText)3391 void MainWindow::filterTrayMenuItems(const QString &searchText)
3392 {
3393     addMenuItems(m_trayMenu, getPlaceholderForTrayMenu(), m_options.trayItems, searchText);
3394     m_trayMenu->markItemInClipboard(m_clipboardData);
3395 }
3396 
openLogDialog()3397 void MainWindow::openLogDialog()
3398 {
3399     openDialog<LogDialog>(this);
3400 }
3401 
openAboutDialog()3402 void MainWindow::openAboutDialog()
3403 {
3404     openDialog<AboutDialog>(this);
3405 }
3406 
showClipboardContent()3407 void MainWindow::showClipboardContent()
3408 {
3409     ClipboardDialog *clipboardDialog = openDialog<ClipboardDialog>(this);
3410     connect( clipboardDialog, &ClipboardDialog::changeClipboard,
3411              this, &MainWindow::setClipboardAndSelection );
3412 }
3413 
showProcessManagerDialog()3414 void MainWindow::showProcessManagerDialog()
3415 {
3416     m_sharedData->actions->showProcessManagerDialog(this);
3417 }
3418 
openActionDialog(const QVariantMap & data)3419 ActionDialog *MainWindow::openActionDialog(const QVariantMap &data)
3420 {
3421     auto actionDialog = openDialog<ActionDialog>(this);
3422     actionDialog->setInputData(data);
3423 
3424     const auto tabs = ui->tabWidget->tabs();
3425     actionDialog->setOutputTabs(tabs);
3426 
3427     const int currentTabIndex = ui->tabWidget->currentIndex();
3428     if (currentTabIndex >= 0) {
3429         const auto currentTab = ui->tabWidget->tabName(currentTabIndex);
3430         actionDialog->setCurrentTab(currentTab);
3431     }
3432 
3433     connect( actionDialog, &ActionDialog::commandAccepted,
3434              this, &MainWindow::onActionDialogAccepted );
3435 
3436     connect( actionDialog, &ActionDialog::saveCommand,
3437              this, &MainWindow::onSaveCommand );
3438 
3439     stealFocus(*actionDialog);
3440 
3441     return actionDialog;
3442 }
3443 
openActionDialog()3444 void MainWindow::openActionDialog()
3445 {
3446     auto c = browser();
3447     const auto data = c ? addSelectionData(*c) : QVariantMap();
3448     openActionDialog(data);
3449 }
3450 
showItemContent()3451 void MainWindow::showItemContent()
3452 {
3453     auto c = browser( ui->tabWidget->currentIndex() );
3454     if (!c)
3455         return;
3456 
3457     const QModelIndex current = c->currentIndex();
3458     if ( current.isValid() )
3459         openDialog<ClipboardDialog>(current, c->model(), this);
3460 }
3461 
openPreferences()3462 void MainWindow::openPreferences()
3463 {
3464     if ( !isEnabled() )
3465         return;
3466 
3467     if (cm) {
3468         cm->activateWindow();
3469         return;
3470     }
3471 
3472     ConfigurationManager configurationManager(m_sharedData->itemFactory, this);
3473     WindowGeometryGuard::create(&configurationManager);
3474 
3475     // notify window if configuration changes
3476     connect( &configurationManager, &ConfigurationManager::configurationChanged,
3477              this, &MainWindow::configurationChanged );
3478     connect( &configurationManager, &ConfigurationManager::error,
3479              this, &MainWindow::showError );
3480 
3481     // WORKAROUND: Fix drag'n'drop in list in modal dialog for Qt 5.9.2 (QTBUG-63846).
3482     configurationManager.setWindowModality(Qt::WindowModal);
3483 
3484     cm = &configurationManager;
3485     configurationManager.exec();
3486     cm = nullptr;
3487 }
3488 
openCommands()3489 void MainWindow::openCommands()
3490 {
3491     if ( !isEnabled() )
3492         return;
3493 
3494     if (m_commandDialog) {
3495         m_commandDialog->show();
3496         m_commandDialog->activateWindow();
3497     } else {
3498         const QVector<Command> pluginCommands = m_sharedData->itemFactory->commands();
3499         QStringList formats = m_sharedData->itemFactory->formatsToSave();
3500         formats.prepend(mimeText);
3501         formats.removeDuplicates();
3502 
3503         QWidget *parent = this;
3504         if (cm)
3505             parent = cm;
3506 
3507         m_commandDialog = openDialog<CommandDialog>(pluginCommands, formats, parent);
3508         connect(this, &QObject::destroyed, m_commandDialog.data(), &QWidget::close);
3509         connect(m_commandDialog.data(), &CommandDialog::commandsSaved, this, &MainWindow::updateEnabledCommands);
3510     }
3511 
3512     if (cm && cm->isVisible())
3513         m_commandDialog->setWindowModality(Qt::ApplicationModal);
3514 }
3515 
browser(int index)3516 ClipboardBrowser *MainWindow::browser(int index)
3517 {
3518     return getPlaceholder(index)->createBrowser();
3519 }
3520 
browser()3521 ClipboardBrowser *MainWindow::browser()
3522 {
3523     return browser( ui->tabWidget->currentIndex() );
3524 }
3525 
browserOrNull()3526 ClipboardBrowser *MainWindow::browserOrNull()
3527 {
3528     const ClipboardBrowserPlaceholder *currentPlaceholder = getPlaceholder();
3529     return currentPlaceholder ? currentPlaceholder->browser() : nullptr;
3530 }
3531 
browserForItem(const QModelIndex & index)3532 ClipboardBrowser *MainWindow::browserForItem(const QModelIndex &index)
3533 {
3534     if ( index.isValid() ) {
3535         auto c = qobject_cast<ClipboardBrowser*>(index.model()->parent());
3536         Q_ASSERT(c);
3537         return c;
3538     }
3539 
3540     return nullptr;
3541 }
3542 
addAndFocusTab(const QString & name)3543 void MainWindow::addAndFocusTab(const QString &name)
3544 {
3545     auto placeholder = createTab(name, MatchExactTabName, Tabs());
3546     if (!placeholder)
3547         return;
3548 
3549     int i = 0;
3550     for( ; i < ui->tabWidget->count() && getPlaceholder(i) != placeholder; ++i ) {}
3551     setCurrentTab(i);
3552 
3553     saveTabPositions();
3554 }
3555 
editNewItem()3556 void MainWindow::editNewItem()
3557 {
3558     auto c = browser( ui->tabWidget->currentIndex() );
3559     if (!c)
3560         return;
3561 
3562     showWindow();
3563     if ( !c->isInternalEditorOpen() ) {
3564         c->setFocus();
3565         c->editNew();
3566     }
3567 }
3568 
pasteItems()3569 void MainWindow::pasteItems()
3570 {
3571     const QMimeData *data = m_clipboard->mimeData(ClipboardMode::Clipboard);
3572     if (data == nullptr)
3573         return;
3574 
3575     auto c = browser();
3576     if (!c)
3577         return;
3578 
3579     QModelIndexList list = c->selectionModel()->selectedIndexes();
3580     std::sort( list.begin(), list.end() );
3581     const int row = list.isEmpty() ? 0 : list.first().row();
3582     c->addAndSelect( cloneData(*data), row );
3583 }
3584 
copyItems()3585 void MainWindow::copyItems()
3586 {
3587     auto c = browser();
3588     if (!c)
3589         return;
3590 
3591     const QModelIndexList indexes = c->selectionModel()->selectedRows();
3592     if ( indexes.isEmpty() )
3593         return;
3594 
3595     const auto data = c->copyIndexes(indexes);
3596     setClipboard(data);
3597 }
3598 
saveTab(const QString & fileName,int tabIndex)3599 bool MainWindow::saveTab(const QString &fileName, int tabIndex)
3600 {
3601     QFile file(fileName);
3602     if ( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) )
3603         return false;
3604 
3605     QDataStream out(&file);
3606     out.setVersion(QDataStream::Qt_4_7);
3607 
3608     int i = tabIndex >= 0 ? tabIndex : ui->tabWidget->currentIndex();
3609     auto c = browser(i);
3610     if (!c)
3611         return false;
3612 
3613     out << QByteArray("CopyQ v2") << c->tabName();
3614     serializeData(*c->model(), &out);
3615 
3616     file.close();
3617 
3618     return true;
3619 }
3620 
exportData()3621 bool MainWindow::exportData()
3622 {
3623     ImportExportDialog exportDialog(this);
3624     exportDialog.setWindowTitle( tr("Options for Export") );
3625     exportDialog.setTabs( ui->tabWidget->tabs() );
3626     if ( !ui->tabWidget->isTabGroupSelected() )
3627         exportDialog.setCurrentTab( getPlaceholder()->tabName() );
3628     if ( exportDialog.exec() != QDialog::Accepted )
3629         return false;
3630 
3631     auto fileName = QFileDialog::getSaveFileName(
3632                 this, QString(), QString(), importExportFileDialogFilter() );
3633     if ( fileName.isNull() )
3634         return false;
3635 
3636     if ( !fileName.endsWith(".cpq") )
3637         fileName.append(".cpq");
3638 
3639     const auto tabs = exportDialog.selectedTabs();
3640     const bool exportConfiguration = exportDialog.isConfigurationEnabled();
3641     const bool exportCommands = exportDialog.isCommandsEnabled();
3642 
3643     if ( !exportDataFrom(fileName, tabs, exportConfiguration, exportCommands) ) {
3644         QMessageBox::critical(
3645                     this, tr("Export Error"),
3646                     tr("Failed to export file %1!")
3647                     .arg(quoteString(fileName)) );
3648         return false;
3649     }
3650 
3651     return true;
3652 }
3653 
saveTabs()3654 void MainWindow::saveTabs()
3655 {
3656     for( int i = 0; i < ui->tabWidget->count(); ++i ) {
3657         auto c = getPlaceholder(i)->browser();
3658         if (c)
3659             c->saveUnsavedItems();
3660     }
3661     ui->tabWidget->saveTabInfo();
3662 }
3663 
loadTab(const QString & fileName)3664 bool MainWindow::loadTab(const QString &fileName)
3665 {
3666     QFile file(fileName);
3667     if ( !file.open(QIODevice::ReadOnly) )
3668         return false;
3669 
3670     QDataStream in(&file);
3671     in.setVersion(QDataStream::Qt_4_7);
3672 
3673     QByteArray header;
3674     QString tabName;
3675     in >> header >> tabName;
3676     if ( !(header.startsWith("CopyQ v1") || header.startsWith("CopyQ v2")) || tabName.isEmpty() ) {
3677         file.close();
3678         return false;
3679     }
3680 
3681     // Find unique tab name.
3682     renameToUnique(&tabName, ui->tabWidget->tabs());
3683 
3684     auto c = createTab(tabName, MatchExactTabName, Tabs())->createBrowser();
3685     if (!c)
3686         return false;
3687 
3688     deserializeData(c->model(), &in, m_sharedData->maxItems);
3689 
3690     c->loadItems();
3691     c->saveItems();
3692 
3693     file.close();
3694 
3695     ui->tabWidget->setCurrentIndex( ui->tabWidget->count() - 1 );
3696 
3697     return true;
3698 }
3699 
importDataFrom(const QString & fileName,ImportOptions options)3700 bool MainWindow::importDataFrom(const QString &fileName, ImportOptions options)
3701 {
3702     // Compatibility with v2.9.0 and earlier.
3703     if ( loadTab(fileName) )
3704         return true;
3705 
3706     QFile file(fileName);
3707     if ( !file.open(QIODevice::ReadOnly) )
3708         return false;
3709 
3710     QDataStream in(&file);
3711     in.setVersion(QDataStream::Qt_4_7);
3712 
3713     if ( importDataV4(&in, options) )
3714         return true;
3715 
3716     file.seek(0);
3717     return importDataV3(&in, options);
3718 }
3719 
exportAllData(const QString & fileName)3720 bool MainWindow::exportAllData(const QString &fileName)
3721 {
3722     const auto tabs = ui->tabWidget->tabs();
3723     const bool exportConfiguration = true;
3724     const bool exportCommands = true;
3725 
3726     return exportDataFrom(fileName, tabs, exportConfiguration, exportCommands);
3727 }
3728 
importData()3729 bool MainWindow::importData()
3730 {
3731     const auto fileName = QFileDialog::getOpenFileName(
3732                 this, QString(), QString(), importExportFileDialogFilter() );
3733     if ( fileName.isNull() )
3734         return false;
3735 
3736     if ( !importDataFrom(fileName, ImportOptions::Select) ) {
3737         QMessageBox::critical(
3738                     this, tr("Import Error"),
3739                     tr("Failed to import file %1!")
3740                     .arg(quoteString(fileName)) );
3741         return false;
3742     }
3743 
3744     return true;
3745 }
3746 
sortSelectedItems()3747 void MainWindow::sortSelectedItems()
3748 {
3749     auto c = browser();
3750     if (c)
3751         c->sortItems( c->selectionModel()->selectedRows() );
3752 }
3753 
reverseSelectedItems()3754 void MainWindow::reverseSelectedItems()
3755 {
3756     auto c = browser();
3757     if (c)
3758         c->reverseItems( c->selectionModel()->selectedRows() );
3759 }
3760 
action(const QVariantMap & data,const Command & cmd,const QModelIndex & outputIndex)3761 Action *MainWindow::action(const QVariantMap &data, const Command &cmd, const QModelIndex &outputIndex)
3762 {
3763     if (cmd.wait) {
3764         auto actionDialog = openActionDialog(data);
3765         if ( !cmd.outputTab.isEmpty() )
3766             actionDialog->setCurrentTab(cmd.outputTab);
3767         actionDialog->setCommand(cmd);
3768     } else if ( cmd.cmd.isEmpty() ) {
3769         m_sharedData->actions->addFinishedAction(cmd.name);
3770     } else {
3771         auto act = new Action();
3772         act->setCommand( cmd.cmd, QStringList(getTextData(data)) );
3773         act->setInputWithFormat(data, cmd.input);
3774         act->setName(cmd.name);
3775         act->setData(data);
3776 
3777         if ( !cmd.output.isEmpty() ) {
3778             if ( outputIndex.isValid() )
3779                 actionOutput(this, act, cmd.output, outputIndex);
3780             else if ( !cmd.sep.isEmpty() )
3781                 actionOutput(this, act, cmd.output, cmd.outputTab, QRegularExpression(cmd.sep));
3782             else
3783                 actionOutput(this, act, cmd.output, cmd.outputTab);
3784         }
3785 
3786         m_sharedData->actions->action(act);
3787         return act;
3788     }
3789 
3790     return nullptr;
3791 }
3792 
triggerMenuCommand(const Command & command,const QString & triggeredShortcut)3793 bool MainWindow::triggerMenuCommand(const Command &command, const QString &triggeredShortcut)
3794 {
3795     updateShortcuts();
3796 
3797     Command cmd = command;
3798     for (auto act : m_menuItem->findChildren<CommandAction*>()) {
3799         if ( !act->isEnabled() || !act->isVisible() )
3800             continue;
3801 
3802         const Command &menuCommand = act->command();
3803         // Ignore outputTab value overridden in the action.
3804         cmd.outputTab = menuCommand.outputTab;
3805         if (cmd == menuCommand) {
3806             onItemCommandActionTriggered(act, triggeredShortcut);
3807             return true;
3808         }
3809     }
3810     return false;
3811 }
3812 
runInternalAction(Action * action)3813 void MainWindow::runInternalAction(Action *action)
3814 {
3815     m_sharedData->actions->internalAction(action);
3816 }
3817 
isInternalActionId(int id) const3818 bool MainWindow::isInternalActionId(int id) const
3819 {
3820     return id != -1 && m_sharedData->actions->isInternalActionId(id);
3821 }
3822 
openNewTabDialog(const QString & name)3823 void MainWindow::openNewTabDialog(const QString &name)
3824 {
3825     auto d = new TabDialog(TabDialog::TabNew, this);
3826     d->setAttribute(Qt::WA_DeleteOnClose, true);
3827     d->setTabs(ui->tabWidget->tabs());
3828 
3829     d->setTabName(name);
3830 
3831     connect( d, &TabDialog::newTabNameAccepted,
3832              this, &MainWindow::addAndFocusTab );
3833 
3834     d->open();
3835 }
3836 
openNewTabDialog()3837 void MainWindow::openNewTabDialog()
3838 {
3839     QString tabPath = ui->tabWidget->getCurrentTabPath();
3840     if ( ui->tabWidget->isTabGroup(tabPath) )
3841         tabPath.append('/');
3842 
3843     openNewTabDialog(tabPath);
3844 }
3845 
openRenameTabGroupDialog(const QString & name)3846 void MainWindow::openRenameTabGroupDialog(const QString &name)
3847 {
3848     auto d = new TabDialog(TabDialog::TabGroupRename, this);
3849     d->setAttribute(Qt::WA_DeleteOnClose, true);
3850     d->setTabs(ui->tabWidget->tabs());
3851     d->setTabGroupName(name);
3852 
3853     connect( d, &TabDialog::treeTabNameAccepted,
3854              this, &MainWindow::renameTabGroup );
3855 
3856     d->open();
3857 }
3858 
renameTabGroup(const QString & newName,const QString & oldName)3859 void MainWindow::renameTabGroup(const QString &newName, const QString &oldName)
3860 {
3861     const QStringList tabs = ui->tabWidget->tabs();
3862     const QString tabPrefix = oldName + '/';
3863 
3864     for ( int i = 0; i < tabs.size(); ++i ) {
3865         const QString &tab = tabs[i];
3866         if ( tab == oldName || tab.startsWith(tabPrefix) )
3867             renameTab( newName + tab.mid(oldName.size()), i );
3868     }
3869 }
3870 
openRenameTabDialog(int tabIndex)3871 void MainWindow::openRenameTabDialog(int tabIndex)
3872 {
3873     auto d = new TabDialog(TabDialog::TabRename, this);
3874     d->setAttribute(Qt::WA_DeleteOnClose, true);
3875     d->setTabIndex(tabIndex);
3876     d->setTabs(ui->tabWidget->tabs());
3877     d->setTabName( getPlaceholder(tabIndex)->tabName() );
3878 
3879     connect( d, &TabDialog::barTabNameAccepted,
3880              this, &MainWindow::renameTab );
3881 
3882     d->open();
3883 }
3884 
openRenameTabDialog()3885 void MainWindow::openRenameTabDialog()
3886 {
3887     const int tabIndex = ui->tabWidget->currentIndex();
3888     if (tabIndex >= 0)
3889         openRenameTabDialog(tabIndex);
3890 }
3891 
renameTab(const QString & name,int tabIndex)3892 void MainWindow::renameTab(const QString &name, int tabIndex)
3893 {
3894     if ( name.isEmpty() || ui->tabWidget->tabs().contains(name) )
3895         return;
3896 
3897     auto placeholder = getPlaceholder(tabIndex);
3898     if (placeholder) {
3899         const QString oldName = placeholder->tabName();
3900         if ( placeholder->setTabName(name) ) {
3901             updateTabIcon(name, oldName);
3902             ui->tabWidget->setTabName(tabIndex, name);
3903             saveTabPositions();
3904 
3905             QVariantList optionsAndValues;
3906             if (oldName == m_options.clipboardTab) {
3907                 optionsAndValues.append(QStringLiteral("clipboard_tab"));
3908                 optionsAndValues.append(name);
3909             }
3910             if (oldName == m_options.trayTabName) {
3911                 optionsAndValues.append(QStringLiteral("tray_tab"));
3912                 optionsAndValues.append(name);
3913             }
3914             if ( !optionsAndValues.isEmpty() )
3915                 config(optionsAndValues);
3916         }
3917     }
3918 }
3919 
removeTabGroup(const QString & name)3920 void MainWindow::removeTabGroup(const QString &name)
3921 {
3922     int answer = QMessageBox::question(
3923                 this,
3924                 tr("Remove All Tabs in Group?"),
3925                 tr("Do you want to remove <strong>all tabs</strong> in group <strong>%1</strong>?"
3926                    ).arg(name),
3927                 QMessageBox::Yes | QMessageBox::No,
3928                 QMessageBox::Yes);
3929     if (answer == QMessageBox::Yes) {
3930         ui->tabWidget->setCurrentIndex(0);
3931         const QStringList tabs = ui->tabWidget->tabs();
3932         const QString tabPrefix = name + '/';
3933         const int currentTabIndex = ui->tabWidget->currentIndex();
3934 
3935         for ( int i = tabs.size() - 1; i >= 0; --i ) {
3936             const QString &tab = tabs[i];
3937             if ( tab == name || tab.startsWith(tabPrefix) ) {
3938                 if (currentTabIndex == i)
3939                     ui->tabWidget->setCurrentIndex(0);
3940 
3941                 auto placeholder = getPlaceholder(i);
3942                 placeholder->removeItems();
3943                 placeholder->deleteLater();
3944 
3945                 ui->tabWidget->removeTab(i);
3946             }
3947         }
3948 
3949         saveTabPositions();
3950     }
3951 }
3952 
removeTab()3953 void MainWindow::removeTab()
3954 {
3955     removeTab(true, ui->tabWidget->currentIndex());
3956 }
3957 
removeTab(bool ask,int tabIndex)3958 void MainWindow::removeTab(bool ask, int tabIndex)
3959 {
3960     if (tabIndex < 0)
3961         return;
3962 
3963     TabWidget *w = ui->tabWidget;
3964 
3965     auto placeholder = getPlaceholder(tabIndex);
3966 
3967     if ( placeholder != nullptr && w->count() > 1 ) {
3968         int answer = QMessageBox::Yes;
3969         if (ask) {
3970             answer = QMessageBox::question(
3971                         this,
3972                         tr("Remove Tab?"),
3973                         tr("Do you want to remove tab <strong>%1</strong>?"
3974                            ).arg( w->tabName(tabIndex).remove('&') ),
3975                         QMessageBox::Yes | QMessageBox::No,
3976                         QMessageBox::Yes);
3977         }
3978         if (answer == QMessageBox::Yes) {
3979             placeholder->removeItems();
3980             placeholder->deleteLater();
3981             w->removeTab(tabIndex);
3982             saveTabPositions();
3983         }
3984     }
3985 }
3986 
setTabIcon()3987 void MainWindow::setTabIcon()
3988 {
3989     if ( ui->tabWidget->isTabGroupSelected() )
3990         setTabIcon( ui->tabWidget->getCurrentTabPath() );
3991     else
3992         setTabIcon( getPlaceholder()->tabName() );
3993 }
3994 
setTabIcon(const QString & tabName)3995 void MainWindow::setTabIcon(const QString &tabName)
3996 {
3997     IconSelectDialog dialog( getIconNameForTabName(tabName), this );
3998 
3999     if ( dialog.exec() == QDialog::Accepted )
4000         setTabIcon(tabName, dialog.selectedIcon());
4001 }
4002 
setTabIcon(const QString & tabName,const QString & icon)4003 void MainWindow::setTabIcon(const QString &tabName, const QString &icon)
4004 {
4005     if ( tabs().contains(tabName) || ui->tabWidget->isTabGroup(tabName) ) {
4006         setIconNameForTabName(tabName, icon);
4007         ui->tabWidget->updateTabIcon(tabName);
4008     }
4009 }
4010 
unloadTab(const QString & tabName)4011 bool MainWindow::unloadTab(const QString &tabName)
4012 {
4013     ClipboardBrowserPlaceholder *placeholder = getPlaceholder(tabName);
4014     return !placeholder || placeholder->expire();
4015 }
4016 
forceUnloadTab(const QString & tabName)4017 void MainWindow::forceUnloadTab(const QString &tabName)
4018 {
4019     ClipboardBrowserPlaceholder *placeholder = getPlaceholder(tabName);
4020     if (!placeholder)
4021         return;
4022 
4023     placeholder->unloadBrowser();
4024 
4025     placeholder->createLoadButton();
4026 }
4027 
~MainWindow()4028 MainWindow::~MainWindow()
4029 {
4030     delete ui;
4031 }
4032