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