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