Changeset View
Standalone View
keyboard_layout.cpp
Show All 16 Lines | |||||
17 | You should have received a copy of the GNU General Public License | 17 | You should have received a copy of the GNU General Public License | ||
18 | along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 | along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
19 | *********************************************************************/ | 19 | *********************************************************************/ | ||
20 | #include "keyboard_layout.h" | 20 | #include "keyboard_layout.h" | ||
21 | #include "keyboard_input.h" | 21 | #include "keyboard_input.h" | ||
22 | #include "input_event.h" | 22 | #include "input_event.h" | ||
23 | #include "main.h" | 23 | #include "main.h" | ||
24 | #include "platform.h" | 24 | #include "platform.h" | ||
25 | #include "utils.h" | ||||
25 | 26 | | |||
27 | #include <KConfigGroup> | ||||
26 | #include <KGlobalAccel> | 28 | #include <KGlobalAccel> | ||
29 | #include <KLocalizedString> | ||||
30 | #include <KNotifications/KStatusNotifierItem> | ||||
27 | #include <QAction> | 31 | #include <QAction> | ||
28 | #include <QDBusConnection> | 32 | #include <QDBusConnection> | ||
29 | #include <QDBusMessage> | 33 | #include <QDBusMessage> | ||
30 | #include <QDBusPendingCall> | 34 | #include <QDBusPendingCall> | ||
35 | #include <QMenu> | ||||
31 | 36 | | |||
32 | namespace KWin | 37 | namespace KWin | ||
33 | { | 38 | { | ||
34 | 39 | | |||
35 | KeyboardLayout::KeyboardLayout(Xkb *xkb) | 40 | KeyboardLayout::KeyboardLayout(Xkb *xkb) | ||
36 | : QObject() | 41 | : QObject() | ||
37 | , m_xkb(xkb) | 42 | , m_xkb(xkb) | ||
43 | , m_notifierItem(nullptr) | ||||
38 | { | 44 | { | ||
39 | } | 45 | } | ||
40 | 46 | | |||
41 | KeyboardLayout::~KeyboardLayout() = default; | 47 | KeyboardLayout::~KeyboardLayout() = default; | ||
42 | 48 | | |||
43 | void KeyboardLayout::init() | 49 | void KeyboardLayout::init() | ||
44 | { | 50 | { | ||
45 | QAction *switchKeyboardAction = new QAction(this); | 51 | QAction *switchKeyboardAction = new QAction(this); | ||
46 | switchKeyboardAction->setObjectName(QStringLiteral("Switch to Next Keyboard Layout")); | 52 | switchKeyboardAction->setObjectName(QStringLiteral("Switch to Next Keyboard Layout")); | ||
47 | switchKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); | 53 | switchKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); | ||
48 | const QKeySequence sequence = QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_K); | 54 | const QKeySequence sequence = QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_K); | ||
49 | KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); | 55 | KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); | ||
50 | KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); | 56 | KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); | ||
51 | kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); | 57 | kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); | ||
52 | connect(switchKeyboardAction, &QAction::triggered, this, | 58 | connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout); | ||
53 | [this] { | | |||
54 | m_xkb->switchToNextLayout(); | | |||
55 | checkLayoutChange(); | | |||
56 | } | | |||
57 | ); | | |||
58 | 59 | | |||
59 | QDBusConnection::sessionBus().connect(QString(), | 60 | QDBusConnection::sessionBus().connect(QString(), | ||
60 | QStringLiteral("/Layouts"), | 61 | QStringLiteral("/Layouts"), | ||
61 | QStringLiteral("org.kde.keyboard"), | 62 | QStringLiteral("org.kde.keyboard"), | ||
62 | QStringLiteral("reloadConfig"), | 63 | QStringLiteral("reloadConfig"), | ||
63 | this, | 64 | this, | ||
64 | SLOT(reconfigure())); | 65 | SLOT(reconfigure())); | ||
65 | 66 | | |||
66 | reconfigure(); | 67 | reconfigure(); | ||
67 | } | 68 | } | ||
68 | 69 | | |||
70 | void KeyboardLayout::initNotifierItem() | ||||
71 | { | ||||
72 | bool showNotifier = true; | ||||
73 | bool showSingle = false; | ||||
74 | if (m_config) { | ||||
75 | const auto config = m_config->group(QStringLiteral("Layout")); | ||||
76 | showNotifier = config.readEntry("ShowLayoutIndicator", true); | ||||
77 | showSingle = config.readEntry("ShowSingle", false); | ||||
78 | } | ||||
79 | const bool shouldShow = showNotifier && (showSingle || m_xkb->numberOfLayouts() > 1); | ||||
80 | if (shouldShow) { | ||||
81 | if (m_notifierItem) { | ||||
82 | return; | ||||
83 | } | ||||
84 | } else { | ||||
85 | delete m_notifierItem; | ||||
86 | m_notifierItem = nullptr; | ||||
87 | return; | ||||
88 | } | ||||
89 | | ||||
90 | m_notifierItem = new KStatusNotifierItem(this); | ||||
91 | m_notifierItem->setCategory(KStatusNotifierItem::Hardware); | ||||
92 | m_notifierItem->setStatus(KStatusNotifierItem::Active); | ||||
93 | m_notifierItem->setToolTipTitle(i18nc("tooltip title", "Keyboard Layout")); | ||||
94 | m_notifierItem->setTitle(i18nc("tooltip title", "Keyboard Layout")); | ||||
95 | m_notifierItem->setToolTipIconByName(QStringLiteral("preferences-desktop-keyboard")); | ||||
96 | m_notifierItem->setStandardActionsEnabled(false); | ||||
97 | | ||||
98 | // TODO: proper icon | ||||
99 | m_notifierItem->setIconByName(QStringLiteral("preferences-desktop-keyboard")); | ||||
100 | | ||||
101 | connect(m_notifierItem, &KStatusNotifierItem::activateRequested, this, &KeyboardLayout::switchToNextLayout); | ||||
102 | connect(m_notifierItem, &KStatusNotifierItem::scrollRequested, this, | ||||
103 | [this] (int delta, Qt::Orientation orientation) { | ||||
104 | if (orientation == Qt::Horizontal) { | ||||
105 | return; | ||||
106 | } | ||||
107 | if (delta > 0) { | ||||
108 | switchToNextLayout(); | ||||
109 | } else { | ||||
110 | switchToPreviousLayout(); | ||||
111 | } | ||||
112 | } | ||||
113 | ); | ||||
114 | | ||||
115 | m_notifierItem->setStatus(KStatusNotifierItem::Active); | ||||
116 | } | ||||
117 | | ||||
118 | void KeyboardLayout::switchToNextLayout() | ||||
119 | { | ||||
120 | m_xkb->switchToNextLayout(); | ||||
121 | checkLayoutChange(); | ||||
122 | } | ||||
123 | | ||||
124 | void KeyboardLayout::switchToPreviousLayout() | ||||
125 | { | ||||
126 | m_xkb->switchToPreviousLayout(); | ||||
127 | checkLayoutChange(); | ||||
128 | } | ||||
129 | | ||||
69 | void KeyboardLayout::reconfigure() | 130 | void KeyboardLayout::reconfigure() | ||
70 | { | 131 | { | ||
71 | m_xkb->reconfigure(); | 132 | m_xkb->reconfigure(); | ||
133 | if (m_config) { | ||||
134 | m_config->reparseConfiguration(); | ||||
135 | } | ||||
72 | resetLayout(); | 136 | resetLayout(); | ||
73 | } | 137 | } | ||
74 | 138 | | |||
75 | void KeyboardLayout::resetLayout() | 139 | void KeyboardLayout::resetLayout() | ||
76 | { | 140 | { | ||
77 | m_layout = m_xkb->currentLayout(); | 141 | m_layout = m_xkb->currentLayout(); | ||
142 | initNotifierItem(); | ||||
davidedmundson: You only want this in ::reconfigure() before resetLayout and not here.
You're calling this… | |||||
Yes it does. processKeymapChange is called when a parent Wayland server informs this nested Wayland server about a new keymap. This means we have a new number of keyboard layouts and that can affect whether the notifier is shown or not. graesslin: > that won't affect whether you're showing or hiding it.
Yes it does. processKeymapChange is… | |||||
143 | updateNotifier(); | ||||
144 | reinitNotifierMenu(); | ||||
78 | } | 145 | } | ||
79 | 146 | | |||
80 | void KeyboardLayout::keyEvent(KeyEvent *event) | 147 | void KeyboardLayout::keyEvent(KeyEvent *event) | ||
81 | { | 148 | { | ||
82 | if (!event->isAutoRepeat()) { | 149 | if (!event->isAutoRepeat()) { | ||
83 | checkLayoutChange(); | 150 | checkLayoutChange(); | ||
84 | } | 151 | } | ||
85 | } | 152 | } | ||
86 | 153 | | |||
87 | void KeyboardLayout::checkLayoutChange() | 154 | void KeyboardLayout::checkLayoutChange() | ||
88 | { | 155 | { | ||
89 | const auto layout = m_xkb->currentLayout(); | 156 | const auto layout = m_xkb->currentLayout(); | ||
90 | if (m_layout == layout) { | 157 | if (m_layout == layout) { | ||
91 | return; | 158 | return; | ||
92 | } | 159 | } | ||
93 | m_layout = layout; | 160 | m_layout = layout; | ||
94 | notifyLayoutChange(); | 161 | notifyLayoutChange(); | ||
162 | updateNotifier(); | ||||
95 | } | 163 | } | ||
96 | 164 | | |||
97 | void KeyboardLayout::notifyLayoutChange() | 165 | void KeyboardLayout::notifyLayoutChange() | ||
98 | { | 166 | { | ||
99 | // notify OSD service about the new layout | 167 | // notify OSD service about the new layout | ||
100 | if (!kwinApp()->usesLibinput()) { | 168 | if (!kwinApp()->usesLibinput()) { | ||
101 | return; | 169 | return; | ||
102 | } | 170 | } | ||
103 | // only if kwin is in charge of keyboard input | 171 | // only if kwin is in charge of keyboard input | ||
104 | QDBusMessage msg = QDBusMessage::createMethodCall( | 172 | QDBusMessage msg = QDBusMessage::createMethodCall( | ||
105 | QStringLiteral("org.kde.plasmashell"), | 173 | QStringLiteral("org.kde.plasmashell"), | ||
106 | QStringLiteral("/org/kde/osdService"), | 174 | QStringLiteral("/org/kde/osdService"), | ||
107 | QStringLiteral("org.kde.osdService"), | 175 | QStringLiteral("org.kde.osdService"), | ||
108 | QStringLiteral("kbdLayoutChanged")); | 176 | QStringLiteral("kbdLayoutChanged")); | ||
109 | 177 | | |||
110 | msg << m_xkb->layoutName(); | 178 | msg << m_xkb->layoutName(); | ||
111 | 179 | | |||
112 | QDBusConnection::sessionBus().asyncCall(msg); | 180 | QDBusConnection::sessionBus().asyncCall(msg); | ||
113 | } | 181 | } | ||
114 | 182 | | |||
183 | void KeyboardLayout::updateNotifier() | ||||
184 | { | ||||
185 | if (!m_notifierItem) { | ||||
186 | return; | ||||
187 | } | ||||
188 | m_notifierItem->setToolTipSubTitle(i18nd("xkeyboard-config", m_xkb->layoutName().toUtf8().constData())); | ||||
189 | // TODO: update icon | ||||
190 | } | ||||
191 | | ||||
192 | void KeyboardLayout::reinitNotifierMenu() | ||||
193 | { | ||||
194 | if (!m_notifierItem) { | ||||
195 | return; | ||||
196 | } | ||||
197 | const auto layouts = m_xkb->layoutNames(); | ||||
198 | | ||||
199 | QMenu *menu = new QMenu; | ||||
200 | auto switchLayout = [this] (xkb_layout_index_t index) { | ||||
201 | m_xkb->switchToLayout(index); | ||||
202 | checkLayoutChange(); | ||||
203 | }; | ||||
204 | for (auto it = layouts.begin(); it != layouts.end(); it++) { | ||||
205 | menu->addAction(i18nd("xkeyboard-config", it.value().toUtf8().constData()), std::bind(switchLayout, it.key())); | ||||
davidedmundson: so KToolInvocation::startServiceByName()?
| |||||
yes, except that we used to use KToolInvocation and ported away from it. Something - in the case of KWin - doesn't work for it. The standard case of KWin is special. You can see it with the line below. We create a Process, not a QProcess. It's a specialized class to do KWin specific adjustments. Sometimes KWin is meh. graesslin: yes, except that we used to use KToolInvocation and ported away from it. Something - in the… | |||||
206 | } | ||||
207 | | ||||
208 | menu->addSeparator(); | ||||
209 | menu->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Layouts..."), this, | ||||
210 | [this] { | ||||
211 | // TODO: introduce helper function to start kcmshell5 | ||||
212 | QProcess *p = new Process(this); | ||||
213 | p->setArguments(QStringList{QStringLiteral("--args=--tab=layouts"), QStringLiteral("kcm_keyboard")}); | ||||
214 | p->setProcessEnvironment(kwinApp()->processStartupEnvironment()); | ||||
215 | p->setProgram(QStringLiteral("kcmshell5")); | ||||
216 | connect(p, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), p, &QProcess::deleteLater); | ||||
217 | connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this, | ||||
218 | [p] (QProcess::ProcessError e) { | ||||
219 | if (e == QProcess::FailedToStart) { | ||||
220 | qCDebug(KWIN_CORE) << "Failed to start kcmshell5"; | ||||
221 | } | ||||
222 | } | ||||
You probably don't want to be doing this. SNI has two two code paths for menus. addAction() which works the way you'd expect creating a DBus menu and sending that setContextMenu is a more legacy version that gets a signal from Plasma to show a menu, then kwin's process does the actual showing of it. (and generally speaking doing that won't work on wayland..it might be an exception here) Given you want a flat list, use m_notifierItem->addAction(..) even for the separator. davidedmundson: You probably don't want to be doing this.
SNI has two two code paths for menus.
addAction()… | |||||
uh nice, didn't know that and followed the existing code too blindly. graesslin: uh nice, didn't know that and followed the existing code too blindly. | |||||
This doesn't work :-( I added the actions, but the menu doesn't get shown at all. graesslin: This doesn't work :-( I added the actions, but the menu doesn't get shown at all. | |||||
223 | ); | ||||
224 | p->start(); | ||||
225 | } | ||||
226 | ); | ||||
227 | | ||||
228 | m_notifierItem->setContextMenu(menu); | ||||
229 | } | ||||
230 | | ||||
115 | } | 231 | } |
You only want this in ::reconfigure() before resetLayout and not here.
You're calling this method from reconfigure and in processKeymapChange, from keyboard_input - that won't affect whether you're showing or hiding it.