Package tipy :: Module gui
[hide private]
[frames] | no frames]

Source Code for Module tipy.gui

  1  #!/usr/bin/env python3 
  2  # -*- coding: utf-8 -*- 
  3   
  4  # The following can appear on program shutdown: 
  5  # QBasicTimer::start: QBasicTimer can only be used with threads started with 
  6  # QThread 
  7  # There is nothing to worry about it is due to Python GC deleting Qt C++ objects 
  8  # in the wrong order. 
  9   
 10  # If you have any poblem with the ui such as ugly characters font or missing 
 11  # menu bar try unseting the environnement variable with : 
 12  # $ QT_QPA_PLATFORMTHEME= 
 13  # It default value is something like: 
 14  # QT_QPA_PLATFORMTHEME=appmenu-qt5 
 15  # I can't do much to solve these bugs, it's on Qt side. 
 16  # See: https://bugs.launchpad.net/ubuntu/+source/appmenu-qt5/+bug/1307619 
 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
78 -class LogDockWidget(QtWidgets.QDockWidget):
79 """A main window dockable widget showing the app logs.""" 80
81 - def __init__(self, config):
82 super(LogDockWidget, self).__init__('log') 83 self.setWidget(LoggerTextEdit(config)) 84 self.handler = StreamHandler(self.widget()) 85 self.handler.setLevel(DEBUG) 86 self.handler.setFormatter(CF) 87 lg.addHandler(self.handler)
88
89 - def setColor(self, fg, bg):
90 """Set the background color of the log window.""" 91 self.widget().setStyleSheet("color: %s;background-color: %s" % (fg, bg))
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 ############################## MINING TAB ############################## 167 self.miningTab = QtWidgets.QWidget() 168 self.gridLayout_5 = QtWidgets.QGridLayout(self.miningTab) 169 # =================== DATABASE STATISTICS GROUP BOX ================== # 170 self.dbStatsGroupBox = QtWidgets.QGroupBox(self.miningTab) 171 self.dbStatsGroupBox.setTitle('Miner\'s database statistics') 172 self.gridLayout_6 = QtWidgets.QGridLayout(self.dbStatsGroupBox) 173 # Path label 174 self.dbPathLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 175 self.dbPathLabel.setText('Path:') 176 # Path value label 177 self.dbPathValueLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 178 self.dbPathValueLabel.setText('-') 179 # n-grams size label 180 self.ngramsSizeLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 181 self.ngramsSizeLabel.setText('N-grams size:') 182 # n-grams size value label 183 self.ngramsSizeValueLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 184 self.ngramsSizeValueLabel.setText('-') 185 # Number of n-grams label 186 self.noNgramsLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 187 self.noNgramsLabel.setText('Number of n-grams:') 188 # Table associating n-grams size value and n-grams count 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 # Predictors using the miner database label 199 self.predsUsingDbLabel = QtWidgets.QLabel(self.dbStatsGroupBox) 200 self.predsUsingDbLabel.setText('Predictors using the database:') 201 # List of predictors using the miner database as input 202 self.predsUsingDbList = QtWidgets.QListWidget(self.dbStatsGroupBox) 203 # ==================== MINING OPERATIONS GROUP BOX =================== # 204 self.miningGroupBox = QtWidgets.QGroupBox(self.miningTab) 205 self.miningGroupBox.setTitle('Mining operations') 206 self.gridLayout_2 = QtWidgets.QGridLayout(self.miningGroupBox) 207 # A comboBox to select between available miners 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 # Operation status label 213 self.minerOperationLabel = QtWidgets.QLabel(self.miningGroupBox) 214 self.minerOperationLabel.setText('Operation:') 215 # Operation status value label 216 self.minerOperationValueLabel = QtWidgets.QLabel(self.miningGroupBox) 217 self.minerOperationValueLabel.setText('-') 218 # Operation progression label 219 self.progressLabel = QtWidgets.QLabel(self.miningGroupBox) 220 self.progressLabel.setText('Progression:') 221 # Operation progress bar 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 # Delete database file button 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 # Run mining operation button 234 self.mineBtn = QtWidgets.QPushButton(self.miningGroupBox) 235 self.mineBtn.setText('MINE !') 236 self.mineBtn.released.connect(self.on_mine_btn_released) 237 ############################ PREDICTING TAB ############################ 238 self.predictingTab = QtWidgets.QWidget() 239 self.gridLayout_4 = QtWidgets.QGridLayout(self.predictingTab) 240 self.inputTextEdit = QtWidgets.QPlainTextEdit(self.predictingTab) 241 #~ self.inputTextEdit.textChanged.connect(self.on_input_text_change) 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 ######################### TABS CONTENT AND NAME ######################## 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 ###################### ADD EVERYTHING TO LAYOUTS ####################### 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 ################################ MENUBAR ############################### 282 # The menubar 283 self.menubar = QtWidgets.QMenuBar(MainWindow) 284 self.menubar.setGeometry(QtCore.QRect(0, 0, 640, 18)) 285 # ============================= THE MENUS ============================ # 286 # The File menu 287 self.menuFile = QtWidgets.QMenu(self.menubar) 288 self.menuFile.setTitle('File') 289 # The Edition menu 290 self.menuEdition = QtWidgets.QMenu(self.menubar) 291 self.menuEdition.setTitle('Edition') 292 # The View menu 293 self.menuView = QtWidgets.QMenu(self.menubar) 294 self.menuView.setTitle('View') 295 # =========================== THE ACTIONS ============================ # 296 # The Open action 297 self.actionOpen = QtWidgets.QAction(MainWindow) 298 self.actionOpen.setText('Import some text...') 299 # The Quit action 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 # The Paste action 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 # The Copy action 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 # The Cut action 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 # The Settings action 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 # The Afficher les probabilités action 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 # ===================== PUTTING IT ALL TOGETHER ====================== # 332 # Add the actions to the File menu 333 self.menuFile.addAction(self.actionOpen) 334 self.menuFile.addSeparator() 335 self.menuFile.addAction(self.actionQuit) 336 # Add the actions to the Edition menu 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 # Add the actions to the View menu 343 self.menuView.addAction(self.actionShowProbabilities) 344 # Add the menus to the menubar 345 self.menubar.addAction(self.menuFile.menuAction()) 346 self.menubar.addAction(self.menuEdition.menuAction()) 347 self.menubar.addAction(self.menuView.menuAction()) 348 # Set the menubar 349 MainWindow.setMenuBar(self.menubar) 350 ############################### STATUSBAR ############################## 351 # The statusbar 352 self.statusbar = QtWidgets.QStatusBar(MainWindow) 353 # Set the statusbar 354 MainWindow.setStatusBar(self.statusbar) 355 ############################ LOG DOCK WIDGET ########################### 356 # The dock widget for the log 357 self.logDock = LogDockWidget(self.config) 358 self.logDock.setColor('white', 'black') 359 # Set the dock 360 MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(8), self.logDock) 361 QtCore.QMetaObject.connectSlotsByName(MainWindow) 362 # Show something 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
377 - def on_show_probabilities_triggered(self, checked=False):
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
393 - def predictors_using_db(self):
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
443 - def set_miner_widgets_values(self):
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
498 - def on_rm_db_btn_released(self):
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: # should never happen 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
536 - def on_mine_btn_released(self):
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: # should never happen 573 self.progress_callback(0, 'error: unknown miner') 574 lg.warning('Unknown miner') 575 return 576 self.set_miner_widgets_values()
577
578 - def on_input_text_change(self):
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
612 - def make_suggestions(self):
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: # Char(s) have been added or removed 664 self.on_input_text_change() 665 elif posDiff < 0 and lenDiff == 0: # Move cursor to the left 666 self.driver.callback.update('\x1B[D', posDiff) 667 elif posDiff > 0 and lenDiff == 0: # Move cursor to the right 668 self.driver.callback.update('\x1B[C', posDiff) 669 else: # Should never happen 670 self.prevCursorPosition = currentCursorPosition 671 self.prevTextLen = currentTextLen 672 return 673 self.prevCursorPosition = currentCursorPosition 674 self.prevTextLen = currentTextLen 675 self.make_suggestions()
676
677 - def on_suggestion_double_clicked(self, item):
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 # If the word is finished then suggestion = word and completion = '' 711 # But we still have to add a space after the word so check if 712 # completion is not False 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