|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2022 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 PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt |
|
14 from PyQt6.QtGui import QKeySequence |
|
15 from PyQt6.QtWidgets import QHeaderView, QDialog, QTreeWidgetItem |
|
16 |
|
17 from EricWidgets.EricApplication import ericApp |
|
18 from EricWidgets import EricMessageBox |
|
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 (EricAction) |
|
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 = ericApp().getObject("PluginManager") |
|
129 pm.initOnDemandPlugins() |
|
130 |
|
131 # populate the various lists |
|
132 self.projectItem = self.__generateCategoryItem(self.tr("Project")) |
|
133 for act in ericApp().getObject("Project").getActions(): |
|
134 self.__generateShortcutItem(self.projectItem, act) |
|
135 |
|
136 self.uiItem = self.__generateCategoryItem(self.tr("General")) |
|
137 for act in ericApp().getObject("UserInterface").getActions('ui'): |
|
138 self.__generateShortcutItem(self.uiItem, act) |
|
139 |
|
140 self.wizardsItem = self.__generateCategoryItem(self.tr("Wizards")) |
|
141 for act in ( |
|
142 ericApp().getObject("UserInterface").getActions('wizards') |
|
143 ): |
|
144 self.__generateShortcutItem(self.wizardsItem, act) |
|
145 |
|
146 self.debugItem = self.__generateCategoryItem(self.tr("Debug")) |
|
147 for act in ericApp().getObject("DebugUI").getActions(): |
|
148 self.__generateShortcutItem(self.debugItem, act) |
|
149 |
|
150 self.editItem = self.__generateCategoryItem(self.tr("Edit")) |
|
151 for act in ericApp().getObject("ViewManager").getActions('edit'): |
|
152 self.__generateShortcutItem(self.editItem, act) |
|
153 |
|
154 self.fileItem = self.__generateCategoryItem(self.tr("File")) |
|
155 for act in ericApp().getObject("ViewManager").getActions('file'): |
|
156 self.__generateShortcutItem(self.fileItem, act) |
|
157 |
|
158 self.searchItem = self.__generateCategoryItem(self.tr("Search")) |
|
159 for act in ericApp().getObject("ViewManager").getActions('search'): |
|
160 self.__generateShortcutItem(self.searchItem, act) |
|
161 |
|
162 self.viewItem = self.__generateCategoryItem(self.tr("View")) |
|
163 for act in ericApp().getObject("ViewManager").getActions('view'): |
|
164 self.__generateShortcutItem(self.viewItem, act) |
|
165 |
|
166 self.macroItem = self.__generateCategoryItem(self.tr("Macro")) |
|
167 for act in ericApp().getObject("ViewManager").getActions('macro'): |
|
168 self.__generateShortcutItem(self.macroItem, act) |
|
169 |
|
170 self.bookmarkItem = self.__generateCategoryItem( |
|
171 self.tr("Bookmarks")) |
|
172 for act in ( |
|
173 ericApp().getObject("ViewManager").getActions('bookmark') |
|
174 ): |
|
175 self.__generateShortcutItem(self.bookmarkItem, act) |
|
176 |
|
177 self.spellingItem = self.__generateCategoryItem( |
|
178 self.tr("Spelling")) |
|
179 for act in ( |
|
180 ericApp().getObject("ViewManager").getActions('spelling') |
|
181 ): |
|
182 self.__generateShortcutItem(self.spellingItem, act) |
|
183 |
|
184 actions = ericApp().getObject("ViewManager").getActions('window') |
|
185 if actions: |
|
186 self.windowItem = self.__generateCategoryItem( |
|
187 self.tr("Window")) |
|
188 for act in actions: |
|
189 self.__generateShortcutItem(self.windowItem, act) |
|
190 |
|
191 self.pluginCategoryItems = [] |
|
192 for category, ref in ericApp().getPluginObjects(): |
|
193 if hasattr(ref, "getActions"): |
|
194 categoryItem = self.__generateCategoryItem(category) |
|
195 objectType = ericApp().getPluginObjectType(category) |
|
196 for act in ref.getActions(): |
|
197 self.__generateShortcutItem(categoryItem, act, |
|
198 objectType=objectType) |
|
199 self.pluginCategoryItems.append(categoryItem) |
|
200 |
|
201 else: |
|
202 self.helpViewerItem = self.__generateCategoryItem( |
|
203 self.tr("eric Web Browser")) |
|
204 for act in helpViewer.getActions(): |
|
205 self.__generateShortcutItem(self.helpViewerItem, act, True) |
|
206 |
|
207 self.__resort() |
|
208 self.__resizeColumns() |
|
209 |
|
210 self.__editTopItem = None |
|
211 |
|
212 def on_shortcutsList_itemDoubleClicked(self, itm, column): |
|
213 """ |
|
214 Private slot to handle a double click in the shortcuts list. |
|
215 |
|
216 @param itm the list item that was double clicked (QTreeWidgetItem) |
|
217 @param column the list item was double clicked in (integer) |
|
218 """ |
|
219 if itm.childCount(): |
|
220 return |
|
221 |
|
222 self.__editTopItem = itm.parent() |
|
223 |
|
224 self.shortcutDialog.setKeys( |
|
225 QKeySequence(itm.text(1)), |
|
226 QKeySequence(itm.text(2)), |
|
227 itm.data(0, self.noCheckRole), |
|
228 itm.data(0, self.objectTypeRole)) |
|
229 self.shortcutDialog.show() |
|
230 |
|
231 def on_shortcutsList_itemClicked(self, itm, column): |
|
232 """ |
|
233 Private slot to handle a click in the shortcuts list. |
|
234 |
|
235 @param itm the list item that was clicked (QTreeWidgetItem) |
|
236 @param column the list item was clicked in (integer) |
|
237 """ |
|
238 if itm.childCount() or column not in [1, 2]: |
|
239 return |
|
240 |
|
241 self.shortcutsList.openPersistentEditor(itm, column) |
|
242 |
|
243 def on_shortcutsList_itemChanged(self, itm, column): |
|
244 """ |
|
245 Private slot to handle the edit of a shortcut key. |
|
246 |
|
247 @param itm reference to the item changed (QTreeWidgetItem) |
|
248 @param column column changed (integer) |
|
249 """ |
|
250 if column != 0: |
|
251 keystr = itm.text(column).title() |
|
252 if ( |
|
253 not itm.data(0, self.noCheckRole) and |
|
254 not self.__checkShortcut(QKeySequence(keystr), |
|
255 itm.data(0, self.objectTypeRole), |
|
256 itm.parent()) |
|
257 ): |
|
258 itm.setText(column, "") |
|
259 else: |
|
260 itm.setText(column, keystr) |
|
261 self.shortcutsList.closePersistentEditor(itm, column) |
|
262 |
|
263 def __shortcutChanged(self, keysequence, altKeysequence, noCheck, |
|
264 objectType): |
|
265 """ |
|
266 Private slot to handle the shortcutChanged signal of the shortcut |
|
267 dialog. |
|
268 |
|
269 @param keysequence the keysequence of the changed action (QKeySequence) |
|
270 @param altKeysequence the alternative keysequence of the changed |
|
271 action (QKeySequence) |
|
272 @param noCheck flag indicating that no uniqueness check should |
|
273 be performed (boolean) |
|
274 @param objectType type of the object (string). |
|
275 """ |
|
276 if ( |
|
277 not noCheck and |
|
278 (not self.__checkShortcut( |
|
279 keysequence, objectType, self.__editTopItem) or |
|
280 not self.__checkShortcut( |
|
281 altKeysequence, objectType, self.__editTopItem)) |
|
282 ): |
|
283 return |
|
284 |
|
285 self.shortcutsList.currentItem().setText(1, keysequence.toString()) |
|
286 self.shortcutsList.currentItem().setText(2, altKeysequence.toString()) |
|
287 |
|
288 self.__resort() |
|
289 self.__resizeColumns() |
|
290 |
|
291 def __checkShortcut(self, keysequence, objectType, origTopItem): |
|
292 """ |
|
293 Private method to check a keysequence for uniqueness. |
|
294 |
|
295 @param keysequence the keysequence to check (QKeySequence) |
|
296 @param objectType type of the object (string). Entries with the same |
|
297 object type are not checked for uniqueness. |
|
298 @param origTopItem refrence to the parent of the item to be checked |
|
299 (QTreeWidgetItem) |
|
300 @return flag indicating uniqueness (boolean) |
|
301 """ |
|
302 if keysequence.isEmpty(): |
|
303 return True |
|
304 |
|
305 keystr = keysequence.toString() |
|
306 keyname = self.shortcutsList.currentItem().text(0) |
|
307 for topIndex in range(self.shortcutsList.topLevelItemCount()): |
|
308 topItem = self.shortcutsList.topLevelItem(topIndex) |
|
309 for index in range(topItem.childCount()): |
|
310 itm = topItem.child(index) |
|
311 |
|
312 # 1. shall a check be performed? |
|
313 if itm.data(0, self.noCheckRole): |
|
314 continue |
|
315 |
|
316 # 2. check object type |
|
317 itmObjectType = itm.data(0, self.objectTypeRole) |
|
318 if ( |
|
319 itmObjectType and |
|
320 itmObjectType == objectType and |
|
321 topItem != origTopItem |
|
322 ): |
|
323 continue |
|
324 |
|
325 # 3. check key name |
|
326 if itm.text(0) != keyname: |
|
327 for col in [1, 2]: |
|
328 # check against primary, then alternative binding |
|
329 itmseq = itm.text(col) |
|
330 # step 1: check if shortcut is already allocated |
|
331 if keystr == itmseq: |
|
332 res = EricMessageBox.yesNo( |
|
333 self, |
|
334 self.tr("Edit shortcuts"), |
|
335 self.tr( |
|
336 """<p><b>{0}</b> has already been""" |
|
337 """ allocated to the <b>{1}</b> action. """ |
|
338 """Remove this binding?</p>""") |
|
339 .format(keystr, itm.text(0)), |
|
340 icon=EricMessageBox.Warning) |
|
341 if res: |
|
342 itm.setText(col, "") |
|
343 return True |
|
344 else: |
|
345 return False |
|
346 |
|
347 if not itmseq: |
|
348 continue |
|
349 |
|
350 # step 2: check if shortcut hides an already allocated |
|
351 if itmseq.startswith("{0}+".format(keystr)): |
|
352 res = EricMessageBox.yesNo( |
|
353 self, |
|
354 self.tr("Edit shortcuts"), |
|
355 self.tr( |
|
356 """<p><b>{0}</b> hides the <b>{1}</b>""" |
|
357 """ action. Remove this binding?</p>""") |
|
358 .format(keystr, itm.text(0)), |
|
359 icon=EricMessageBox.Warning) |
|
360 if res: |
|
361 itm.setText(col, "") |
|
362 return True |
|
363 else: |
|
364 return False |
|
365 |
|
366 # step 3: check if shortcut is hidden by an |
|
367 # already allocated |
|
368 if keystr.startswith("{0}+".format(itmseq)): |
|
369 res = EricMessageBox.yesNo( |
|
370 self, |
|
371 self.tr("Edit shortcuts"), |
|
372 self.tr( |
|
373 """<p><b>{0}</b> is hidden by the """ |
|
374 """<b>{1}</b> action. """ |
|
375 """Remove this binding?</p>""") |
|
376 .format(keystr, itm.text(0)), |
|
377 icon=EricMessageBox.Warning) |
|
378 if res: |
|
379 itm.setText(col, "") |
|
380 return True |
|
381 else: |
|
382 return False |
|
383 |
|
384 return True |
|
385 |
|
386 def __saveCategoryActions(self, category, actions): |
|
387 """ |
|
388 Private method to save the actions for a category. |
|
389 |
|
390 @param category reference to the category item (QTreeWidgetItem) |
|
391 @param actions list of actions for the category (list of EricAction) |
|
392 """ |
|
393 for index in range(category.childCount()): |
|
394 itm = category.child(index) |
|
395 txt = itm.data(0, self.objectNameRole) |
|
396 for act in actions: |
|
397 if txt == act.objectName(): |
|
398 act.setShortcut(QKeySequence(itm.text(1))) |
|
399 act.setAlternateShortcut( |
|
400 QKeySequence(itm.text(2)), removeEmpty=True) |
|
401 break |
|
402 |
|
403 def on_buttonBox_accepted(self): |
|
404 """ |
|
405 Private slot to handle the OK button press. |
|
406 """ |
|
407 if self.__helpViewer is None: |
|
408 self.__saveCategoryActions( |
|
409 self.projectItem, |
|
410 ericApp().getObject("Project").getActions()) |
|
411 self.__saveCategoryActions( |
|
412 self.uiItem, |
|
413 ericApp().getObject("UserInterface").getActions('ui')) |
|
414 self.__saveCategoryActions( |
|
415 self.wizardsItem, |
|
416 ericApp().getObject("UserInterface").getActions('wizards')) |
|
417 self.__saveCategoryActions( |
|
418 self.debugItem, |
|
419 ericApp().getObject("DebugUI").getActions()) |
|
420 self.__saveCategoryActions( |
|
421 self.editItem, |
|
422 ericApp().getObject("ViewManager").getActions('edit')) |
|
423 self.__saveCategoryActions( |
|
424 self.fileItem, |
|
425 ericApp().getObject("ViewManager").getActions('file')) |
|
426 self.__saveCategoryActions( |
|
427 self.searchItem, |
|
428 ericApp().getObject("ViewManager").getActions('search')) |
|
429 self.__saveCategoryActions( |
|
430 self.viewItem, |
|
431 ericApp().getObject("ViewManager").getActions('view')) |
|
432 self.__saveCategoryActions( |
|
433 self.macroItem, |
|
434 ericApp().getObject("ViewManager").getActions('macro')) |
|
435 self.__saveCategoryActions( |
|
436 self.bookmarkItem, |
|
437 ericApp().getObject("ViewManager").getActions('bookmark')) |
|
438 self.__saveCategoryActions( |
|
439 self.spellingItem, |
|
440 ericApp().getObject("ViewManager").getActions('spelling')) |
|
441 |
|
442 actions = ericApp().getObject("ViewManager").getActions('window') |
|
443 if actions: |
|
444 self.__saveCategoryActions(self.windowItem, actions) |
|
445 |
|
446 for categoryItem in self.pluginCategoryItems: |
|
447 category = categoryItem.text(0) |
|
448 ref = ericApp().getPluginObject(category) |
|
449 if ref is not None and hasattr(ref, "getActions"): |
|
450 self.__saveCategoryActions(categoryItem, ref.getActions()) |
|
451 |
|
452 Shortcuts.saveShortcuts() |
|
453 |
|
454 else: |
|
455 self.__saveCategoryActions( |
|
456 self.helpViewerItem, self.__helpViewer.getActions()) |
|
457 Shortcuts.saveShortcuts(helpViewer=self.__helpViewer) |
|
458 |
|
459 Preferences.syncPreferences() |
|
460 |
|
461 self.updateShortcuts.emit() |
|
462 self.hide() |
|
463 |
|
464 @pyqtSlot(str) |
|
465 def on_searchEdit_textChanged(self, txt): |
|
466 """ |
|
467 Private slot called, when the text in the search edit changes. |
|
468 |
|
469 @param txt text of the search edit (string) |
|
470 """ |
|
471 rx = re.compile(re.escape(txt), re.IGNORECASE) |
|
472 for topIndex in range(self.shortcutsList.topLevelItemCount()): |
|
473 topItem = self.shortcutsList.topLevelItem(topIndex) |
|
474 childHiddenCount = 0 |
|
475 for index in range(topItem.childCount()): |
|
476 itm = topItem.child(index) |
|
477 if ( |
|
478 txt and ( |
|
479 (self.actionButton.isChecked() and |
|
480 rx.search(itm.text(0)) is None) or |
|
481 (self.shortcutButton.isChecked() and |
|
482 txt.lower() not in itm.text(1).lower() and |
|
483 txt.lower() not in itm.text(2).lower()) |
|
484 ) |
|
485 ): |
|
486 itm.setHidden(True) |
|
487 childHiddenCount += 1 |
|
488 else: |
|
489 itm.setHidden(False) |
|
490 topItem.setHidden(childHiddenCount == topItem.childCount()) |
|
491 |
|
492 @pyqtSlot(bool) |
|
493 def on_actionButton_toggled(self, checked): |
|
494 """ |
|
495 Private slot called, when the action radio button is toggled. |
|
496 |
|
497 @param checked state of the action radio button (boolean) |
|
498 """ |
|
499 if checked: |
|
500 self.on_searchEdit_textChanged(self.searchEdit.text()) |
|
501 |
|
502 @pyqtSlot(bool) |
|
503 def on_shortcutButton_toggled(self, checked): |
|
504 """ |
|
505 Private slot called, when the shortcuts radio button is toggled. |
|
506 |
|
507 @param checked state of the shortcuts radio button (boolean) |
|
508 """ |
|
509 if checked: |
|
510 self.on_searchEdit_textChanged(self.searchEdit.text()) |