|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a personal information manager used to complete form |
|
8 fields. |
|
9 """ |
|
10 |
|
11 import functools |
|
12 |
|
13 from PyQt6.QtCore import Qt, QObject, QPoint |
|
14 from PyQt6.QtWidgets import QDialog, QMenu |
|
15 |
|
16 import Preferences |
|
17 import UI.PixmapCache |
|
18 |
|
19 from ..WebBrowserPage import WebBrowserPage |
|
20 |
|
21 |
|
22 class PersonalInformationManager(QObject): |
|
23 """ |
|
24 Class implementing the personal information manager used to complete form |
|
25 fields. |
|
26 """ |
|
27 FullName = 0 |
|
28 LastName = 1 |
|
29 FirstName = 2 |
|
30 Email = 3 |
|
31 Mobile = 4 |
|
32 Phone = 5 |
|
33 Address = 6 |
|
34 City = 7 |
|
35 Zip = 8 |
|
36 State = 9 |
|
37 Country = 10 |
|
38 HomePage = 11 |
|
39 Special1 = 12 |
|
40 Special2 = 13 |
|
41 Special3 = 14 |
|
42 Special4 = 15 |
|
43 Max = 16 |
|
44 Invalid = 256 |
|
45 |
|
46 def __init__(self, parent=None): |
|
47 """ |
|
48 Constructor |
|
49 |
|
50 @param parent reference to the parent object (QObject) |
|
51 """ |
|
52 super().__init__(parent) |
|
53 |
|
54 self.__loaded = False |
|
55 self.__allInfo = {} |
|
56 self.__infoMatches = {} |
|
57 self.__translations = {} |
|
58 |
|
59 self.__view = None |
|
60 self.__clickedPos = QPoint() |
|
61 |
|
62 def __loadSettings(self): |
|
63 """ |
|
64 Private method to load the settings. |
|
65 """ |
|
66 self.__allInfo[self.FullName] = Preferences.getWebBrowser( |
|
67 "PimFullName") |
|
68 self.__allInfo[self.LastName] = Preferences.getWebBrowser( |
|
69 "PimLastName") |
|
70 self.__allInfo[self.FirstName] = Preferences.getWebBrowser( |
|
71 "PimFirstName") |
|
72 self.__allInfo[self.Email] = Preferences.getWebBrowser("PimEmail") |
|
73 self.__allInfo[self.Mobile] = Preferences.getWebBrowser("PimMobile") |
|
74 self.__allInfo[self.Phone] = Preferences.getWebBrowser("PimPhone") |
|
75 self.__allInfo[self.Address] = Preferences.getWebBrowser("PimAddress") |
|
76 self.__allInfo[self.City] = Preferences.getWebBrowser("PimCity") |
|
77 self.__allInfo[self.Zip] = Preferences.getWebBrowser("PimZip") |
|
78 self.__allInfo[self.State] = Preferences.getWebBrowser("PimState") |
|
79 self.__allInfo[self.Country] = Preferences.getWebBrowser("PimCountry") |
|
80 self.__allInfo[self.HomePage] = Preferences.getWebBrowser( |
|
81 "PimHomePage") |
|
82 self.__allInfo[self.Special1] = Preferences.getWebBrowser( |
|
83 "PimSpecial1") |
|
84 self.__allInfo[self.Special2] = Preferences.getWebBrowser( |
|
85 "PimSpecial2") |
|
86 self.__allInfo[self.Special3] = Preferences.getWebBrowser( |
|
87 "PimSpecial3") |
|
88 self.__allInfo[self.Special4] = Preferences.getWebBrowser( |
|
89 "PimSpecial4") |
|
90 |
|
91 self.__translations[self.FullName] = self.tr("Full Name") |
|
92 self.__translations[self.LastName] = self.tr("Last Name") |
|
93 self.__translations[self.FirstName] = self.tr("First Name") |
|
94 self.__translations[self.Email] = self.tr("E-mail") |
|
95 self.__translations[self.Mobile] = self.tr("Mobile") |
|
96 self.__translations[self.Phone] = self.tr("Phone") |
|
97 self.__translations[self.Address] = self.tr("Address") |
|
98 self.__translations[self.City] = self.tr("City") |
|
99 self.__translations[self.Zip] = self.tr("ZIP Code") |
|
100 self.__translations[self.State] = self.tr("State/Region") |
|
101 self.__translations[self.Country] = self.tr("Country") |
|
102 self.__translations[self.HomePage] = self.tr("Home Page") |
|
103 self.__translations[self.Special1] = self.tr("Custom 1") |
|
104 self.__translations[self.Special2] = self.tr("Custom 2") |
|
105 self.__translations[self.Special3] = self.tr("Custom 3") |
|
106 self.__translations[self.Special4] = self.tr("Custom 4") |
|
107 |
|
108 self.__infoMatches[self.FullName] = ["fullname", "realname"] |
|
109 self.__infoMatches[self.LastName] = ["lastname", "surname"] |
|
110 self.__infoMatches[self.FirstName] = ["firstname", "name"] |
|
111 self.__infoMatches[self.Email] = ["email", "e-mail", "mail"] |
|
112 self.__infoMatches[self.Mobile] = ["mobile", "mobilephone"] |
|
113 self.__infoMatches[self.Phone] = ["phone", "telephone"] |
|
114 self.__infoMatches[self.Address] = ["address"] |
|
115 self.__infoMatches[self.City] = ["city"] |
|
116 self.__infoMatches[self.Zip] = ["zip"] |
|
117 self.__infoMatches[self.State] = ["state", "region"] |
|
118 self.__infoMatches[self.Country] = ["country"] |
|
119 self.__infoMatches[self.HomePage] = ["homepage", "www"] |
|
120 |
|
121 self.__loaded = True |
|
122 |
|
123 def showConfigurationDialog(self): |
|
124 """ |
|
125 Public method to show the configuration dialog. |
|
126 """ |
|
127 from .PersonalDataDialog import PersonalDataDialog |
|
128 dlg = PersonalDataDialog() |
|
129 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
130 dlg.storeData() |
|
131 self.__loadSettings() |
|
132 |
|
133 def createSubMenu(self, menu, view, hitTestResult): |
|
134 """ |
|
135 Public method to create the personal information sub-menu. |
|
136 |
|
137 @param menu reference to the main menu (QMenu) |
|
138 @param view reference to the view (HelpBrowser) |
|
139 @param hitTestResult reference to the hit test result |
|
140 (WebHitTestResult) |
|
141 """ |
|
142 self.__view = view |
|
143 self.__clickedPos = hitTestResult.pos() |
|
144 |
|
145 if not hitTestResult.isContentEditable(): |
|
146 return |
|
147 |
|
148 if not self.__loaded: |
|
149 self.__loadSettings() |
|
150 |
|
151 submenu = QMenu(self.tr("Insert Personal Information"), menu) |
|
152 submenu.setIcon(UI.PixmapCache.getIcon("pim")) |
|
153 |
|
154 for key, info in sorted(self.__allInfo.items()): |
|
155 if info: |
|
156 act = submenu.addAction(self.__translations[key]) |
|
157 act.setData(info) |
|
158 act.triggered.connect( |
|
159 functools.partial(self.__insertData, act)) |
|
160 |
|
161 submenu.addSeparator() |
|
162 submenu.addAction(self.tr("Edit Personal Information"), |
|
163 self.showConfigurationDialog) |
|
164 |
|
165 menu.addMenu(submenu) |
|
166 menu.addSeparator() |
|
167 |
|
168 def __insertData(self, act): |
|
169 """ |
|
170 Private slot to insert the selected personal information. |
|
171 |
|
172 @param act reference to the action that triggered |
|
173 @type QAction |
|
174 """ |
|
175 if self.__view is None or self.__clickedPos.isNull(): |
|
176 return |
|
177 |
|
178 info = act.data() |
|
179 info = info.replace('"', '\\"') |
|
180 |
|
181 source = """ |
|
182 var e = document.elementFromPoint({0}, {1}); |
|
183 if (e) {{ |
|
184 var v = e.value.substring(0, e.selectionStart); |
|
185 v += "{2}" + e.value.substring(e.selectionEnd); |
|
186 e.value = v; |
|
187 }}""".format(self.__clickedPos.x(), self.__clickedPos.y(), info) |
|
188 self.__view.page().runJavaScript(source, WebBrowserPage.SafeJsWorld) |
|
189 |
|
190 def viewKeyPressEvent(self, view, evt): |
|
191 """ |
|
192 Protected method to handle key press events we are interested in. |
|
193 |
|
194 @param view reference to the view (HelpBrowser) |
|
195 @param evt reference to the key event (QKeyEvent) |
|
196 @return flag indicating handling of the event (boolean) |
|
197 """ |
|
198 if view is None: |
|
199 return False |
|
200 |
|
201 isEnter = evt.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter] |
|
202 isControlModifier = ( |
|
203 evt.modifiers() & Qt.KeyboardModifier.ControlModifier |
|
204 ) |
|
205 if not isEnter or not isControlModifier: |
|
206 return False |
|
207 |
|
208 if not self.__loaded: |
|
209 self.__loadSettings() |
|
210 |
|
211 source = """ |
|
212 var inputs = document.getElementsByTagName('input'); |
|
213 var table = {0}; |
|
214 for (var i = 0; i < inputs.length; ++i) {{ |
|
215 var input = inputs[i]; |
|
216 if (input.type != 'text' || input.name == '') |
|
217 continue; |
|
218 for (var key in table) {{ |
|
219 if (!table.hasOwnProperty(key)) |
|
220 continue; |
|
221 if (key == input.name || input.name.indexOf(key) != -1) {{ |
|
222 input.value = table[key]; |
|
223 break; |
|
224 }} |
|
225 }} |
|
226 }}""".format(self.__matchingJsTable()) |
|
227 view.page().runJavaScript(source, WebBrowserPage.SafeJsWorld) |
|
228 |
|
229 return True |
|
230 |
|
231 def connectPage(self, page): |
|
232 """ |
|
233 Public method to allow the personal information manager to connect to |
|
234 the page. |
|
235 |
|
236 @param page reference to the web page |
|
237 @type WebBrowserPage |
|
238 """ |
|
239 page.loadFinished.connect(lambda ok: self.__pageLoadFinished(ok, page)) |
|
240 |
|
241 def __pageLoadFinished(self, ok, page): |
|
242 """ |
|
243 Private slot to handle the completion of a page load. |
|
244 |
|
245 @param ok flag indicating a successful load |
|
246 @type bool |
|
247 @param page reference to the web page object |
|
248 @type WebBrowserPage |
|
249 """ |
|
250 if page is None or not ok: |
|
251 return |
|
252 |
|
253 if not self.__loaded: |
|
254 self.__loadSettings() |
|
255 |
|
256 source = """ |
|
257 var inputs = document.getElementsByTagName('input'); |
|
258 var table = {0}; |
|
259 for (var i = 0; i < inputs.length; ++i) {{ |
|
260 var input = inputs[i]; |
|
261 if (input.type != 'text' || input.name == '') |
|
262 continue; |
|
263 for (var key in table) {{ |
|
264 if (!table.hasOwnProperty(key) || table[key] == '') |
|
265 continue; |
|
266 if (key == input.name || input.name.indexOf(key) != -1) {{ |
|
267 input.style['-webkit-appearance'] = 'none'; |
|
268 input.style['-webkit-box-shadow'] = |
|
269 'inset 0 0 2px 1px #000EEE'; |
|
270 break; |
|
271 }} |
|
272 }} |
|
273 }}""".format(self.__matchingJsTable()) |
|
274 page.runJavaScript(source, WebBrowserPage.SafeJsWorld) |
|
275 |
|
276 def __matchingJsTable(self): |
|
277 """ |
|
278 Private method to create the common part of the JavaScript sources. |
|
279 |
|
280 @return JavaScript source |
|
281 @rtype str |
|
282 """ |
|
283 values = [] |
|
284 for key, names in self.__infoMatches.items(): |
|
285 for name in names: |
|
286 value = self.__allInfo[key].replace('"', '\\"') |
|
287 values.append('"{0}":"{1}"'.format(name, value)) |
|
288 return "{{ {0} }}".format(",".join(values)) |