1 /*
2  * LXImage-Qt - a simple and fast image viewer
3  * Copyright (C) 2013  PCMan <pcman.tw@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  */
20 
21 #include "preferencesdialog.h"
22 #include "application.h"
23 #include <QDir>
24 #include <QStringBuilder>
25 #include <QKeyEvent>
26 #include <QWindow>
27 #include <QScreen>
28 #include <glib.h>
29 
30 using namespace LxImage;
31 
32 static QHash<QString, QString> ACTION_DISPLAY_NAMES;
33 
keyPressEvent(QKeyEvent * event)34 void KeySequenceEdit::keyPressEvent(QKeyEvent* event) {
35     // by not allowing multiple shortcuts,
36     // the Qt bug that makes Meta a non-modifier is worked around
37     clear();
38     QKeySequenceEdit::keyPressEvent (event);
39 }
40 
createEditor(QWidget * parent,const QStyleOptionViewItem &,const QModelIndex &) const41 QWidget* Delegate::createEditor(QWidget* parent,
42                                 const QStyleOptionViewItem& /*option*/,
43                                 const QModelIndex& /*index*/) const {
44   return new KeySequenceEdit(parent);
45 }
46 
eventFilter(QObject * object,QEvent * event)47 bool Delegate::eventFilter(QObject* object, QEvent* event) {
48   QWidget* editor = qobject_cast<QWidget*>(object);
49   if(editor && event->type() == QEvent::KeyPress) {
50     int k = static_cast<QKeyEvent *>(event)->key();
51     if (k == Qt::Key_Return || k == Qt::Key_Enter) {
52       Q_EMIT QAbstractItemDelegate::commitData(editor);
53       Q_EMIT QAbstractItemDelegate::closeEditor(editor);
54       return true;
55     }
56   }
57   return QStyledItemDelegate::eventFilter(object, event);
58 }
59 /*************************/
PreferencesDialog(QWidget * parent)60 PreferencesDialog::PreferencesDialog(QWidget* parent):
61   QDialog(parent) {
62   ui.setupUi(this);
63   setAttribute(Qt::WA_DeleteOnClose);
64 
65   ui.warningLabel->setStyleSheet(QStringLiteral("QLabel {background-color: #7d0000; color: white; border-radius: 3px; margin: 2px; padding: 5px;}"));
66   ui.warningLabel->hide();
67   warningTimer_ = nullptr;
68 
69   Delegate *del = new Delegate(ui.tableWidget);
70   ui.tableWidget->setItemDelegate(del);
71   ui.tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
72   ui.tableWidget->horizontalHeader()->setSectionsClickable(true);
73   ui.tableWidget->sortByColumn(0, Qt::AscendingOrder);
74   ui.tableWidget->setToolTip(tr("Use a modifier key to clear a shortcut\nin the editing mode."));
75 
76   Application* app = static_cast<Application*>(qApp);
77   Settings& settings = app->settings();
78   app->addWindow();
79 
80   initIconThemes(settings);
81   ui.bgColor->setColor(settings.bgColor());
82   ui.fullScreenBgColor->setColor(settings.fullScreenBgColor());
83   ui.maxRecentFiles->setValue(settings.maxRecentFiles());
84   ui.slideShowInterval->setValue(settings.slideShowInterval());
85   ui.oulineBox->setChecked(settings.isOutlineShown());
86   ui.menubarBox->setChecked(settings.isMenubarShown());
87   ui.toolbarBox->setChecked(settings.isToolbarShown());
88   ui.annotationBox->setChecked(settings.isAnnotationsToolbarShown());
89   ui.forceZoomFitBox->setChecked(settings.forceZoomFit());
90   ui.smoothOnZoomBox->setChecked(settings.smoothOnZoom());
91   ui.useTrashBox->setChecked(settings.useTrash());
92 
93   ui.exifDataBox->setChecked(settings.showExifData());
94   ui.thumbnailBox->setChecked(settings.showThumbnails());
95   // the max. thumbnail size spinbox is in MiB
96   ui.thumbnailSpin->setValue(qBound(0, settings.maxThumbnailFileSize() / 1024, 1024));
97   initThumbnailSizes(settings);
98   initThumbnailsPositions(settings);
99 
100   // shortcuts
101   initShortcuts();
102 }
103 
~PreferencesDialog()104 PreferencesDialog::~PreferencesDialog() {
105   if(warningTimer_) {
106     warningTimer_->stop();
107     delete warningTimer_;
108     warningTimer_ = nullptr;
109   }
110   Application* app = static_cast<Application*>(qApp);
111   app->removeWindow();
112 }
113 
showEvent(QShowEvent * event)114 void PreferencesDialog::showEvent(QShowEvent* event) {
115   QSize prefSize = static_cast<Application*>(qApp)->settings().getPrefSize();
116   if(QWindow *window = windowHandle()) {
117     if(QScreen *sc = window->screen()) {
118       prefSize = prefSize.boundedTo(sc->availableGeometry().size() - QSize(50, 100));
119     }
120   }
121   resize(prefSize);
122 
123   QDialog::showEvent(event);
124 }
125 
accept()126 void PreferencesDialog::accept() {
127   Application* app = static_cast<Application*>(qApp);
128   Settings& settings = app->settings();
129 
130   // apply icon theme
131   if(settings.useFallbackIconTheme()) {
132     // only apply the value if icon theme combo box is in use
133     // the combo box is hidden when auto-detection of icon theme by qt works.
134     QString newIconTheme = ui.iconTheme->itemData(ui.iconTheme->currentIndex()).toString();
135     if(newIconTheme != settings.fallbackIconTheme()) {
136       settings.setFallbackIconTheme(newIconTheme);
137       QIcon::setThemeName(newIconTheme);
138       // update the UI by emitting a style change event
139       const auto allWidgets = QApplication::allWidgets();
140       for(QWidget *widget : allWidgets) {
141         QEvent event(QEvent::StyleChange);
142         QApplication::sendEvent(widget, &event);
143       }
144     }
145   }
146 
147   settings.setBgColor(ui.bgColor->color());
148   settings.setFullScreenBgColor(ui.fullScreenBgColor->color());
149   settings.setMaxRecentFiles(ui.maxRecentFiles->value());
150   settings.setSlideShowInterval(ui.slideShowInterval->value());
151   settings.showOutline(ui.oulineBox->isChecked());
152   settings.showMenubar(ui.menubarBox->isChecked());
153   settings.showToolbar(ui.toolbarBox->isChecked());
154   settings.showAnnotationsToolbar(ui.annotationBox->isChecked());
155   settings.setForceZoomFit(ui.forceZoomFitBox->isChecked());
156   settings.setSmoothOnZoom(ui.smoothOnZoomBox->isChecked());
157   settings.setUseTrash(ui.useTrashBox->isChecked());
158 
159   settings.setShowExifData(ui.exifDataBox->isChecked());
160   settings.setShowThumbnails(ui.thumbnailBox->isChecked());
161   settings.setThumbnailsPosition(ui.thumbnailsPositionComboBox->itemData(ui.thumbnailsPositionComboBox->currentIndex()).toInt());
162   // the max. thumbnail size spinbox is in MiB
163   settings.setMaxThumbnailFileSize(ui.thumbnailSpin->value() * 1024);
164   settings.setThumbnailSize(ui.thumbnailSizeComboBox->itemData(ui.thumbnailSizeComboBox->currentIndex()).toInt());
165 
166   updateThumbnails();
167   applyNewShortcuts();
168   settings.save();
169   QDialog::accept();
170   app->applySettings();
171 }
172 
findIconThemesInDir(QHash<QString,QString> & iconThemes,const QString & dirName)173 static void findIconThemesInDir(QHash<QString, QString>& iconThemes, const QString& dirName) {
174   QDir dir(dirName);
175   const QStringList subDirs = dir.entryList(QDir::AllDirs);
176   GKeyFile* kf = g_key_file_new();
177   for(const QString& subDir : subDirs) {
178     QString indexFile = dirName + QLatin1Char('/') + subDir + QStringLiteral("/index.theme");
179     if(g_key_file_load_from_file(kf, indexFile.toLocal8Bit().constData(), GKeyFileFlags(0), nullptr)) {
180       // FIXME: skip hidden ones
181       // icon theme must have this key, so it has icons if it has this key
182       // otherwise, it might be a cursor theme or any other kind of theme.
183       if(g_key_file_has_key(kf, "Icon Theme", "Directories", nullptr)) {
184         char* dispName = g_key_file_get_locale_string(kf, "Icon Theme", "Name", nullptr, nullptr);
185         // char* comment = g_key_file_get_locale_string(kf, "Icon Theme", "Comment", NULL, NULL);
186         iconThemes[subDir] = QString::fromUtf8(dispName);
187         g_free(dispName);
188       }
189     }
190   }
191   g_key_file_free(kf);
192 }
193 
initThumbnailSizes(Settings & settings)194 void PreferencesDialog::initThumbnailSizes(Settings& settings) {
195   int i = 0;
196   for(const auto & size : settings.thumbnailSizes()) {
197     ui.thumbnailSizeComboBox->addItem(QStringLiteral("%1 × %1").arg(size), size);
198     if(settings.thumbnailSize() == size) {
199       ui.thumbnailSizeComboBox->setCurrentIndex(i);
200     }
201     ++i;
202   }
203 }
204 
initThumbnailsPositions(Settings & settings)205 void PreferencesDialog::initThumbnailsPositions(Settings& settings) {
206   ui.thumbnailsPositionComboBox->addItem(tr("Bottom"), Qt::BottomDockWidgetArea);
207   ui.thumbnailsPositionComboBox->addItem(tr("Top"), Qt::TopDockWidgetArea);
208   ui.thumbnailsPositionComboBox->addItem(tr("Left"), Qt::LeftDockWidgetArea);
209 
210   Qt::DockWidgetArea pos = settings.thumbnailsPosition();
211   for(int i = 0; i < ui.thumbnailsPositionComboBox->count(); ++i) {
212     if(ui.thumbnailsPositionComboBox->itemData(i).toInt() == pos) {
213       ui.thumbnailsPositionComboBox->setCurrentIndex(i);
214       return;
215     }
216   }
217   ui.thumbnailsPositionComboBox->setCurrentIndex(0);
218 }
219 
initIconThemes(Settings & settings)220 void PreferencesDialog::initIconThemes(Settings& settings) {
221   // check if auto-detection is done (for example, from xsettings)
222   if(settings.useFallbackIconTheme()) { // auto-detection failed
223     // load xdg icon themes and select the current one
224     QHash<QString, QString> iconThemes;
225     // user customed icon themes
226     findIconThemesInDir(iconThemes, QString::fromUtf8(g_get_home_dir()) + QStringLiteral("/.icons"));
227 
228     // search for icons in system data dir
229     const char* const* dataDirs = g_get_system_data_dirs();
230     for(const char* const* dataDir = dataDirs; *dataDir; ++dataDir) {
231       findIconThemesInDir(iconThemes, QString::fromUtf8((*dataDir)) + QStringLiteral("/icons"));
232     }
233 
234     iconThemes.remove(QStringLiteral("hicolor")); // remove hicolor, which is only a fallback
235     QHash<QString, QString>::const_iterator it;
236     for(it = iconThemes.constBegin(); it != iconThemes.constEnd(); ++it) {
237       ui.iconTheme->addItem(it.value(), it.key());
238     }
239     ui.iconTheme->model()->sort(0); // sort the list of icon theme names
240 
241     // select current theme name
242     int n = ui.iconTheme->count();
243     int i;
244     for(i = 0; i < n; ++i) {
245       QVariant itemData = ui.iconTheme->itemData(i);
246       if(itemData == settings.fallbackIconTheme()) {
247         break;
248       }
249     }
250     if(i >= n)
251       i = 0;
252     ui.iconTheme->setCurrentIndex(i);
253   }
254   else { // auto-detection of icon theme works, hide the fallback icon theme combo box.
255     ui.iconThemeLabel->hide();
256     ui.iconTheme->hide();
257   }
258 }
259 
showWarning(const QString & text,bool temporary)260 void PreferencesDialog::showWarning(const QString& text, bool temporary) {
261   if(text.isEmpty()) {
262     permanentWarning_.clear();
263     ui.warningLabel->clear();
264     ui.warningLabel->hide();
265   }
266   else {
267     ui.warningLabel->setText(text);
268     ui.warningLabel->show();
269     if(!temporary) {
270       permanentWarning_ = text;
271     }
272     else {
273       if(warningTimer_ == nullptr) {
274         warningTimer_ = new QTimer();
275         warningTimer_->setSingleShot (true);
276         connect(warningTimer_, &QTimer::timeout, this, [this] {
277           ui.warningLabel->setText(permanentWarning_);
278           ui.warningLabel->setVisible(!permanentWarning_.isEmpty());
279         });
280       }
281       warningTimer_->start(5000);
282     }
283   }
284 }
285 
initShortcuts()286 void PreferencesDialog::initShortcuts() {
287   Application* app = static_cast<Application*>(qApp);
288   Settings& settings = app->settings();
289 
290   // pair display and object names together (to get the latter from the former)
291   if(ACTION_DISPLAY_NAMES.isEmpty()) {
292     QHash<QString, Application::ShortcutDescription>::const_iterator iter = app->defaultShortcuts().constBegin();
293     while (iter != app->defaultShortcuts().constEnd()) {
294       ACTION_DISPLAY_NAMES.insert(iter.value().displayText, iter.key());
295       ++ iter;
296     }
297   }
298 
299   // fill the table widget
300   ui.tableWidget->setRowCount(ACTION_DISPLAY_NAMES.size());
301   ui.tableWidget->setSortingEnabled(false);
302   int index = 0;
303   QHash<QString, QString> ca = settings.customShortcutActions();
304   QHash<QString, QString>::const_iterator iter = ACTION_DISPLAY_NAMES.constBegin();
305   while(iter != ACTION_DISPLAY_NAMES.constEnd()) {
306     // add the action item
307     QTableWidgetItem *item = new QTableWidgetItem(iter.key());
308     item->setFlags(item->flags() & ~Qt::ItemIsEditable & ~Qt::ItemIsSelectable);
309     ui.tableWidget->setItem(index, 0, item);
310 
311     // NOTE: Shortcuts are saved in the PortableText format but
312     // their texts should be added to the table in the NativeText format.
313     QString shortcut;
314     if(ca.contains(iter.value())) { // a cusrom shortcut
315       shortcut = ca.value(iter.value());
316       QKeySequence keySeq(shortcut, QKeySequence::PortableText);
317       shortcut = keySeq.toString(QKeySequence::NativeText);
318     }
319     else { // a default shortcut
320       shortcut = app->defaultShortcuts().value(iter.value()).shortcut.toString(QKeySequence::NativeText);
321     }
322     ui.tableWidget->setItem(index, 1, new QTableWidgetItem(shortcut));
323     allShortcuts_.insert(iter.key(), shortcut);
324 
325     ++ iter;
326     ++ index;
327   }
328   ui.tableWidget->setSortingEnabled(true);
329   ui.tableWidget->setCurrentCell(0, 1);
330 
331   const auto shortcuts = allShortcuts_.values();
332   for(int i = 0; i < shortcuts.size(); ++i) {
333     if(!shortcuts.at(i).isEmpty() && shortcuts.indexOf(shortcuts.at(i), i + 1) > -1) {
334       showWarning(tr("<b>Warning: Ambiguous shortcut detected!</b>"), false);
335       break;
336     }
337   }
338 
339   connect(ui.tableWidget, &QTableWidget::itemChanged, this, &PreferencesDialog::onShortcutChange);
340   connect(ui.defaultButton, &QAbstractButton::clicked, this, &PreferencesDialog::restoreDefaultShortcuts);
341   ui.defaultButton->setDisabled(ca.isEmpty());
342 }
343 
onShortcutChange(QTableWidgetItem * item)344 void PreferencesDialog::onShortcutChange(QTableWidgetItem* item) {
345   QString desc = ui.tableWidget->item(ui.tableWidget->currentRow(), 0)->text();
346   QString txt = item->text();
347 
348   const auto shortcuts = allShortcuts_.values();
349   for(const auto& s : shortcuts) {
350     if(!s.isEmpty() && s == txt) {
351       showWarning(tr("<b>Ambiguous shortcut not accepted.</b>"));
352       disconnect(ui.tableWidget, &QTableWidget::itemChanged, this, &PreferencesDialog::onShortcutChange);
353       item->setText(allShortcuts_.value (desc)); // restore the previous shortcut
354       connect(ui.tableWidget, &QTableWidget::itemChanged, this, &PreferencesDialog::onShortcutChange);
355       return;
356     }
357   }
358   showWarning(QString()); // ambiguous shortcuts might have been added manually
359 
360   allShortcuts_.insert(desc, txt);
361 
362   if(!txt.isEmpty()) {
363     // NOTE: The QKeySequenceEdit text is in the NativeText format but
364     // it should be converted into the PortableText format for saving.
365     QKeySequence keySeq(txt, QKeySequence::NativeText);
366     txt = keySeq.toString(QKeySequence::PortableText);
367   }
368   modifiedShortcuts_.insert(ACTION_DISPLAY_NAMES.value(desc), txt);
369 
370   // also set the state of the Default button
371   Application* app = static_cast<Application*>(qApp);
372   for(int i = 0; i < ui.tableWidget->rowCount(); ++i) {
373     const QString objectName = ACTION_DISPLAY_NAMES.value(ui.tableWidget->item(i, 0)->text());
374     if(app->defaultShortcuts().value(objectName).shortcut.toString(QKeySequence::PortableText)
375        != ui.tableWidget->item(i, 1)->text()) {
376       ui.defaultButton->setEnabled(true);
377       return;
378     }
379   }
380   ui.defaultButton->setEnabled(false);
381 }
382 
restoreDefaultShortcuts()383 void PreferencesDialog::restoreDefaultShortcuts() {
384   Application* app = static_cast<Application*>(qApp);
385   if (modifiedShortcuts_.isEmpty()
386       && app->settings().customShortcutActions().isEmpty()) {
387     // do nothing if there's no custom shortcut
388     return;
389   }
390 
391   showWarning(QString());
392 
393   disconnect(ui.tableWidget, &QTableWidget::itemChanged, this, &PreferencesDialog::onShortcutChange);
394   int cur = ui.tableWidget->currentColumn() == 0 ? 0 : ui.tableWidget->currentRow();
395   ui.tableWidget->setSortingEnabled(false);
396 
397   // consider all shortcuts modified
398   QHash<QString, Application::ShortcutDescription>::const_iterator iter = app->defaultShortcuts().constBegin();
399   while (iter != app->defaultShortcuts().constEnd()) {
400     modifiedShortcuts_.insert(iter.key(), iter.value().shortcut.toString(QKeySequence::PortableText));
401     ++ iter;
402   }
403 
404   // restore default shortcuts in the GUI
405   for(int i = 0; i < ui.tableWidget->rowCount(); ++i) {
406     const QString objectName = ACTION_DISPLAY_NAMES.value(ui.tableWidget->item(i, 0)->text());
407     QString s = app->defaultShortcuts().value(objectName).shortcut.toString(QKeySequence::PortableText);
408     ui.tableWidget->item(i, 1)->setText(s);
409   }
410 
411   ui.tableWidget->setSortingEnabled(true);
412   ui.tableWidget->setCurrentCell(cur, 1);
413   connect(ui.tableWidget, &QTableWidget::itemChanged, this, &PreferencesDialog::onShortcutChange);
414   ui.defaultButton->setEnabled(false);
415 }
416 
updateThumbnails()417 void PreferencesDialog::updateThumbnails() {
418   const auto windows = qApp->topLevelWidgets();
419   for(const auto& window : windows) {
420     if(window->inherits("LxImage::MainWindow")) {
421       static_cast<MainWindow*>(window)->updateThumbnails();
422     }
423   }
424 }
425 
applyNewShortcuts()426 void PreferencesDialog::applyNewShortcuts() {
427   // remember the modified shortcuts if they are different from the default ones
428   Application* app = static_cast<Application*>(qApp);
429   Settings& settings = app->settings();
430   QHash<QString, QString>::const_iterator it = modifiedShortcuts_.constBegin();
431   while(it != modifiedShortcuts_.constEnd()) {
432     if(app->defaultShortcuts().value(it.key()).shortcut.toString(QKeySequence::PortableText) == it.value()) {
433       settings.removeShortcut(it.key());
434     }
435     else {
436       settings.addShortcut(it.key(), it.value());
437     }
438     ++it;
439   }
440 }
441 
done(int r)442 void PreferencesDialog::done(int r) {
443   // remember size
444   Settings& settings = static_cast<Application*>(qApp)->settings();
445   settings.setPrefSize(size());
446 
447   QDialog::done(r);
448   deleteLater();
449 }
450 
451