1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a personal information manager used to complete form
8fields.
9"""
10
11import functools
12
13from PyQt5.QtCore import Qt, QObject, QPoint
14from PyQt5.QtWidgets import QDialog, QMenu
15
16import Preferences
17import UI.PixmapCache
18
19from ..WebBrowserPage import WebBrowserPage
20
21
22class 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))
289