1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """The program graphical user interface.
19
20 @todo 0.0.9:
21 Correct the input selection bug.
22 """
23
24 from PyQt5 import QtCore, QtGui, QtWidgets
25 from logging import StreamHandler, DEBUG
26 from tipy.db import SqliteDatabaseConnector
27 from sqlite3 import OperationalError
28 from tipy.minr import CorpusMiner, DictMiner, FacebookMiner
29 from tipy.lg import (lg, CNRM, CBLK, CRED, CGRN, CYEL, CBLU, CMAG, CCYN, CWHT,
30 BNRM, BBLK, BRED, BGRN, BYEL, BBLU, BMAG, BCYN, BWHT, BOLD,
31 CF)
32 from distutils.version import LooseVersion
33
34
35 -class LoggerTextEdit(QtWidgets.QTextEdit):
36 """A multCSVLine text field showing log messages."""
37
38 - def __init__(self, config):
39 super(LoggerTextEdit, self).__init__()
40 self.pal = QtGui.QPalette()
41 textc = QtGui.QColor(255, 255, 255)
42 self.pal.setColor(QtGui.QPalette.Text, textc)
43 self.setPalette(self.pal)
44 self.setReadOnly(True)
45 font = QtGui.QFont()
46 try:
47 font.setPointSize(config.getas('GUI', 'font_size', 'int'))
48 except KeyError:
49 font.setPointSize(10)
50 self.setFont(font)
51
52 - def write(self, text):
53 """Simulate LogHandler by having a write() method."""
54 text = text.replace(CNRM, '<font color="White">')
55 text = text.replace(CBLK, '<font color="Gray">')
56 text = text.replace(CRED, '<font color="Crimson">')
57 text = text.replace(CGRN, '<font color="LimeGreen">')
58 text = text.replace(CYEL, '<font color="Gold">')
59 text = text.replace(CBLU, '<font color="RoyalBlue ">')
60 text = text.replace(CMAG, '<font color="DeepPink ">')
61 text = text.replace(CCYN, '<font color="MediumAquaMarine ">')
62 text = text.replace(CWHT, '<font color="White">')
63 text = text.replace(BNRM, '<b><font color="White"></b>')
64 text = text.replace(BBLK, '<b><font color="Gray"></b>')
65 text = text.replace(BRED, '<b><font color="Crimson"></b>')
66 text = text.replace(BGRN, '<b><font color="LimeGreen"></b>')
67 text = text.replace(BYEL, '<b><font color="Gold"></b>')
68 text = text.replace(BBLU, '<b><font color="RoyalBlue "></b>')
69 text = text.replace(BMAG, '<b><font color="DeepPink "></b>')
70 text = text.replace(BCYN, '<b><font color="MediumAquaMarine "></b>')
71 text = text.replace(BWHT, '<b><font color="White"></b>')
72 text = text.replace(BOLD, '<b></b>')
73 text = text.replace('\n', '<br>')
74 text = text + '</font>'
75 self.insertHtml(text)
76
77
92
93
94 -class MainWindow(QtWidgets.QMainWindow):
95 """The main window of the graphical user interface.
96
97 G{classtree MainWindow}
98 """
99
100 CORPUS_NGRAM_PREDICTOR = 0
101 USER_SMOOTH_NGRAM_PREDICTOR = 1
102 FB_SMOOTH_NGRAM_PREDICTOR = 2
103 LAST_OCCUR_PREDICTOR = 3
104 MEMORIZE_PREDICTOR = 4
105 DICTIONARY_PREDICTOR = 5
106 TEXT_FILE_MINER = 0
107 DICTIONARY_MINER = 1
108 FACEBOOK_MINER = 2
109 TWITTER_MINER = 3
110
111 - def __init__(self, driver):
112 """MainWindow creator.
113
114 It allow the user to:
115 - Type text and get predictive suggestions.
116 - Directly complete input words or insert predicted words in the
117 input.
118 - See the miners informations.
119 - Execute mining operations using the defined miners.
120 - Delete miners database.
121 - Modify almost every options of the configuration file using the
122 settings window.
123
124 @param driver:
125 The Driver instance wich contains everything needed for word
126 prediction.
127 @type driver: L{Driver}
128 """
129 super(MainWindow, self).__init__()
130 self.driver = driver
131 self.config = self.driver.configuration
132 self.predictors = [
133 'CorpusNgramPredictor',
134 'InputNgramPredictor',
135 'FbNgramPredictor',
136 'LateOccurPredictor',
137 'MemorizePredictor',
138 'DictionaryPredictor']
139 self.miners = [
140 'CorpusMiner',
141 'DictMiner',
142 'FbMiner',
143 'TwitterMiner']
144 self.prevText = ''
145 self.prevCursorPosition = 0
146 self.prevTextLen = 0
147 self.dbPath = '[UNDEFINED]'
148 self.nGramSize = '[UNDEFINED]'
149 self.predsUsingDb = []
150 self.setupUi(self)
151 self.mainTabs.setCurrentIndex(1)
152 self.on_miner_selected(self.minersComboBox.currentIndex())
153
154 - def setupUi(self, MainWindow):
155 """Create and set the main window widgets and layouts."""
156 MainWindow.resize(640, 510)
157 MainWindow.setWindowTitle('Preditor')
158 self.centralwidget = QtWidgets.QWidget(MainWindow)
159 MainWindow.setCentralWidget(self.centralwidget)
160 self.mainTabs = QtWidgets.QTabWidget(self.centralwidget)
161 self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
162 self.gridLayout_7 = QtWidgets.QGridLayout()
163 self.gridLayout_8 = QtWidgets.QGridLayout()
164 self.gridLayout_3 = QtWidgets.QGridLayout()
165 self.horizontalLayout = QtWidgets.QHBoxLayout()
166
167 self.miningTab = QtWidgets.QWidget()
168 self.gridLayout_5 = QtWidgets.QGridLayout(self.miningTab)
169
170 self.dbStatsGroupBox = QtWidgets.QGroupBox(self.miningTab)
171 self.dbStatsGroupBox.setTitle('Miner\'s database statistics')
172 self.gridLayout_6 = QtWidgets.QGridLayout(self.dbStatsGroupBox)
173
174 self.dbPathLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
175 self.dbPathLabel.setText('Path:')
176
177 self.dbPathValueLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
178 self.dbPathValueLabel.setText('-')
179
180 self.ngramsSizeLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
181 self.ngramsSizeLabel.setText('N-grams size:')
182
183 self.ngramsSizeValueLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
184 self.ngramsSizeValueLabel.setText('-')
185
186 self.noNgramsLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
187 self.noNgramsLabel.setText('Number of n-grams:')
188
189 self.ngramsTable = QtWidgets.QTableWidget(self.dbStatsGroupBox)
190 self.ngramsTable.setColumnCount(2)
191 self.ngramsTable.setHorizontalHeaderItem(
192 0, QtWidgets.QTableWidgetItem())
193 self.ngramsTable.setHorizontalHeaderItem(
194 1, QtWidgets.QTableWidgetItem())
195 self.ngramsTable.setSortingEnabled(True)
196 self.ngramsTable.horizontalHeaderItem(0).setText('N-gram size')
197 self.ngramsTable.horizontalHeaderItem(1).setText('Number of n-grams')
198
199 self.predsUsingDbLabel = QtWidgets.QLabel(self.dbStatsGroupBox)
200 self.predsUsingDbLabel.setText('Predictors using the database:')
201
202 self.predsUsingDbList = QtWidgets.QListWidget(self.dbStatsGroupBox)
203
204 self.miningGroupBox = QtWidgets.QGroupBox(self.miningTab)
205 self.miningGroupBox.setTitle('Mining operations')
206 self.gridLayout_2 = QtWidgets.QGridLayout(self.miningGroupBox)
207
208 self.minersComboBox = QtWidgets.QComboBox(self.miningGroupBox)
209 for miner in self.miners:
210 self.minersComboBox.addItem(miner)
211 self.minersComboBox.currentIndexChanged.connect(self.on_miner_selected)
212
213 self.minerOperationLabel = QtWidgets.QLabel(self.miningGroupBox)
214 self.minerOperationLabel.setText('Operation:')
215
216 self.minerOperationValueLabel = QtWidgets.QLabel(self.miningGroupBox)
217 self.minerOperationValueLabel.setText('-')
218
219 self.progressLabel = QtWidgets.QLabel(self.miningGroupBox)
220 self.progressLabel.setText('Progression:')
221
222 self.progressBar = QtWidgets.QProgressBar(self.miningGroupBox)
223 self.progressBar.setProperty("value", 0)
224 self.progressBar.setAlignment(QtCore.Qt.AlignCenter)
225 self.progressBar.setTextVisible(True)
226 self.progressBar.setInvertedAppearance(False)
227 self.progressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom)
228 self.progressBar.setRange(0, 100)
229
230 self.deleteDbBtn = QtWidgets.QPushButton(self.miningGroupBox)
231 self.deleteDbBtn.setText('Delete database file')
232 self.deleteDbBtn.released.connect(self.on_rm_db_btn_released)
233
234 self.mineBtn = QtWidgets.QPushButton(self.miningGroupBox)
235 self.mineBtn.setText('MINE !')
236 self.mineBtn.released.connect(self.on_mine_btn_released)
237
238 self.predictingTab = QtWidgets.QWidget()
239 self.gridLayout_4 = QtWidgets.QGridLayout(self.predictingTab)
240 self.inputTextEdit = QtWidgets.QPlainTextEdit(self.predictingTab)
241
242 self.inputTextEdit.cursorPositionChanged.connect(
243 self.on_input_text_cursor_position_change)
244 self.suggestionsList = QtWidgets.QListWidget(self.predictingTab)
245 self.suggestionsList.itemDoubleClicked.connect(
246 self.on_suggestion_double_clicked)
247
248 self.mainTabs.addTab(self.miningTab, '')
249 self.mainTabs.addTab(self.predictingTab, '')
250 self.mainTabs.setTabText(
251 self.mainTabs.indexOf(self.miningTab), 'Mining')
252 self.mainTabs.setTabText(
253 self.mainTabs.indexOf(self.predictingTab), 'Predicting')
254
255 self.horizontalLayout.addWidget(self.deleteDbBtn)
256 self.horizontalLayout.addWidget(self.mineBtn)
257 self.gridLayout_2.addWidget(self.minersComboBox, 0, 0, 1, 1)
258 self.gridLayout_3.addWidget(self.minerOperationLabel, 0, 0, 1, 1)
259 self.gridLayout_3.addWidget(self.progressLabel, 1, 0, 1, 1)
260 self.gridLayout_3.addWidget(self.progressBar, 1, 1, 1, 1)
261 self.gridLayout_3.addWidget(self.minerOperationValueLabel, 0, 1, 1, 1)
262 self.gridLayout_2.addLayout(self.gridLayout_3, 4, 0, 1, 1)
263 self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
264 self.gridLayout_3.addWidget(self.minerOperationValueLabel, 0, 1, 1, 1)
265 self.gridLayout_5.addWidget(self.miningGroupBox, 0, 0, 1, 1)
266 self.gridLayout_4.addWidget(self.inputTextEdit, 0, 0, 1, 1)
267 self.gridLayout.addWidget(self.mainTabs, 0, 0, 1, 1)
268 self.gridLayout_6.addWidget(self.noNgramsLabel, 6, 0, 1, 1)
269 self.gridLayout_7.addWidget(self.dbPathLabel, 0, 0, 1, 1)
270 self.gridLayout_7.addWidget(self.ngramsSizeLabel, 1, 0, 1, 1)
271 self.gridLayout_7.addWidget(self.dbPathValueLabel, 0, 1, 1, 1)
272 self.gridLayout_7.addWidget(self.ngramsSizeValueLabel, 1, 1, 1, 1)
273 self.gridLayout_6.addLayout(self.gridLayout_7, 0, 0, 1, 1)
274 self.gridLayout_6.addWidget(self.predsUsingDbLabel, 1, 0, 1, 1)
275 self.gridLayout_6.addLayout(self.gridLayout_8, 2, 0, 1, 1)
276 self.gridLayout_6.addWidget(self.ngramsTable, 7, 0, 1, 1)
277 self.gridLayout_5.addWidget(self.dbStatsGroupBox, 3, 0, 1, 1)
278 self.gridLayout_6.addWidget(self.predsUsingDbList, 2, 0, 1, 1)
279 self.gridLayout_5.addWidget(self.dbStatsGroupBox, 3, 0, 1, 1)
280 self.gridLayout_4.addWidget(self.suggestionsList, 1, 0, 1, 1)
281
282
283 self.menubar = QtWidgets.QMenuBar(MainWindow)
284 self.menubar.setGeometry(QtCore.QRect(0, 0, 640, 18))
285
286
287 self.menuFile = QtWidgets.QMenu(self.menubar)
288 self.menuFile.setTitle('File')
289
290 self.menuEdition = QtWidgets.QMenu(self.menubar)
291 self.menuEdition.setTitle('Edition')
292
293 self.menuView = QtWidgets.QMenu(self.menubar)
294 self.menuView.setTitle('View')
295
296
297 self.actionOpen = QtWidgets.QAction(MainWindow)
298 self.actionOpen.setText('Import some text...')
299
300 self.actionQuit = QtWidgets.QAction(MainWindow)
301 self.actionQuit.setText('Quit')
302 self.actionQuit.setShortcut('Ctrl+Q')
303 self.actionQuit.triggered.connect(
304 QtCore.QCoreApplication.instance().quit)
305
306 self.actionPaste = QtWidgets.QAction(MainWindow)
307 self.actionPaste.setText('Paste')
308 self.actionPaste.setShortcut('Ctrl+V')
309 self.actionPaste.triggered.connect(self.on_paste_btn_triggered)
310
311 self.actionCopy = QtWidgets.QAction(MainWindow)
312 self.actionCopy.setText('Copy')
313 self.actionCopy.setShortcut('Ctrl+C')
314 self.actionCopy.triggered.connect(self.on_copy_btn_triggered)
315
316 self.actionCut = QtWidgets.QAction(MainWindow)
317 self.actionCut.setText('Cut')
318 self.actionCut.setShortcut('Ctrl+X')
319 self.actionCut.triggered.connect(self.on_cut_btn_triggered)
320
321 self.actionSettings = QtWidgets.QAction(MainWindow)
322 self.actionSettings.setText('Preferences')
323 self.actionSettings.setShortcut('Ctrl+Alt+P')
324 self.actionSettings.triggered.connect(self.on_pref_triggered)
325
326 self.actionShowProbabilities = QtWidgets.QAction(MainWindow)
327 self.actionShowProbabilities.setText('Show probabilities')
328 self.actionShowProbabilities.triggered.connect(
329 self.on_show_probabilities_triggered)
330 self.actionShowProbabilities.setCheckable(True)
331
332
333 self.menuFile.addAction(self.actionOpen)
334 self.menuFile.addSeparator()
335 self.menuFile.addAction(self.actionQuit)
336
337 self.menuEdition.addAction(self.actionCut)
338 self.menuEdition.addAction(self.actionCopy)
339 self.menuEdition.addAction(self.actionPaste)
340 self.menuEdition.addSeparator()
341 self.menuEdition.addAction(self.actionSettings)
342
343 self.menuView.addAction(self.actionShowProbabilities)
344
345 self.menubar.addAction(self.menuFile.menuAction())
346 self.menubar.addAction(self.menuEdition.menuAction())
347 self.menubar.addAction(self.menuView.menuAction())
348
349 MainWindow.setMenuBar(self.menubar)
350
351
352 self.statusbar = QtWidgets.QStatusBar(MainWindow)
353
354 MainWindow.setStatusBar(self.statusbar)
355
356
357 self.logDock = LogDockWidget(self.config)
358 self.logDock.setColor('white', 'black')
359
360 MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(8), self.logDock)
361 QtCore.QMetaObject.connectSlotsByName(MainWindow)
362
363 lg.info('Session starts.')
364
365 - def on_paste_btn_triggered(self, checked=False):
366 """Paste what's inside the clipboard."""
367 self.inputTextEdit.paste()
368
369 - def on_copy_btn_triggered(self, checked=False):
370 """Copy the selection."""
371 self.inputTextEdit.copy()
372
373 - def on_cut_btn_triggered(self, checked=False):
374 """Cut the selection."""
375 self.inputTextEdit.cut()
376
378 """Show the suggested words probabilities."""
379 lg.warning('Not yet implemented.')
380
381 - def on_pref_triggered(self, checked=False):
382 """Open the settings dialog."""
383 settingsDialog = stngs.Settings_UI(self.config)
384 settingsDialog.exec_()
385 self.write_config()
386
387 - def write_config(self):
388 """Write the current configuration in the configuration file."""
389 if self.driver.configFile:
390 with open(self.driver.configFile, 'w') as f:
391 self.config.write(f)
392
394 """Create a list of every predictors name which use a given database.
395
396 @return:
397 The list of the predictors using the database file 'self.dbPath'.
398 @rtype: list
399 """
400 predsUsingDb = []
401 for pred in self.predictors:
402 try:
403 if self.config[pred]['database'] == self.dbPath:
404 predsUsingDb.append(pred)
405 except KeyError:
406 continue
407 return predsUsingDb
408
409 - def on_miner_selected(self, miner):
410 """Callback called when the user select a miner in the combo box.
411
412 When a miner is selected in the miner selection combo box some settings
413 of the miner have to be retrieved from the configuration in order to
414 display them to the user using the set_miner_widgets_values() method.
415 As every miners store their results (n-grams) in a database the database
416 path and the n-gram size are retrieved from the configuration and stored
417 into the "dbPath" and "nGramSize" instance variables.
418
419 @param miner:
420 The index of the miner in the combo box.
421 @type miner: int
422 """
423 if miner == self.TEXT_FILE_MINER:
424 self.dbPath = self.config.getas('CorpusMiner', 'database')
425 self.nGramSize = self.config.getas('CorpusMiner', 'n')
426 self.predsUsingDb = self.predictors_using_db()
427 elif miner == self.DICTIONARY_MINER:
428 self.dbPath = self.config.getas('DictMiner', 'database')
429 self.nGramSize = '1'
430 self.predsUsingDb = self.predictors_using_db()
431 elif miner == self.FACEBOOK_MINER:
432 self.dbPath = self.config.getas('FbMiner', 'database')
433 self.nGramSize = self.config.getas('FbMiner', 'n')
434 self.predsUsingDb = self.predictors_using_db()
435 elif miner == self.TWITTER_MINER:
436 self.dbPath = '[UNDEFINED]'
437 self.nGramSize = '[UNDEFINED]'
438 else:
439 self.dbPath = '[UNDEFINED]'
440 self.nGramSize = '[UNDEFINED]'
441 self.set_miner_widgets_values()
442
444 """Update the widgets (labels and list) displaying infos on the miner.
445
446 When a miner is selected in the miner selection combo box, its config
447 settings are retrieve from the configuration and the instance variables
448 "dbPath", "nGramSize" and "predsUsingDb" are set.
449 This method modify the value of the widgets displaying the informations
450 about the miner with the above-mentioned variables.
451 """
452 nGramCount = []
453 self.dbPathValueLabel.setText(self.dbPath)
454 self.ngramsSizeValueLabel.setText(self.nGramSize)
455 while self.predsUsingDbList.count():
456 self.predsUsingDbList.takeItem(0)
457 for pred in self.predictors:
458 if pred in self.predsUsingDb:
459 self.predsUsingDbList.addItem(pred)
460 if self.nGramSize == '[UNDEFINED]' or self.dbPath == '[UNDEFINED]':
461 self.ngramsTable.setRowCount(0)
462 return
463 database = SqliteDatabaseConnector(self.dbPath, self.nGramSize)
464 for n in range(1, int(self.nGramSize) + 1):
465 try:
466 nGramCount.append(database.ngrams_in_table(n))
467 except OperationalError:
468 nGramCount.append(0)
469 database.close_database()
470 self.ngramsTable.setRowCount(n)
471 for i, count in enumerate(nGramCount):
472 self.ngramsTable.setItem(
473 i, 0, QtWidgets.QTableWidgetItem(str(i + 1)))
474 self.ngramsTable.setItem(
475 i, 1, QtWidgets.QTableWidgetItem(str(count)))
476
477 - def progress_callback(self, perc=0, text=None):
478 """Update the label showing the mining operation and the progress bar.
479
480 This method is a callback which is called from the miners. It update the
481 text of the label displaying the minig operation and the value of the
482 progress bar displaying the operation progression.
483
484 @param perc:
485 Computed by the miners calling the callback, it indicate the
486 operation progression and is usually a float but it must be
487 converted to int as the progress bar only show integers.
488 @type perc: float or int
489 @param text:
490 The text to display in the mining operation label.
491 @type text: str
492 """
493 if text:
494 self.minerOperationValueLabel.setText(text)
495 self.minerOperationValueLabel.repaint()
496 self.progressBar.setValue(round(perc))
497
499 """Callback called when the user press the "Delete database" button.
500
501 The method first identify the current miner selected in the combo box
502 using its index then create an instance of this miner and called its
503 rm_db() method to effectively carry out the database suppression
504 operation.
505 Some miners modify the configuration so the config file needs to be
506 rewrite after the operation.
507 Here is a short description of the miners database suppression
508 operation, please refer to their rm_db() method docstring for more
509 informations:
510 - CorpusMiner: Remove the database file.
511 - FbMiner: Remove the database file and set the "last_update" config
512 option to the minimum value so the config have to be writen
513 afterward.
514 """
515 if self.minersComboBox.currentIndex() == self.TEXT_FILE_MINER:
516 miner = CorpusMiner(self.config, 'CorpusMiner')
517 miner.rm_db()
518 elif self.minersComboBox.currentIndex() == self.DICTIONARY_MINER:
519 miner = DictMiner(self.config, 'DictMiner')
520 miner.rm_db()
521 elif self.minersComboBox.currentIndex() == self.FACEBOOK_MINER:
522 miner = FacebookMiner(self.config, 'FbMiner')
523 miner.rm_db()
524 self.write_config()
525 elif self.minersComboBox.currentIndex() == self.TWITTER_MINER:
526 self.progress_callback(0, 'error: [MINER NOT IMPLEMENTED YET]')
527 lg.error('Miner not implemented yet')
528 return
529 else:
530 self.progress_callback(0, 'error: unknown miner')
531 lg.error('Unknown miner "{0}"'.format(
532 self.minersComboBox.currentIndex()))
533 return
534 self.set_miner_widgets_values()
535
537 """Callback called when the user press the "MINE!" button.
538
539 The method first identify the current miner selected in the combo box
540 using its index then create an instance of this miner and called its
541 mine() method to effectively carry out the mining operation.
542 Some miners modify the configuration so the config file needs to be
543 rewrite after the operation.
544 Here is a short description of the miners mining operation, please refer
545 to their mine() method docstring for more informations:
546 - CorpusMiner: Mine a text corpus (i.e. a set of text files) by
547 extracting n-grams from the files and inserting them into a
548 database.
549 - FbMiner: Mine a facebook user wall (only text messages from
550 posts) by extracting n-grams from the posts and inserting them
551 into a database. This miner modifies the "last_update" option of
552 its config section so the config have to be writen afterward.
553 - TwiterMiner: Not implemented yet but it will be very similar to
554 FbMiner.
555 """
556 if self.minersComboBox.currentIndex() == self.TEXT_FILE_MINER:
557 miner = CorpusMiner(
558 self.config, 'CorpusMiner', self.progress_callback)
559 miner.mine()
560 elif self.minersComboBox.currentIndex() == self.DICTIONARY_MINER:
561 miner = DictMiner(self.config, 'DictMiner', self.progress_callback)
562 miner.mine()
563 elif self.minersComboBox.currentIndex() == self.FACEBOOK_MINER:
564 miner = FacebookMiner(
565 self.config, 'FbMiner', self.progress_callback)
566 miner.mine()
567 self.write_config()
568 elif self.minersComboBox.currentIndex() == self.TWITTER_MINER:
569 self.progress_callback(0, 'error: [MINER NOT IMPLEMENTED YET]')
570 lg.warning('Miner not implemented yet')
571 return
572 else:
573 self.progress_callback(0, 'error: unknown miner')
574 lg.warning('Unknown miner')
575 return
576 self.set_miner_widgets_values()
577
579 """Update the input buffers and compute the suggestion.
580
581 This method is called whenever one or more character(s) is/are added to
582 or removed from the input text. It update the input left and right
583 buffers according to the text change and then generate the suggestions
584 for the new input context.
585 There is three king of input text change:
586 - Characters have been appened:
587 -> Characters are added to the left input buffer (via
588 callback.update()).
589 - Part of the characters have been removed:
590 -> Characters are removed to the left input buffer (via
591 callback.update()). It simulates a backspace input.
592 - Every characters have been removed (while new ones have been
593 added):
594 -> Every characters of the left input buffer are removed (via
595 callback.update()). New characters, if any, are added to the left
596 input buffer (via callback.update()).
597 @note: It correspond to a Ctrl+A then <some printable characters> or
598 <backspace>.
599 """
600 text = self.inputTextEdit.toPlainText()
601 if text.startswith(self.prevText):
602 change = text[len(self.prevText):]
603 self.driver.callback.update(change)
604 elif self.prevText.startswith(text):
605 self.driver.callback.update('\b', len(self.prevText) - len(text))
606 else:
607 self.driver.callback.update('\b', len(self.prevText))
608 self.driver.callback.update(text)
609 self.prevText = text
610 self.make_suggestions()
611
613 """Compute and show suggestions.
614
615 Request the PredictorActivator to compute the suggestions and show them
616 in the suggestion list.
617 """
618 self.suggestionsList.clear()
619 for p in self.driver.predict():
620 self.suggestionsList.addItem(p)
621
623 """Callback called when the input text cursor position change.
624
625 There's three possible moves types:
626 - The cursor moved because one or more character(s) have been added
627 to or removed from the input text:
628 -> The cursor position and the input text length are saved and the
629 suggestions are updated according to the current input.
630 - The cursor moved because the user pressed the left arrow key or
631 clicked somewhere in the text, on the left of the previous cursor
632 position:
633 -> In this case the input text has not change we call the Driver
634 callback to modify the input left and right buffer so that the
635 left buffer now contains what's on the left of the cursor and the
636 right buffer now contains what's on the right of the cursor. The
637 suggestions are then updated according to the current cursor
638 position.
639 - The cursor moved because the user pressed the right arrow key or
640 clicked somewhere in the text, on the right of the previous cursor
641 position:
642 -> In this case the input text has not change we call the Driver
643 callback to modify the input left and right buffer so that the
644 left buffer now contains what's on the left of the cursor and the
645 right buffer now contains what's on the right of the cursor. The
646 suggestions are then updated according to the current cursor
647 position.
648
649 @note: The on_input_text_change() method used to be connected to the
650 textChanged signal of the input text widget but it is redundant
651 because if the text change, the cursor position automatically change
652 too. It save some operations.
653
654 @bug:
655 When selecting characters in the input field (self.inputTextEdit)
656 the cursor position is modified and induce erroneous context changes
657 and word predictions.
658 """
659 currentCursorPosition = self.inputTextEdit.textCursor().position()
660 currentTextLen = len(self.inputTextEdit.toPlainText())
661 posDiff = currentCursorPosition - self.prevCursorPosition
662 lenDiff = currentTextLen - self.prevTextLen
663 if lenDiff == posDiff:
664 self.on_input_text_change()
665 elif posDiff < 0 and lenDiff == 0:
666 self.driver.callback.update('\x1B[D', posDiff)
667 elif posDiff > 0 and lenDiff == 0:
668 self.driver.callback.update('\x1B[C', posDiff)
669 else:
670 self.prevCursorPosition = currentCursorPosition
671 self.prevTextLen = currentTextLen
672 return
673 self.prevCursorPosition = currentCursorPosition
674 self.prevTextLen = currentTextLen
675 self.make_suggestions()
676
678 """Complete the input with the selected suggestion.
679
680 When the user double click on a word of the list the word is used to
681 complete the input text. There is two kind of completion depending on
682 the input cursor position:
683 - The cursor is at the end of the text:
684 -> The completion for the last token is computed and append to the
685 input text. A space is then added allowing the user to save a key
686 and the program to immediatly suggest the next word prediction.
687 - The cursor is not at the end of the text:
688 -> The completion for the token in which the cursor is is computed
689 and the end of the token is replaced by the completion.
690
691 During this method the input text widget signals are disconnected which
692 means that no suggestions will be compute during the completion process.
693 This is important because this method will modify the input text but, of
694 course, there is no need to generate any suggestions as the user isn't
695 typing anything during the process.
696
697 @note: A token can be a word or a separator. If it is a word then the
698 completion represents the end of the word. If it is a separator
699 then the completion represents the next word.
700
701 @param item:
702 The selected item (word) in the suggestion list.
703 @type item:
704 QtWidgets.QListWidgetItem
705 """
706 self.inputTextEdit.cursorPositionChanged.disconnect()
707 currentCursorPosition = self.inputTextEdit.textCursor().position()
708 if currentCursorPosition == len(self.inputTextEdit.toPlainText()):
709 completion = self.driver.make_completion(item.text())
710
711
712
713 if not completion is False:
714 self.inputTextEdit.insertPlainText(completion)
715 self.driver.callback.update(completion)
716 self.prevCursorPosition = \
717 self.inputTextEdit.textCursor().position()
718 self.prevTextLen = len(self.inputTextEdit.toPlainText())
719 self.prevText = self.inputTextEdit.toPlainText()
720 self.inputTextEdit.cursorPositionChanged.connect(
721 self.on_input_text_cursor_position_change)
722 self.inputTextEdit.insertPlainText(' ')
723 else:
724 completion = self.driver.make_completion(item.text())
725 if completion:
726 suffix = self.driver.contextMonitor.suffix()
727 tmpCursor = self.inputTextEdit.textCursor()
728 tmpCursor.movePosition(
729 QtQTextCursor.Right,
730 QtQTextCursor.MoveAnchor, len(suffix))
731 self.inputTextEdit.setTextCursor(tmpCursor)
732 self.driver.callback.update('\x1B[C', len(suffix))
733 for i in range(len(suffix)):
734 self.inputTextEdit.textCursor().deletePreviousChar()
735 self.driver.callback.update('\b', 1)
736 self.inputTextEdit.insertPlainText(completion)
737 self.driver.callback.update(completion)
738 self.prevCursorPosition = \
739 self.inputTextEdit.textCursor().position()
740 self.prevTextLen = len(self.inputTextEdit.toPlainText())
741 self.prevText = self.inputTextEdit.toPlainText()
742 self.inputTextEdit.cursorPositionChanged.connect(
743 self.on_input_text_cursor_position_change)
744