Blob Blame History Raw
/*
    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 "configurationmanager.h"
#include "ui_configurationmanager.h"

#include "common/appconfig.h"
#include "common/command.h"
#include "common/common.h"
#include "common/config.h"
#include "common/log.h"
#include "common/mimetypes.h"
#include "common/option.h"
#include "common/settings.h"
#include "gui/iconfactory.h"
#include "gui/icons.h"
#include "gui/pluginwidget.h"
#include "gui/tabicons.h"
#include "gui/windowgeometryguard.h"
#include "item/clipboardmodel.h"
#include "item/itemdelegate.h"
#include "item/itemfactory.h"
#include "item/itemwidget.h"
#include "platform/platformnativeinterface.h"

#include <QDesktopWidget>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QMimeData>
#include <QSettings>
#include <QTranslator>

namespace {

class PluginItem : public ItemOrderList::Item {
public:
    explicit PluginItem(const ItemLoaderPtr &loader)
        : m_loader(loader)
    {
    }

    QVariant data() const override { return m_loader->id(); }

private:
    QWidget *createWidget(QWidget *parent) const override
    {
        return new PluginWidget(m_loader, parent);
    }

    ItemLoaderPtr m_loader;
};

QString nativeLanguageName(const QString &localeName)
{
    // Traditional Chinese
    if (localeName == "zh_TW")
        return QString::fromUtf8("\xe6\xad\xa3\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87");

    // Simplified Chinese
    if (localeName == "zh_CN")
        return QString::fromUtf8("\xe7\xae\x80\xe4\xbd\x93\xe4\xb8\xad\xe6\x96\x87");

    return QLocale(localeName).nativeLanguageName();
}

} // namespace

ConfigurationManager::ConfigurationManager(ItemFactory *itemFactory, QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::ConfigurationManager)
    , m_options()
{
    ui->setupUi(this);
    setWindowIcon(appIcon());

    ui->spinBoxItems->setMaximum(Config::maxItems);

    if ( itemFactory && itemFactory->hasLoaders() )
        initPluginWidgets(itemFactory);
    else
        ui->tabItems->hide();

    initOptions();

    connect( ui->configTabShortcuts, SIGNAL(openCommandDialogRequest()),
             this, SIGNAL(openCommandDialogRequest()));

    if (itemFactory)
        ui->configTabAppearance->createPreview(itemFactory);

    loadSettings();
}

ConfigurationManager::ConfigurationManager()
    : ui(new Ui::ConfigurationManager)
    , m_options()
{
    ui->setupUi(this);
    initOptions();
}

ConfigurationManager::~ConfigurationManager()
{
    delete ui;
}

void ConfigurationManager::initTabIcons()
{
    QTabWidget *tw = ui->tabWidget;
    if ( !tw->tabIcon(0).isNull() )
        return;

    tw->setTabIcon( tw->indexOf(ui->tabGeneral), getIcon("", IconWrench) );
    tw->setTabIcon( tw->indexOf(ui->tabLayout), getIcon("", IconColumns) );
    tw->setTabIcon( tw->indexOf(ui->tabHistory), getIcon("", IconListAlt) );
    tw->setTabIcon( tw->indexOf(ui->tabItems), getIcon("", IconThList) );
    tw->setTabIcon( tw->indexOf(ui->tabTray), getIcon("", IconInbox) );
    tw->setTabIcon( tw->indexOf(ui->tabNotifications), getIcon("", IconInfoSign) );
    tw->setTabIcon( tw->indexOf(ui->tabShortcuts), getIcon("", IconKeyboard) );
    tw->setTabIcon( tw->indexOf(ui->tabAppearance), getIcon("", IconPicture) );
}

void ConfigurationManager::initPluginWidgets(ItemFactory *itemFactory)
{
    ui->itemOrderListPlugins->clearItems();

    for ( const auto &loader : itemFactory->loaders() ) {
        ItemOrderList::ItemPtr pluginItem(new PluginItem(loader));
        const QIcon icon = getIcon(loader->icon());
        ui->itemOrderListPlugins->appendItem(
                    loader->name(), itemFactory->isLoaderEnabled(loader), false, icon, pluginItem );
    }
}

void ConfigurationManager::initLanguages()
{
    ui->comboBoxLanguage->addItem("English");
    ui->comboBoxLanguage->setItemData(0, "en");

    const QString currentLocale = QLocale().name();
    bool currentLocaleFound = false; // otherwise not found or partial match ("uk" partially matches locale "uk_UA")
    QSet<QString> languages;

    for ( const auto &path : qApp->property("CopyQ_translation_directories").toStringList() ) {
        for ( const auto &item : QDir(path).entryList(QStringList("copyq_*.qm")) ) {
            const int i = item.indexOf('_');
            const QString locale = item.mid(i + 1, item.lastIndexOf('.') - i - 1);
            const QString language = nativeLanguageName(locale);

            if (!language.isEmpty()) {
                languages.insert(language);
                const int index = ui->comboBoxLanguage->count();
                ui->comboBoxLanguage->addItem(language);
                ui->comboBoxLanguage->setItemData(index, locale);

                if (!currentLocaleFound) {
                    currentLocaleFound = (locale == currentLocale);
                    if (currentLocaleFound || currentLocale.startsWith(locale + "_"))
                        ui->comboBoxLanguage->setCurrentIndex(index);
                }
            }
        }
    }

    ui->comboBoxLanguage->setSizeAdjustPolicy(QComboBox::AdjustToContents);
}

void ConfigurationManager::updateAutostart()
{
    PlatformPtr platform = createPlatformNativeInterface();

    if ( platform->canAutostart() ) {
        bind<Config::autostart>(ui->checkBoxAutostart);
    } else {
        ui->checkBoxAutostart->hide();
    }
}

void ConfigurationManager::setAutostartEnable()
{
    PlatformPtr platform = createPlatformNativeInterface();
    platform->setAutostartEnabled( AppConfig().option<Config::autostart>() );
}

void ConfigurationManager::initOptions()
{
    /* general options */
    bind<Config::autostart>(ui->checkBoxAutostart);
    bind<Config::clipboard_tab>(ui->comboBoxClipboardTab->lineEdit());
    bind<Config::maxitems>(ui->spinBoxItems);
    bind<Config::expire_tab>(ui->spinBoxExpireTab);
    bind<Config::editor>(ui->lineEditEditor);
    bind<Config::item_popup_interval>(ui->spinBoxNotificationPopupInterval);
    bind<Config::notification_position>(ui->comboBoxNotificationPosition);
    bind<Config::clipboard_notification_lines>(ui->spinBoxClipboardNotificationLines);
    bind<Config::notification_horizontal_offset>(ui->spinBoxNotificationHorizontalOffset);
    bind<Config::notification_vertical_offset>(ui->spinBoxNotificationVerticalOffset);
    bind<Config::notification_maximum_width>(ui->spinBoxNotificationMaximumWidth);
    bind<Config::notification_maximum_height>(ui->spinBoxNotificationMaximumHeight);
    bind<Config::edit_ctrl_return>(ui->checkBoxEditCtrlReturn);
    bind<Config::show_simple_items>(ui->checkBoxShowSimpleItems);
    bind<Config::move>(ui->checkBoxMove);
    bind<Config::check_clipboard>(ui->checkBoxClip);
    bind<Config::confirm_exit>(ui->checkBoxConfirmExit);
    bind<Config::vi>(ui->checkBoxViMode);
    bind<Config::save_filter_history>(ui->checkBoxSaveFilterHistory);
    bind<Config::always_on_top>(ui->checkBoxAlwaysOnTop);
    bind<Config::open_windows_on_current_screen>(ui->checkBoxOpenWindowsOnCurrentScreen);
    bind<Config::transparency_focused>(ui->spinBoxTransparencyFocused);
    bind<Config::transparency>(ui->spinBoxTransparencyUnfocused);
    bind<Config::hide_tabs>(ui->checkBoxHideTabs);
    bind<Config::hide_toolbar>(ui->checkBoxHideToolbar);
    bind<Config::hide_toolbar_labels>(ui->checkBoxHideToolbarLabels);
    bind<Config::disable_tray>(ui->checkBoxDisableTray);
    bind<Config::hide_main_window>(ui->checkBoxHideWindow);
    bind<Config::tab_tree>(ui->checkBoxTabTree);
    bind<Config::show_tab_item_count>(ui->checkBoxShowTabItemCount);
    bind<Config::text_wrap>(ui->checkBoxTextWrap);

    bind<Config::activate_closes>(ui->checkBoxActivateCloses);
    bind<Config::activate_focuses>(ui->checkBoxActivateFocuses);
    bind<Config::activate_pastes>(ui->checkBoxActivatePastes);

    bind<Config::tray_items>(ui->spinBoxTrayItems);
    bind<Config::tray_item_paste>(ui->checkBoxPasteMenuItem);
    bind<Config::tray_commands>(ui->checkBoxTrayShowCommands);
    bind<Config::tray_tab_is_current>(ui->checkBoxMenuTabIsCurrent);
    bind<Config::tray_images>(ui->checkBoxTrayImages);
    bind<Config::tray_tab>(ui->comboBoxMenuTab->lineEdit());

    /* other options */
    bind<Config::command_history_size>();
#ifdef HAS_MOUSE_SELECTIONS
    /* X11 clipboard selection monitoring and synchronization */
    bind<Config::check_selection>(ui->checkBoxSel);
    bind<Config::copy_clipboard>(ui->checkBoxCopyClip);
    bind<Config::copy_selection>(ui->checkBoxCopySel);
#else
    ui->checkBoxCopySel->hide();
    ui->checkBoxSel->hide();
    ui->checkBoxCopyClip->hide();
#endif

    // values of last submitted action
    bind<Config::action_has_input>();
    bind<Config::action_has_output>();
    bind<Config::action_separator>();
    bind<Config::action_output_tab>();
}

template <typename Config, typename Widget>
void ConfigurationManager::bind(Widget *obj)
{
    bind(Config::name(), obj, Config::defaultValue());
}

template <typename Config>
void ConfigurationManager::bind()
{
    bind(Config::name(), QVariant::fromValue(Config::defaultValue()));
}

void ConfigurationManager::bind(const QString &optionKey, QCheckBox *obj, bool defaultValue)
{
    m_options[optionKey] = Option(defaultValue, "checked", obj);
}

void ConfigurationManager::bind(const QString &optionKey, QSpinBox *obj, int defaultValue)
{
    m_options[optionKey] = Option(defaultValue, "value", obj);
}

void ConfigurationManager::bind(const QString &optionKey, QLineEdit *obj, const QString &defaultValue)
{
    m_options[optionKey] = Option(defaultValue, "text", obj);
}

void ConfigurationManager::bind(const QString &optionKey, QComboBox *obj, int defaultValue)
{
    m_options[optionKey] = Option(defaultValue, "currentIndex", obj);
}

void ConfigurationManager::bind(const QString &optionKey, const QVariant &defaultValue)
{
    m_options[optionKey] = Option(defaultValue);
}

void ConfigurationManager::updateTabComboBoxes()
{
    initTabComboBox(ui->comboBoxClipboardTab);
    initTabComboBox(ui->comboBoxMenuTab);
}

QStringList ConfigurationManager::options() const
{
    QStringList options;
    for ( const auto &option : m_options.keys() ) {
        if ( m_options[option].value().canConvert(QVariant::String)
             && !optionToolTip(option).isEmpty() )
        {
            options.append(option);
        }
    }

    return options;
}

QString ConfigurationManager::optionValue(const QString &name) const
{
    return m_options.value(name).value().toString();
}

bool ConfigurationManager::setOptionValue(const QString &name, const QString &value)
{
    if ( !m_options.contains(name) )
        return false;

    const QString oldValue = optionValue(name);
    m_options[name].setValue(value);
    if ( optionValue(name) == oldValue )
        return false;

    AppConfig().setOption(name, m_options[name].value());
    emit configurationChanged();
    return true;
}

QString ConfigurationManager::optionToolTip(const QString &name) const
{
    return m_options[name].tooltip();
}

void ConfigurationManager::loadSettings()
{
    QSettings settings;

    settings.beginGroup("Options");
    for ( const auto &key : m_options.keys() ) {
        if ( settings.contains(key) ) {
            QVariant value = settings.value(key);
            if ( !value.isValid() || !m_options[key].setValue(value) )
                log( tr("Invalid value for option \"%1\"").arg(key), LogWarning );
        } else {
            m_options[key].reset();
        }
    }
    settings.endGroup();

    settings.beginGroup("Shortcuts");
    ui->configTabShortcuts->loadShortcuts(settings);
    settings.endGroup();

    settings.beginGroup("Theme");
    ui->configTabAppearance->loadTheme(settings);
    settings.endGroup();

    ui->configTabAppearance->setEditor( AppConfig().option<Config::editor>() );

    on_checkBoxMenuTabIsCurrent_stateChanged( ui->checkBoxMenuTabIsCurrent->checkState() );

    updateTabComboBoxes();

    updateAutostart();
}

void ConfigurationManager::on_buttonBox_clicked(QAbstractButton* button)
{
    int answer;

    switch( ui->buttonBox->buttonRole(button) ) {
    case QDialogButtonBox::ApplyRole:
        apply();
        emit configurationChanged();
        break;
    case QDialogButtonBox::AcceptRole:
        accept();
        break;
    case QDialogButtonBox::RejectRole:
        reject();
        break;
    case QDialogButtonBox::ResetRole:
        // ask before resetting values
        answer = QMessageBox::question(
                    this,
                    tr("Reset preferences?"),
                    tr("This action will reset all your preferences (in all tabs) to default values.<br /><br />"
                       "Do you really want to <strong>reset all preferences</strong>?"),
                    QMessageBox::Yes | QMessageBox::No,
                    QMessageBox::Yes);
        if (answer == QMessageBox::Yes) {
            for ( const auto &key : m_options.keys() ) {
                m_options[key].reset();
            }
        }
        break;
    default:
        return;
    }
}

void ConfigurationManager::setVisible(bool visible)
{
    QDialog::setVisible(visible);

    if (visible) {
        initTabIcons();
        initLanguages();
    }
}

void ConfigurationManager::apply()
{
    Settings settings;

    settings.beginGroup("Options");
    for ( const auto &key : m_options.keys() ) {
        settings.setValue( key, m_options[key].value() );
    }
    settings.endGroup();

    // Save configuration without command line alternatives only if option widgets are initialized
    // (i.e. clicked OK or Apply in configuration dialog).
    settings.beginGroup("Shortcuts");
    ui->configTabShortcuts->saveShortcuts(settings.settingsData());
    settings.endGroup();

    settings.beginGroup("Theme");
    ui->configTabAppearance->saveTheme(settings.settingsData());
    settings.endGroup();

    // Save settings for each plugin.
    settings.beginGroup("Plugins");

    QStringList pluginPriority;

    for (int i = 0; i < ui->itemOrderListPlugins->itemCount(); ++i) {
        const QString loaderId = ui->itemOrderListPlugins->data(i).toString();
        Q_ASSERT(!loaderId.isEmpty());

        pluginPriority.append(loaderId);

        settings.beginGroup(loaderId);

        QWidget *w = ui->itemOrderListPlugins->widget(i);
        if (w) {
            PluginWidget *pluginWidget = qobject_cast<PluginWidget *>(w);
            const auto &loader = pluginWidget->loader();
            const QVariantMap s = loader->applySettings();
            for (const auto &name : s.keys())
                settings.setValue(name, s[name]);
        }

        const bool isPluginEnabled = ui->itemOrderListPlugins->isItemChecked(i);
        settings.setValue("enabled", isPluginEnabled);

        settings.endGroup();
    }

    settings.endGroup();

    if (!pluginPriority.isEmpty())
        settings.setValue("plugin_priority", pluginPriority);

    ui->configTabAppearance->setEditor( AppConfig().option<Config::editor>() );

    setAutostartEnable();

    // Language changes after restart.
    const int newLocaleIndex = ui->comboBoxLanguage->currentIndex();
    const QString newLocaleName = ui->comboBoxLanguage->itemData(newLocaleIndex).toString();
    QString oldLocaleName = settings.value("Options/language").toString();
    if (oldLocaleName.isEmpty())
        oldLocaleName = "en";
    const QLocale oldLocale;

    settings.setValue("Options/language", newLocaleName);

    if (QLocale(newLocaleName).name() != oldLocale.name() && newLocaleName != oldLocaleName) {
        QMessageBox::information( this, tr("Restart Required"),
                                  tr("Language will be changed after application is restarted.") );
    }
}

void ConfigurationManager::done(int result)
{
    if (result == QDialog::Accepted) {
        apply();
        emit configurationChanged();
    }

    QDialog::done(result);
}

void ConfigurationManager::on_checkBoxMenuTabIsCurrent_stateChanged(int state)
{
    ui->comboBoxMenuTab->setEnabled(state == Qt::Unchecked);
}

void ConfigurationManager::on_spinBoxTrayItems_valueChanged(int value)
{
    ui->checkBoxPasteMenuItem->setEnabled(value > 0);
}