|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog for the configuration of eric6's keyboard |
|
8 shortcuts. |
|
9 """ |
|
10 |
|
11 from __future__ import unicode_literals |
|
12 |
|
13 from PyQt5.QtCore import pyqtSignal, QRegExp, Qt, pyqtSlot |
|
14 from PyQt5.QtGui import QKeySequence |
|
15 from PyQt5.QtWidgets import QHeaderView, QDialog, QTreeWidgetItem |
|
16 |
|
17 from E5Gui.E5Application import e5App |
|
18 from E5Gui import E5MessageBox |
|
19 |
|
20 from .Ui_ShortcutsDialog import Ui_ShortcutsDialog |
|
21 |
|
22 import Preferences |
|
23 from Preferences import Shortcuts |
|
24 |
|
25 |
|
26 class ShortcutsDialog(QDialog, Ui_ShortcutsDialog): |
|
27 """ |
|
28 Class implementing a dialog for the configuration of eric6's keyboard |
|
29 shortcuts. |
|
30 |
|
31 @signal updateShortcuts() emitted when the user pressed the dialogs OK |
|
32 button |
|
33 """ |
|
34 updateShortcuts = pyqtSignal() |
|
35 |
|
36 objectNameRole = Qt.UserRole |
|
37 noCheckRole = Qt.UserRole + 1 |
|
38 objectTypeRole = Qt.UserRole + 2 |
|
39 |
|
40 def __init__(self, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param parent parent widget of this dialog |
|
45 @type QWidget |
|
46 """ |
|
47 super(ShortcutsDialog, self).__init__(parent) |
|
48 self.setupUi(self) |
|
49 self.setWindowFlags(Qt.Window) |
|
50 |
|
51 self.__helpViewer = None |
|
52 |
|
53 self.shortcutsList.headerItem().setText( |
|
54 self.shortcutsList.columnCount(), "") |
|
55 self.shortcutsList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
56 |
|
57 from .ShortcutDialog import ShortcutDialog |
|
58 self.shortcutDialog = ShortcutDialog() |
|
59 self.shortcutDialog.shortcutChanged.connect(self.__shortcutChanged) |
|
60 |
|
61 def __resort(self): |
|
62 """ |
|
63 Private method to resort the tree. |
|
64 """ |
|
65 self.shortcutsList.sortItems( |
|
66 self.shortcutsList.sortColumn(), |
|
67 self.shortcutsList.header().sortIndicatorOrder()) |
|
68 |
|
69 def __resizeColumns(self): |
|
70 """ |
|
71 Private method to resize the list columns. |
|
72 """ |
|
73 self.shortcutsList.header().resizeSections( |
|
74 QHeaderView.ResizeToContents) |
|
75 self.shortcutsList.header().setStretchLastSection(True) |
|
76 |
|
77 def __generateCategoryItem(self, title): |
|
78 """ |
|
79 Private method to generate a category item. |
|
80 |
|
81 @param title title for the item (string) |
|
82 @return reference to the category item (QTreeWidgetItem) |
|
83 """ |
|
84 itm = QTreeWidgetItem(self.shortcutsList, [title]) |
|
85 itm.setExpanded(True) |
|
86 return itm |
|
87 |
|
88 def __generateShortcutItem(self, category, action, |
|
89 noCheck=False, objectType=""): |
|
90 """ |
|
91 Private method to generate a keyboard shortcut item. |
|
92 |
|
93 @param category reference to the category item (QTreeWidgetItem) |
|
94 @param action reference to the keyboard action (E5Action) |
|
95 @keyparam noCheck flag indicating that no uniqueness check should |
|
96 be performed (boolean) |
|
97 @keyparam objectType type of the object (string). Objects of the same |
|
98 type are not checked for duplicate shortcuts. |
|
99 """ |
|
100 itm = QTreeWidgetItem( |
|
101 category, |
|
102 [action.iconText(), action.shortcut().toString(), |
|
103 action.alternateShortcut().toString()]) |
|
104 itm.setIcon(0, action.icon()) |
|
105 itm.setData(0, self.objectNameRole, action.objectName()) |
|
106 itm.setData(0, self.noCheckRole, noCheck) |
|
107 if objectType: |
|
108 itm.setData(0, self.objectTypeRole, objectType) |
|
109 else: |
|
110 itm.setData(0, self.objectTypeRole, None) |
|
111 |
|
112 def populate(self, helpViewer=None): |
|
113 """ |
|
114 Public method to populate the dialog. |
|
115 |
|
116 @param helpViewer reference to the help window object |
|
117 """ |
|
118 self.searchEdit.clear() |
|
119 self.searchEdit.setFocus() |
|
120 self.shortcutsList.clear() |
|
121 self.actionButton.setChecked(True) |
|
122 |
|
123 self.__helpViewer = helpViewer |
|
124 |
|
125 if helpViewer is None: |
|
126 # let the plugin manager create on demand plugin objects |
|
127 pm = e5App().getObject("PluginManager") |
|
128 pm.initOnDemandPlugins() |
|
129 |
|
130 # populate the various lists |
|
131 self.projectItem = self.__generateCategoryItem(self.tr("Project")) |
|
132 for act in e5App().getObject("Project").getActions(): |
|
133 self.__generateShortcutItem(self.projectItem, act) |
|
134 |
|
135 self.uiItem = self.__generateCategoryItem(self.tr("General")) |
|
136 for act in e5App().getObject("UserInterface").getActions('ui'): |
|
137 self.__generateShortcutItem(self.uiItem, act) |
|
138 |
|
139 self.wizardsItem = self.__generateCategoryItem(self.tr("Wizards")) |
|
140 for act in e5App().getObject("UserInterface")\ |
|
141 .getActions('wizards'): |
|
142 self.__generateShortcutItem(self.wizardsItem, act) |
|
143 |
|
144 self.debugItem = self.__generateCategoryItem(self.tr("Debug")) |
|
145 for act in e5App().getObject("DebugUI").getActions(): |
|
146 self.__generateShortcutItem(self.debugItem, act) |
|
147 |
|
148 self.editItem = self.__generateCategoryItem(self.tr("Edit")) |
|
149 for act in e5App().getObject("ViewManager").getActions('edit'): |
|
150 self.__generateShortcutItem(self.editItem, act) |
|
151 |
|
152 self.fileItem = self.__generateCategoryItem(self.tr("File")) |
|
153 for act in e5App().getObject("ViewManager").getActions('file'): |
|
154 self.__generateShortcutItem(self.fileItem, act) |
|
155 |
|
156 self.searchItem = self.__generateCategoryItem(self.tr("Search")) |
|
157 for act in e5App().getObject("ViewManager").getActions('search'): |
|
158 self.__generateShortcutItem(self.searchItem, act) |
|
159 |
|
160 self.viewItem = self.__generateCategoryItem(self.tr("View")) |
|
161 for act in e5App().getObject("ViewManager").getActions('view'): |
|
162 self.__generateShortcutItem(self.viewItem, act) |
|
163 |
|
164 self.macroItem = self.__generateCategoryItem(self.tr("Macro")) |
|
165 for act in e5App().getObject("ViewManager").getActions('macro'): |
|
166 self.__generateShortcutItem(self.macroItem, act) |
|
167 |
|
168 self.bookmarkItem = self.__generateCategoryItem( |
|
169 self.tr("Bookmarks")) |
|
170 for act in e5App().getObject("ViewManager").getActions('bookmark'): |
|
171 self.__generateShortcutItem(self.bookmarkItem, act) |
|
172 |
|
173 self.spellingItem = self.__generateCategoryItem( |
|
174 self.tr("Spelling")) |
|
175 for act in e5App().getObject("ViewManager").getActions('spelling'): |
|
176 self.__generateShortcutItem(self.spellingItem, act) |
|
177 |
|
178 actions = e5App().getObject("ViewManager").getActions('window') |
|
179 if actions: |
|
180 self.windowItem = self.__generateCategoryItem( |
|
181 self.tr("Window")) |
|
182 for act in actions: |
|
183 self.__generateShortcutItem(self.windowItem, act) |
|
184 |
|
185 self.pluginCategoryItems = [] |
|
186 for category, ref in e5App().getPluginObjects(): |
|
187 if hasattr(ref, "getActions"): |
|
188 categoryItem = self.__generateCategoryItem(category) |
|
189 objectType = e5App().getPluginObjectType(category) |
|
190 for act in ref.getActions(): |
|
191 self.__generateShortcutItem(categoryItem, act, |
|
192 objectType=objectType) |
|
193 self.pluginCategoryItems.append(categoryItem) |
|
194 |
|
195 else: |
|
196 self.helpViewerItem = self.__generateCategoryItem( |
|
197 self.tr("eric6 Web Browser")) |
|
198 for act in helpViewer.getActions(): |
|
199 self.__generateShortcutItem(self.helpViewerItem, act, True) |
|
200 |
|
201 self.__resort() |
|
202 self.__resizeColumns() |
|
203 |
|
204 self.__editTopItem = None |
|
205 |
|
206 def on_shortcutsList_itemDoubleClicked(self, itm, column): |
|
207 """ |
|
208 Private slot to handle a double click in the shortcuts list. |
|
209 |
|
210 @param itm the list item that was double clicked (QTreeWidgetItem) |
|
211 @param column the list item was double clicked in (integer) |
|
212 """ |
|
213 if itm.childCount(): |
|
214 return |
|
215 |
|
216 self.__editTopItem = itm.parent() |
|
217 |
|
218 self.shortcutDialog.setKeys( |
|
219 QKeySequence(itm.text(1)), |
|
220 QKeySequence(itm.text(2)), |
|
221 itm.data(0, self.noCheckRole), |
|
222 itm.data(0, self.objectTypeRole)) |
|
223 self.shortcutDialog.show() |
|
224 |
|
225 def on_shortcutsList_itemClicked(self, itm, column): |
|
226 """ |
|
227 Private slot to handle a click in the shortcuts list. |
|
228 |
|
229 @param itm the list item that was clicked (QTreeWidgetItem) |
|
230 @param column the list item was clicked in (integer) |
|
231 """ |
|
232 if itm.childCount() or column not in [1, 2]: |
|
233 return |
|
234 |
|
235 self.shortcutsList.openPersistentEditor(itm, column) |
|
236 |
|
237 def on_shortcutsList_itemChanged(self, itm, column): |
|
238 """ |
|
239 Private slot to handle the edit of a shortcut key. |
|
240 |
|
241 @param itm reference to the item changed (QTreeWidgetItem) |
|
242 @param column column changed (integer) |
|
243 """ |
|
244 if column != 0: |
|
245 keystr = itm.text(column).title() |
|
246 if not itm.data(0, self.noCheckRole) and \ |
|
247 not self.__checkShortcut(QKeySequence(keystr), |
|
248 itm.data(0, self.objectTypeRole), |
|
249 itm.parent()): |
|
250 itm.setText(column, "") |
|
251 else: |
|
252 itm.setText(column, keystr) |
|
253 self.shortcutsList.closePersistentEditor(itm, column) |
|
254 |
|
255 def __shortcutChanged(self, keysequence, altKeysequence, noCheck, |
|
256 objectType): |
|
257 """ |
|
258 Private slot to handle the shortcutChanged signal of the shortcut |
|
259 dialog. |
|
260 |
|
261 @param keysequence the keysequence of the changed action (QKeySequence) |
|
262 @param altKeysequence the alternative keysequence of the changed |
|
263 action (QKeySequence) |
|
264 @param noCheck flag indicating that no uniqueness check should |
|
265 be performed (boolean) |
|
266 @param objectType type of the object (string). |
|
267 """ |
|
268 if not noCheck and \ |
|
269 (not self.__checkShortcut( |
|
270 keysequence, objectType, self.__editTopItem) or |
|
271 not self.__checkShortcut( |
|
272 altKeysequence, objectType, self.__editTopItem)): |
|
273 return |
|
274 |
|
275 self.shortcutsList.currentItem().setText(1, keysequence.toString()) |
|
276 self.shortcutsList.currentItem().setText(2, altKeysequence.toString()) |
|
277 |
|
278 self.__resort() |
|
279 self.__resizeColumns() |
|
280 |
|
281 def __checkShortcut(self, keysequence, objectType, origTopItem): |
|
282 """ |
|
283 Private method to check a keysequence for uniqueness. |
|
284 |
|
285 @param keysequence the keysequence to check (QKeySequence) |
|
286 @param objectType type of the object (string). Entries with the same |
|
287 object type are not checked for uniqueness. |
|
288 @param origTopItem refrence to the parent of the item to be checked |
|
289 (QTreeWidgetItem) |
|
290 @return flag indicating uniqueness (boolean) |
|
291 """ |
|
292 if keysequence.isEmpty(): |
|
293 return True |
|
294 |
|
295 keystr = keysequence.toString() |
|
296 keyname = self.shortcutsList.currentItem().text(0) |
|
297 for topIndex in range(self.shortcutsList.topLevelItemCount()): |
|
298 topItem = self.shortcutsList.topLevelItem(topIndex) |
|
299 for index in range(topItem.childCount()): |
|
300 itm = topItem.child(index) |
|
301 |
|
302 # 1. shall a check be performed? |
|
303 if itm.data(0, self.noCheckRole): |
|
304 continue |
|
305 |
|
306 # 2. check object type |
|
307 itmObjectType = itm.data(0, self.objectTypeRole) |
|
308 if itmObjectType and \ |
|
309 itmObjectType == objectType and \ |
|
310 topItem != origTopItem: |
|
311 continue |
|
312 |
|
313 # 3. check key name |
|
314 if itm.text(0) != keyname: |
|
315 for col in [1, 2]: |
|
316 # check against primary, then alternative binding |
|
317 itmseq = itm.text(col) |
|
318 # step 1: check if shortcut is already allocated |
|
319 if keystr == itmseq: |
|
320 res = E5MessageBox.yesNo( |
|
321 self, |
|
322 self.tr("Edit shortcuts"), |
|
323 self.tr( |
|
324 """<p><b>{0}</b> has already been""" |
|
325 """ allocated to the <b>{1}</b> action. """ |
|
326 """Remove this binding?</p>""") |
|
327 .format(keystr, itm.text(0)), |
|
328 icon=E5MessageBox.Warning) |
|
329 if res: |
|
330 itm.setText(col, "") |
|
331 return True |
|
332 else: |
|
333 return False |
|
334 |
|
335 if not itmseq: |
|
336 continue |
|
337 |
|
338 # step 2: check if shortcut hides an already allocated |
|
339 if itmseq.startswith("{0}+".format(keystr)): |
|
340 res = E5MessageBox.yesNo( |
|
341 self, |
|
342 self.tr("Edit shortcuts"), |
|
343 self.tr( |
|
344 """<p><b>{0}</b> hides the <b>{1}</b>""" |
|
345 """ action. Remove this binding?</p>""") |
|
346 .format(keystr, itm.text(0)), |
|
347 icon=E5MessageBox.Warning) |
|
348 if res: |
|
349 itm.setText(col, "") |
|
350 return True |
|
351 else: |
|
352 return False |
|
353 |
|
354 # step 3: check if shortcut is hidden by an |
|
355 # already allocated |
|
356 if keystr.startswith("{0}+".format(itmseq)): |
|
357 res = E5MessageBox.yesNo( |
|
358 self, |
|
359 self.tr("Edit shortcuts"), |
|
360 self.tr( |
|
361 """<p><b>{0}</b> is hidden by the """ |
|
362 """<b>{1}</b> action. """ |
|
363 """Remove this binding?</p>""") |
|
364 .format(keystr, itm.text(0)), |
|
365 icon=E5MessageBox.Warning) |
|
366 if res: |
|
367 itm.setText(col, "") |
|
368 return True |
|
369 else: |
|
370 return False |
|
371 |
|
372 return True |
|
373 |
|
374 def __saveCategoryActions(self, category, actions): |
|
375 """ |
|
376 Private method to save the actions for a category. |
|
377 |
|
378 @param category reference to the category item (QTreeWidgetItem) |
|
379 @param actions list of actions for the category (list of E5Action) |
|
380 """ |
|
381 for index in range(category.childCount()): |
|
382 itm = category.child(index) |
|
383 txt = itm.data(0, self.objectNameRole) |
|
384 for act in actions: |
|
385 if txt == act.objectName(): |
|
386 act.setShortcut(QKeySequence(itm.text(1))) |
|
387 act.setAlternateShortcut( |
|
388 QKeySequence(itm.text(2)), removeEmpty=True) |
|
389 break |
|
390 |
|
391 def on_buttonBox_accepted(self): |
|
392 """ |
|
393 Private slot to handle the OK button press. |
|
394 """ |
|
395 if self.__helpViewer is None: |
|
396 self.__saveCategoryActions( |
|
397 self.projectItem, |
|
398 e5App().getObject("Project").getActions()) |
|
399 self.__saveCategoryActions( |
|
400 self.uiItem, |
|
401 e5App().getObject("UserInterface").getActions('ui')) |
|
402 self.__saveCategoryActions( |
|
403 self.wizardsItem, |
|
404 e5App().getObject("UserInterface").getActions('wizards')) |
|
405 self.__saveCategoryActions( |
|
406 self.debugItem, |
|
407 e5App().getObject("DebugUI").getActions()) |
|
408 self.__saveCategoryActions( |
|
409 self.editItem, |
|
410 e5App().getObject("ViewManager").getActions('edit')) |
|
411 self.__saveCategoryActions( |
|
412 self.fileItem, |
|
413 e5App().getObject("ViewManager").getActions('file')) |
|
414 self.__saveCategoryActions( |
|
415 self.searchItem, |
|
416 e5App().getObject("ViewManager").getActions('search')) |
|
417 self.__saveCategoryActions( |
|
418 self.viewItem, |
|
419 e5App().getObject("ViewManager").getActions('view')) |
|
420 self.__saveCategoryActions( |
|
421 self.macroItem, |
|
422 e5App().getObject("ViewManager").getActions('macro')) |
|
423 self.__saveCategoryActions( |
|
424 self.bookmarkItem, |
|
425 e5App().getObject("ViewManager").getActions('bookmark')) |
|
426 self.__saveCategoryActions( |
|
427 self.spellingItem, |
|
428 e5App().getObject("ViewManager").getActions('spelling')) |
|
429 |
|
430 actions = e5App().getObject("ViewManager").getActions('window') |
|
431 if actions: |
|
432 self.__saveCategoryActions(self.windowItem, actions) |
|
433 |
|
434 for categoryItem in self.pluginCategoryItems: |
|
435 category = categoryItem.text(0) |
|
436 ref = e5App().getPluginObject(category) |
|
437 if ref is not None and hasattr(ref, "getActions"): |
|
438 self.__saveCategoryActions(categoryItem, ref.getActions()) |
|
439 |
|
440 Shortcuts.saveShortcuts() |
|
441 |
|
442 else: |
|
443 self.__saveCategoryActions( |
|
444 self.helpViewerItem, self.__helpViewer.getActions()) |
|
445 Shortcuts.saveShortcuts(helpViewer=self.__helpViewer) |
|
446 |
|
447 Preferences.syncPreferences() |
|
448 |
|
449 self.updateShortcuts.emit() |
|
450 self.hide() |
|
451 |
|
452 @pyqtSlot(str) |
|
453 def on_searchEdit_textChanged(self, txt): |
|
454 """ |
|
455 Private slot called, when the text in the search edit changes. |
|
456 |
|
457 @param txt text of the search edit (string) |
|
458 """ |
|
459 for topIndex in range(self.shortcutsList.topLevelItemCount()): |
|
460 topItem = self.shortcutsList.topLevelItem(topIndex) |
|
461 childHiddenCount = 0 |
|
462 for index in range(topItem.childCount()): |
|
463 itm = topItem.child(index) |
|
464 if (self.actionButton.isChecked() and |
|
465 not QRegExp(txt, Qt.CaseInsensitive).indexIn( |
|
466 itm.text(0)) > -1) or \ |
|
467 (self.shortcutButton.isChecked() and |
|
468 not txt.lower() in itm.text(1).lower() and |
|
469 not txt.lower() in itm.text(2).lower()): |
|
470 itm.setHidden(True) |
|
471 childHiddenCount += 1 |
|
472 else: |
|
473 itm.setHidden(False) |
|
474 topItem.setHidden(childHiddenCount == topItem.childCount()) |
|
475 |
|
476 @pyqtSlot(bool) |
|
477 def on_actionButton_toggled(self, checked): |
|
478 """ |
|
479 Private slot called, when the action radio button is toggled. |
|
480 |
|
481 @param checked state of the action radio button (boolean) |
|
482 """ |
|
483 if checked: |
|
484 self.on_searchEdit_textChanged(self.searchEdit.text()) |
|
485 |
|
486 @pyqtSlot(bool) |
|
487 def on_shortcutButton_toggled(self, checked): |
|
488 """ |
|
489 Private slot called, when the shortcuts radio button is toggled. |
|
490 |
|
491 @param checked state of the shortcuts radio button (boolean) |
|
492 """ |
|
493 if checked: |
|
494 self.on_searchEdit_textChanged(self.searchEdit.text()) |