/*
Copyright (c) 2017, Lukas Holecek <hluk@email.cz>
This file is part of CopyQ.
CopyQ is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CopyQ is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CopyQ. If not, see <http://www.gnu.org/licenses/>.
*/
#include "traymenu.h"
#include "common/contenttype.h"
#include "common/common.h"
#include "common/display.h"
#include "gui/icons.h"
#include "gui/iconfactory.h"
#include "platform/platformnativeinterface.h"
#include "platform/platformwindow.h"
#include <QApplication>
#include <QKeyEvent>
#include <QModelIndex>
#include <QPixmap>
namespace {
bool canActivate(const QAction &action)
{
return !action.isSeparator() && action.isEnabled();
}
QAction *firstEnabledAction(QMenu *menu)
{
// First action is active when menu pops up.
for (auto action : menu->actions()) {
if ( canActivate(*action) )
return action;
}
return nullptr;
}
QAction *lastEnabledAction(QMenu *menu)
{
// First action is active when menu pops up.
QList<QAction *> actions = menu->actions();
for (int i = actions.size() - 1; i >= 0; --i) {
if ( canActivate(*actions[i]) )
return actions[i];
}
return nullptr;
}
} // namespace
TrayMenu::TrayMenu(QWidget *parent)
: QMenu(parent)
, m_clipboardItemActionsSeparator()
, m_customActionsSeparator()
, m_clipboardItemActionCount(0)
, m_omitPaste(false)
, m_viMode(false)
{
initSingleShotTimer( &m_timerUpdateActiveAction, 0, this, SLOT(updateActiveAction()) );
}
void TrayMenu::addClipboardItemAction(const QModelIndex &index, bool showImages, bool isCurrent)
{
resetSeparators();
// Show search text at top of the menu.
if ( m_clipboardItemActionCount == 0 && m_searchText.isEmpty() )
setSearchMenuItem( m_viMode ? tr("Press '/' to search") : tr("Type to search") );
const QVariantMap data = index.data(contentType::data).toMap();
QAction *act = addAction(QString());
act->setData(index.data(contentType::hash));
insertAction(m_clipboardItemActionsSeparator, act);
QString format;
// Add number key hint.
if (m_clipboardItemActionCount < 10) {
format = tr("&%1. %2",
"Key hint (number shortcut) for items in tray menu (%1 is number, %2 is item label)")
.arg(m_clipboardItemActionCount);
}
m_clipboardItemActionCount++;
const QString label = textLabelForData( data, act->font(), format, true );
act->setText(label);
// Menu item icon from image.
if (showImages) {
const QStringList formats = data.keys();
const int imageIndex = formats.indexOf( QRegExp("^image/.*") );
if (imageIndex != -1) {
const auto &mime = formats[imageIndex];
QPixmap pix;
pix.loadFromData( data.value(mime).toByteArray(), mime.toLatin1().data() );
const int iconSize = smallIconSize();
int x = 0;
int y = 0;
if (pix.width() > pix.height()) {
pix = pix.scaledToHeight(iconSize);
x = (pix.width() - iconSize) / 2;
} else {
pix = pix.scaledToWidth(iconSize);
y = (pix.height() - iconSize) / 2;
}
pix = pix.copy(x, y, iconSize, iconSize);
act->setIcon(pix);
}
}
connect(act, SIGNAL(triggered()), this, SLOT(onClipboardItemActionTriggered()));
if (isCurrent)
setActiveAction(act);
}
void TrayMenu::clearClipboardItems()
{
resetSeparators();
for ( auto action : actions() ) {
if (action->isSeparator())
break;
if (action != m_searchAction)
removeAction(action);
}
m_clipboardItemActionCount = 0;
// Show search text at top of the menu.
if ( !m_searchText.isEmpty() )
setSearchMenuItem(m_searchText);
}
void TrayMenu::addCustomAction(QAction *action)
{
resetSeparators();
insertAction(m_customActionsSeparator, action);
}
void TrayMenu::clearAllActions()
{
clear();
m_clipboardItemActionCount = 0;
m_searchText.clear();
}
void TrayMenu::setViModeEnabled(bool enabled)
{
m_viMode = enabled;
}
void TrayMenu::keyPressEvent(QKeyEvent *event)
{
const int key = event->key();
m_omitPaste = false;
if ( m_viMode && m_searchText.isEmpty() && handleViKey(event, this) ) {
return;
} else {
// Movement in tray menu.
switch (key) {
case Qt::Key_PageDown:
case Qt::Key_End: {
QAction *action = lastEnabledAction(this);
if (action != nullptr)
setActiveAction(action);
break;
}
case Qt::Key_PageUp:
case Qt::Key_Home: {
QAction *action = firstEnabledAction(this);
if (action != nullptr)
setActiveAction(action);
break;
}
case Qt::Key_Escape:
close();
break;
case Qt::Key_Backspace:
case Qt::Key_Delete:
search(QString());
break;
case Qt::Key_Alt:
return;
default:
// Type text for search.
if ( (m_clipboardItemActionCount > 0 || !m_searchText.isEmpty())
&& (!m_viMode || !m_searchText.isEmpty() || key == Qt::Key_Slash)
&& !event->modifiers().testFlag(Qt::AltModifier)
&& !event->modifiers().testFlag(Qt::ControlModifier) )
{
const QString txt = event->text();
if ( !txt.isEmpty() && txt[0].isPrint() ) {
// Activate item at row when number is entered.
if (m_searchText.isEmpty()) {
bool ok;
const int row = txt.toInt(&ok);
if (ok && row < m_clipboardItemActionCount) {
// Allow keypad digit to activate appropriate item in context menu.
if (event->modifiers() == Qt::KeypadModifier)
event->setModifiers(Qt::NoModifier);
break;
}
}
search(m_searchText + txt);
return;
}
}
break;
}
}
QMenu::keyPressEvent(event);
}
void TrayMenu::mousePressEvent(QMouseEvent *event)
{
m_omitPaste = (event->button() == Qt::RightButton);
QMenu::mousePressEvent(event);
}
void TrayMenu::showEvent(QShowEvent *event)
{
// If appmenu is used to handle the menu, most events won't be received
// so search won't work.
// This shows the search menu item only if show event is received.
if ( !m_searchAction.isNull() )
m_searchAction->setVisible(true);
QMenu::showEvent(event);
}
void TrayMenu::hideEvent(QHideEvent *event)
{
QMenu::hideEvent(event);
if ( !m_searchAction.isNull() )
m_searchAction->setVisible(false);
}
void TrayMenu::actionEvent(QActionEvent *event)
{
QMenu::actionEvent(event);
m_timerUpdateActiveAction.start();
}
void TrayMenu::leaveEvent(QEvent *event)
{
// Omit clearing active action if menu is resizes and mouse pointer leaves menu.
auto action = activeAction();
QMenu::leaveEvent(event);
setActiveAction(action);
}
void TrayMenu::resetSeparators()
{
if ( m_customActionsSeparator.isNull() ) {
QAction *firstAction = actions().value(0, nullptr);
m_customActionsSeparator = firstAction != nullptr ? insertSeparator(firstAction)
: addSeparator();
}
if ( m_clipboardItemActionsSeparator.isNull() )
m_clipboardItemActionsSeparator = insertSeparator(m_customActionsSeparator);
}
void TrayMenu::search(const QString &text)
{
if (m_searchText == text)
return;
m_searchText = text;
emit searchRequest(m_viMode ? m_searchText.mid(1) : m_searchText);
}
void TrayMenu::setSearchMenuItem(const QString &text)
{
if ( m_searchAction.isNull() ) {
const QIcon icon = getIcon("edit-find", IconSearch);
m_searchAction = new QAction(icon, text, this);
m_searchAction->setEnabled(false);
// Search menu item is hidden by default, see showEvent().
m_searchAction->setVisible( isVisible() );
insertAction( actions().value(0), m_searchAction );
} else {
m_searchAction->setText(text);
}
}
void TrayMenu::onClipboardItemActionTriggered()
{
QAction *act = qobject_cast<QAction *>(sender());
Q_ASSERT(act != nullptr);
QVariant actionData = act->data();
Q_ASSERT( actionData.isValid() );
uint hash = actionData.toUInt();
emit clipboardItemActionTriggered(hash, m_omitPaste);
close();
}
void TrayMenu::updateActiveAction()
{
const auto action = firstEnabledAction(this);
if (action != nullptr)
setActiveAction(action);
}