1 /*
2  * <one line to give the library's name and an idea of what it does.>
3  * Copyright (C) 2014  <copyright holder> <email>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library 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 GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  */
20 
21 #include "keyboardlayoutconfig.h"
22 #include <QProcess>
23 #include <QFile>
24 #include <QHash>
25 #include <QDebug>
26 #include "selectkeyboardlayoutdialog.h"
27 #include <LXQt/Settings>
28 
KeyboardLayoutConfig(LXQt::Settings * _settings,QWidget * parent)29 KeyboardLayoutConfig::KeyboardLayoutConfig(LXQt::Settings* _settings, QWidget* parent):
30   QWidget(parent),
31   settings(_settings),
32   applyConfig_(false) {
33   ui.setupUi(this);
34 
35   loadLists();
36   loadSettings();
37   initControls();
38 
39   connect(ui.addLayout, &QAbstractButton::clicked, this, &KeyboardLayoutConfig::onAddLayout);
40   connect(ui.removeLayout, &QAbstractButton::clicked, this, &KeyboardLayoutConfig::onRemoveLayout);
41   connect(ui.moveUp, &QAbstractButton::clicked, this, &KeyboardLayoutConfig::onMoveUp);
42   connect(ui.moveDown, &QAbstractButton::clicked, this, &KeyboardLayoutConfig::onMoveDown);
43   connect(ui.keyboardModel, QOverload<int>::of(&QComboBox::activated), [this](int /*index*/) {
44     applyConfig_ = true;
45     Q_EMIT settingsChanged();
46   });
47   connect(ui.switchKey, QOverload<int>::of(&QComboBox::activated), [this](int /*index*/) {
48     applyConfig_ = true;
49     Q_EMIT settingsChanged();
50   });
51 }
52 
~KeyboardLayoutConfig()53 KeyboardLayoutConfig::~KeyboardLayoutConfig() {
54 }
55 
loadSettings()56 void KeyboardLayoutConfig::loadSettings() {
57   // load current settings from the output of setxkbmap command
58   QProcess setxkbmap;
59   setxkbmap.start(QLatin1String("setxkbmap"), QStringList() << QLatin1String("-query")
60     << QLatin1String("-verbose") << QLatin1String("5"));
61   setxkbmap.waitForFinished();
62   if(setxkbmap.exitStatus() == QProcess::NormalExit) {
63     QList<QByteArray> layouts, variants;
64     while(!setxkbmap.atEnd()) {
65       QByteArray line = setxkbmap.readLine();
66       if(line.startsWith("model:")) {
67         keyboardModel_ = QString::fromLatin1(line.mid(6).trimmed());
68       }
69       else if(line.startsWith("layout:")) {
70         layouts = line.mid(7).trimmed().split(',');
71       }
72       else if(line.startsWith("variant:")) {
73         variants = line.mid(8).trimmed().split(',');
74       }
75       else if(line.startsWith("options:")) {
76         const QList<QByteArray> options = line.mid(9).trimmed().split(',');
77         for(const QByteArray &option : options) {
78           if(option.startsWith("grp:"))
79             switchKey_ = QString::fromLatin1(option);
80           else
81             currentOptions_ << QString::fromLatin1(option);
82         }
83       }
84     }
85 
86     const int size = layouts.size(), variantsSize = variants.size();
87     for(int i = 0; i < size; ++i) {
88       currentLayouts_.append(QPair<QString, QString>(QString::fromUtf8(layouts.at(i)), variantsSize > 0 ? QString::fromUtf8(variants.at(i)) : QString()));
89     }
90 
91     setxkbmap.close();
92   }
93 }
94 
95 enum ListSection{
96   NoSection,
97   ModelSection,
98   LayoutSection,
99   VariantSection,
100   OptionSection
101 };
102 
loadLists()103 void KeyboardLayoutConfig::loadLists() {
104   // load known lists from xkb data files
105   // XKBD_BASELIST_PATH is os dependent see keyboardlayoutconfig.h
106   QFile file(QLatin1String(XKBD_BASELIST_PATH));
107   if(file.open(QIODevice::ReadOnly)) {
108     ListSection section = NoSection;
109     while(!file.atEnd()) {
110       QByteArray line = file.readLine().trimmed();
111       if(section == NoSection) {
112         if(line.startsWith("! model"))
113           section = ModelSection;
114         else if(line.startsWith("! layout"))
115           section = LayoutSection;
116         else if(line.startsWith("! variant"))
117           section = VariantSection;
118         else if(line.startsWith("! option"))
119           section = OptionSection;
120       }
121       else {
122         if(line.isEmpty()) {
123           section = NoSection;
124           continue;
125         }
126         int sep = line.indexOf(' ');
127         QString name = QString::fromLatin1(line.constData(), sep);
128         while(line[sep] ==' ') // skip spaces
129           ++sep;
130         QString description = QString::fromUtf8(line.constData() + sep);
131 
132         switch(section) {
133           case ModelSection: {
134             ui.keyboardModel->addItem(description, name);
135             break;
136           }
137           case LayoutSection:
138             knownLayouts_[name] = KeyboardLayoutInfo(description);
139             break;
140           case VariantSection: {
141             // the descriptions of variants are prefixed by their language ids
142             sep = description.indexOf(QLatin1String(": "));
143             if(sep >= 0) {
144               QString lang = description.left(sep);
145               QMap<QString, KeyboardLayoutInfo>::iterator it = knownLayouts_.find(lang);
146               if(it != knownLayouts_.end()) {
147                 KeyboardLayoutInfo& info = *it;
148                 info.variants.append(LayoutVariantInfo(name, description.mid(sep + 2)));
149               }
150             }
151             break;
152           }
153           case OptionSection:
154             if(line.startsWith("grp:")) { // key used to switch to another layout
155               ui.switchKey->addItem(description, name);
156             }
157             break;
158           default:;
159         }
160       }
161     }
162     file.close();
163   }
164 }
165 
initControls()166 void KeyboardLayoutConfig::initControls() {
167   QList<QPair<QString, QString> >::iterator it;
168   for(it = currentLayouts_.begin(); it != currentLayouts_.end(); ++it) {
169     QString name = it->first;
170     QString variant = it->second;
171     addLayout(name, variant);
172   }
173 
174   int n = ui.keyboardModel->count();
175   int row;
176   for(row = 0; row < n; ++row) {
177     if(ui.keyboardModel->itemData(row, Qt::UserRole).toString() == keyboardModel_) {
178       ui.keyboardModel->setCurrentIndex(row);
179       break;
180     }
181   }
182 
183   n = ui.switchKey->count();
184   for(row = 0; row < n; ++row) {
185     if(ui.switchKey->itemData(row, Qt::UserRole).toString() == switchKey_) {
186       ui.switchKey->setCurrentIndex(row);
187       break;
188     }
189   }
190 
191 }
192 
addLayout(QString name,QString variant)193 void KeyboardLayoutConfig::addLayout(QString name, QString variant) {
194   qDebug() << "add" << name << variant;
195   const KeyboardLayoutInfo& info = knownLayouts_.value(name);
196   QTreeWidgetItem* item = new QTreeWidgetItem();
197   item->setData(0, Qt::DisplayRole, info.description);
198   item->setData(0, Qt::UserRole, name);
199   const LayoutVariantInfo* vinfo = info.findVariant(variant);
200   if(vinfo) {
201     item->setData(1, Qt::DisplayRole, vinfo->description);
202     item->setData(1, Qt::UserRole, variant);
203   }
204   ui.layouts->addTopLevelItem(item);
205 }
206 
reset()207 void KeyboardLayoutConfig::reset() {
208   applyConfig_ = true;
209   ui.layouts->clear();
210   initControls();
211   applyConfig();
212 }
213 
applyConfig()214 void KeyboardLayoutConfig::applyConfig() {
215   if(!applyConfig_)
216     return;
217   applyConfig_ = false;
218 
219   // call setxkbmap to apply the changes
220   QProcess setxkbmap;
221   // clear existing options
222   setxkbmap.start(QStringLiteral("setxkbmap"), QStringList() << QStringLiteral("-option"));
223   setxkbmap.waitForFinished();
224   setxkbmap.close();
225 
226   const QString program = QStringLiteral("setxkbmap");
227   QStringList args;
228   // set keyboard model
229   QString model;
230   int cur_model = ui.keyboardModel->currentIndex();
231   if(cur_model >= 0) {
232     model = ui.keyboardModel->itemData(cur_model, Qt::UserRole).toString();
233     args += QLatin1String("-model");
234     args += model;
235   }
236 
237   // set keyboard layout
238   int n = ui.layouts->topLevelItemCount();
239   QString layouts, variants;
240   if(n > 0) {
241     for(int row = 0; row < n; ++row) {
242       QTreeWidgetItem* item = ui.layouts->topLevelItem(row);
243       layouts += item->data(0, Qt::UserRole).toString();
244       variants += item->data(1, Qt::UserRole).toString();
245       if(row < n - 1) { // not the last row
246         layouts += QLatin1Char(',');
247         variants += QLatin1Char(',');
248       }
249     }
250     args += QLatin1String("-layout");
251     args += layouts;
252 
253     if (variants.indexOf(QLatin1Char(',')) > -1 || !variants.isEmpty()) {
254       args += QLatin1String("-variant");
255       args += variants;
256     }
257   }
258 
259   for(const QString& option : qAsConst(currentOptions_)) {
260     if (!option.startsWith(QLatin1String("grp:"))) {
261       args += QLatin1String("-option");
262       args += option;
263     }
264   }
265 
266   QString switchKey;
267   int cur_switch_key = ui.switchKey->currentIndex();
268   if(cur_switch_key > 0) { // index 0 is "None"
269     switchKey = ui.switchKey->itemData(cur_switch_key, Qt::UserRole).toString();
270     args += QLatin1String("-option");
271     args += switchKey;
272   }
273 
274   qDebug() << program << args;
275 
276   // execute the command line
277   setxkbmap.start(program, args);
278   setxkbmap.waitForFinished();
279 
280   // save to lxqt-session config file.
281   settings->beginGroup(QStringLiteral("Keyboard"));
282   settings->setValue(QStringLiteral("layout"), layouts);
283   settings->setValue(QStringLiteral("variant"), variants);
284   settings->setValue(QStringLiteral("model"), model);
285   if(switchKey.isEmpty() && currentOptions_ .isEmpty())
286     settings->remove(QStringLiteral("options"));
287   else
288     settings->setValue(QStringLiteral("options"), switchKey.isEmpty() ? currentOptions_ : (currentOptions_ << switchKey));
289   settings->endGroup();
290 }
291 
onAddLayout()292 void KeyboardLayoutConfig::onAddLayout() {
293   SelectKeyboardLayoutDialog dlg(knownLayouts_, this);
294   if(dlg.exec() == QDialog::Accepted) {
295     addLayout(dlg.selectedLayout(), dlg.selectedVariant());
296     applyConfig_ = true;
297     Q_EMIT settingsChanged();
298   }
299 }
300 
onRemoveLayout()301 void KeyboardLayoutConfig::onRemoveLayout() {
302   if(ui.layouts->topLevelItemCount() > 1) {
303     QTreeWidgetItem* item = ui.layouts->currentItem();
304     if(item) {
305       delete item;
306       applyConfig_ = true;
307       Q_EMIT settingsChanged();
308     }
309   }
310 }
311 
onMoveDown()312 void KeyboardLayoutConfig::onMoveDown() {
313   QTreeWidgetItem* item = ui.layouts->currentItem();
314   if(!item)
315     return;
316   int pos = ui.layouts->indexOfTopLevelItem(item);
317   if(pos < ui.layouts->topLevelItemCount() - 1) { // not the last item
318     ui.layouts->takeTopLevelItem(pos);
319     ui.layouts->insertTopLevelItem(pos + 1, item);
320     ui.layouts->setCurrentItem(item);
321     applyConfig_ = true;
322     Q_EMIT settingsChanged();
323   }
324 }
325 
onMoveUp()326 void KeyboardLayoutConfig::onMoveUp() {
327   QTreeWidgetItem* item = ui.layouts->currentItem();
328   if(!item)
329     return;
330   int pos = ui.layouts->indexOfTopLevelItem(item);
331   if(pos > 0) { // not the first item
332     ui.layouts->takeTopLevelItem(pos);
333     ui.layouts->insertTopLevelItem(pos - 1, item);
334     ui.layouts->setCurrentItem(item);
335     applyConfig_ = true;
336     Q_EMIT settingsChanged();
337   }
338 }
339 
340