instances.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  1. ########################################################################
  2. # Searx-qt - Lightweight desktop application for SearX.
  3. # Copyright (C) 2020 CYBERDEViL
  4. #
  5. # This file is part of Searx-qt.
  6. #
  7. # Searx-qt is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Searx-qt is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from PyQt5.QtWidgets import (
  22. QWidget,
  23. QVBoxLayout,
  24. QLabel,
  25. QTableView,
  26. QHeaderView,
  27. QAbstractItemView,
  28. QHBoxLayout,
  29. QFormLayout,
  30. QCheckBox,
  31. QMenu,
  32. QComboBox,
  33. QSizePolicy,
  34. QScrollArea,
  35. QSplitter,
  36. QFrame,
  37. QWidgetAction
  38. )
  39. from PyQt5.QtCore import pyqtSignal, Qt
  40. from PyQt5.QtWidgets import QMessageBox
  41. from PyQt5.QtGui import QGuiApplication
  42. from searxqt.core.handler import NetworkTypes # for network type filtering.
  43. from searxqt.widgets.buttons import Button
  44. from searxqt.widgets.dialogs import UrlDialog
  45. from searxqt.models.instances import (
  46. InstanceTableModel,
  47. InstancesModelTypes
  48. )
  49. from searxqt.translations import _, timeToString
  50. class AddUserInstanceDialog(UrlDialog):
  51. def __init__(self, url='', parent=None):
  52. UrlDialog.__init__(self, url=url, acceptTxt=_("Add"), parent=parent)
  53. layout = self.layout()
  54. label = QLabel("Update data on add:")
  55. self._updateCheckBox = QCheckBox("", self)
  56. layout.insertRow(2, label, self._updateCheckBox)
  57. def updateOnAdd(self):
  58. return bool(self._updateCheckBox.isChecked())
  59. class InstancesView(QWidget):
  60. def __init__(self, filterModel, instanceSelecter, parent=None):
  61. """
  62. @type model: searxqt.models.instances.InstanceModelFilter
  63. @type instanceSelecter: searxqt.models.instances.InstanceSelecterModel
  64. @type parent: QObject
  65. """
  66. QWidget.__init__(self, parent=parent)
  67. layout = QVBoxLayout(self)
  68. layout.setContentsMargins(0, 0, 0, 0)
  69. layout.setSpacing(0)
  70. self._filterModel = filterModel
  71. self._instanceSelecter = instanceSelecter
  72. self._tableModel = InstanceTableModel(filterModel, self)
  73. # Diffrent types can have diffrent attibutes.
  74. self._currentInstancesType = filterModel.parentModel().Type
  75. headLabel = QLabel("<h2>{0}</h2>".format(_("Instances")), self)
  76. layout.addWidget(headLabel)
  77. # Splitter
  78. self._splitter = QSplitter(self)
  79. self._splitter.setOrientation(Qt.Vertical)
  80. layout.addWidget(self._splitter)
  81. # Filter scroll area
  82. self._scrollArea = QScrollArea(self._splitter)
  83. self._scrollContentWidget = QWidget(self._scrollArea)
  84. scrollLayout = QVBoxLayout(self._scrollContentWidget)
  85. self.filterWidget = FilterWidget(
  86. filterModel,
  87. self._scrollContentWidget
  88. )
  89. self._scrollArea.setWidget(self._scrollContentWidget)
  90. self._scrollArea.setFrameShape(QFrame.NoFrame)
  91. self._scrollArea.setWidgetResizable(True)
  92. self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  93. self._scrollArea.setAlignment(
  94. Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
  95. )
  96. scrollLayout.addWidget(self.filterWidget, 0, Qt.AlignTop)
  97. # Bottom widgets
  98. bottomWidget = QWidget(self._splitter)
  99. bottomLayout = QVBoxLayout(bottomWidget)
  100. self.statsWidget = InstancesStatsWidget(filterModel, bottomWidget)
  101. bottomLayout.addWidget(self.statsWidget)
  102. self._typeDependantButton = Button("", bottomWidget)
  103. bottomLayout.addWidget(self._typeDependantButton)
  104. self._tableView = InstanceTableView(
  105. filterModel,
  106. self._tableModel,
  107. self
  108. )
  109. bottomLayout.addWidget(self._tableView)
  110. # Connections
  111. self._tableView.selectionModel().selectionChanged.connect(
  112. self.__selectionChanged)
  113. self._tableModel.layoutChanged.connect(self.__tableModelLayoutChanged)
  114. self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)
  115. filterModel.parentModel().typeChanged.connect(
  116. self.__instancesModelTypeChanged
  117. )
  118. """ InstancesType dependant methods below
  119. """
  120. def __instancesModelTypeChanged(self, type_):
  121. """ PersistentInstancesModel emitted changed.
  122. """
  123. previousType = self._currentInstancesType
  124. # Disconnect signals
  125. if previousType == InstancesModelTypes.Stats2:
  126. self._typeDependantButton.clicked.disconnect(self.__updateClicked)
  127. elif previousType == InstancesModelTypes.User:
  128. self._typeDependantButton.clicked.disconnect(self.__addClicked)
  129. if type_ == InstancesModelTypes.Stats2:
  130. self._typeDependantButton.setText(_("Update"))
  131. self._typeDependantButton.clicked.connect(self.__updateClicked)
  132. elif type_ == InstancesModelTypes.User:
  133. self._typeDependantButton.setText(_("Add Instance"))
  134. self._typeDependantButton.clicked.connect(self.__addClicked)
  135. self._currentInstancesType = type_
  136. # InstancesModelTypes.Stats2
  137. def __updateClicked(self, checked):
  138. self._filterModel.parentModel().handler().updateInstances()
  139. # InstancesModelTypes.User
  140. def __addClicked(self, checked):
  141. dialog = AddUserInstanceDialog()
  142. if dialog.exec():
  143. handler = self._filterModel.parentModel().handler()
  144. handler.addInstance(dialog.url)
  145. if dialog.updateOnAdd():
  146. handler.updateInstance(dialog.url)
  147. """ -----------------------------------------------------------
  148. """
  149. def selectUrl(self, url):
  150. self._tableView.selectionModel().selectionChanged.disconnect(
  151. self.__selectionChanged)
  152. # Backup horizontal scroll value
  153. horScrollVal = self._tableView.horizontalScrollBar().value()
  154. if url in self._filterModel:
  155. rowIndex = self._tableModel.getByUrl(url)
  156. modelIndex = self._tableModel.index(rowIndex, 0)
  157. self._tableView.setCurrentIndex(modelIndex)
  158. self._tableView.scrollTo(
  159. modelIndex,
  160. QAbstractItemView.PositionAtCenter
  161. )
  162. else:
  163. # No URL set so clear the selection.
  164. self._tableView.clearSelection()
  165. # Restore horizontal scroll value
  166. self._tableView.horizontalScrollBar().setValue(horScrollVal)
  167. self._tableView.selectionModel().selectionChanged.connect(
  168. self.__selectionChanged)
  169. def __instanceChanged(self, url):
  170. """ Select and scroll to table item with passed url.
  171. @param url: Instance url to select and scroll to.
  172. @type url: str
  173. """
  174. if url in self._filterModel:
  175. rowIndex = self._tableModel.getByUrl(url)
  176. modelIndex = self._tableModel.index(rowIndex, 0)
  177. self._tableView.setCurrentIndex(modelIndex)
  178. self._tableView.scrollTo(
  179. modelIndex,
  180. QAbstractItemView.PositionAtCenter
  181. )
  182. def __tableModelLayoutChanged(self):
  183. """ Re-select the current URL when the layout changed.
  184. """
  185. self.selectUrl(self._instanceSelecter.currentUrl)
  186. def __selectionChanged(self):
  187. """ Let the InstanceSelecter know that the current instance has
  188. changed. (Selected by the user.)
  189. """
  190. self._instanceSelecter.instanceChanged.disconnect(
  191. self.__instanceChanged)
  192. selectionModel = self._tableView.selectionModel()
  193. if selectionModel.hasSelection():
  194. rows = selectionModel.selectedRows()
  195. selectedIndex = rows[0].row()
  196. self._instanceSelecter.currentUrl = (
  197. self._tableModel.getByIndex(selectedIndex)
  198. )
  199. else:
  200. self._instanceSelecter.currentUrl = ""
  201. self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)
  202. def __filterWidgetChanged(self):
  203. self._filterModel.updateKwargs(self.filterWidget.data())
  204. def saveSettings(self):
  205. header = self._tableView.horizontalHeader()
  206. data = {
  207. 'hiddenColumnIndexes': self._tableView.getHiddenColumnIndexes(),
  208. 'headerState': header.saveState()
  209. }
  210. return data
  211. def loadSettings(self, data):
  212. self._tableView.setHiddenColumnIndexes(
  213. data.get('hiddenColumnIndexes', [])
  214. )
  215. if 'headerState' in data:
  216. header = self._tableView.horizontalHeader()
  217. header.restoreState(data['headerState'])
  218. class InstanceTableView(QTableView):
  219. def __init__(self, filterModel, tableModel=None, parent=None):
  220. """
  221. @type filterModel: searxqt.models.instances.InstanceModelFilter
  222. @type tableModel: searxqt.models.instances.InstanceTableModel
  223. """
  224. QTableView.__init__(self, parent=parent)
  225. self._filterModel = filterModel
  226. if tableModel:
  227. self.setModel(tableModel)
  228. self.setAlternatingRowColors(True)
  229. self.setSelectionBehavior(QAbstractItemView.SelectRows)
  230. self.setEditTriggers(QAbstractItemView.NoEditTriggers)
  231. self.setSortingEnabled(True)
  232. self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
  233. # Horizontal header
  234. header = self.horizontalHeader()
  235. header.setSectionResizeMode(QHeaderView.ResizeToContents)
  236. header.setSectionsMovable(True)
  237. def _hideColumn(self, index, state):
  238. state = state == 0
  239. self.setColumnHidden(index, state)
  240. def getHiddenColumnIndexes(self):
  241. columnIndex = 0
  242. indexes = []
  243. for columnData in self.model().getColumns():
  244. # Set current value
  245. if self.isColumnHidden(columnIndex):
  246. indexes.append(columnIndex)
  247. columnIndex += 1
  248. return indexes
  249. def setHiddenColumnIndexes(self, indexes):
  250. for index in indexes:
  251. self._hideColumn(index, True)
  252. """ Re-implementations
  253. """
  254. def contextMenuEvent(self, event):
  255. def __removeFromWhitelist(self, url):
  256. filter_ = self._filterModel.filter()
  257. filter_['url'].remove(url)
  258. self._filterModel.updateKwargs(
  259. {'url': filter_['url']},
  260. commit=False
  261. )
  262. selectionModel = self.selectionModel()
  263. if selectionModel.hasSelection():
  264. menu = QMenu(self)
  265. # Add to blacklist
  266. addToBlacklistAction = menu.addAction(_("Add to blacklist"))
  267. # Add to temporary blacklist
  268. addToTmpBlacklistAction = menu.addAction(_("Temporary blacklist"))
  269. # Add to whitelist
  270. addToWhitelistAction = menu.addAction(_("Add to whitelist"))
  271. # Copy URL
  272. copyUrlAction = menu.addAction(_("Copy URL"))
  273. menu.addSeparator()
  274. # Select all
  275. selectAllAction = menu.addAction(_("Select All"))
  276. menu.addSeparator()
  277. # Column options
  278. columnsMenu = QMenu(_("Columns"), self)
  279. menu.addMenu(columnsMenu)
  280. # User instances specific
  281. userInstances = False
  282. handler = self._filterModel.parentModel().handler()
  283. if (
  284. self._filterModel.parentModel().Type
  285. == InstancesModelTypes.User
  286. ):
  287. userInstances = True
  288. menu.addSeparator()
  289. delUserInstanceAction = menu.addAction(_("Remove selected"))
  290. updateUserInstanceAction = menu.addAction(_("Update selected"))
  291. if handler.hasActiveJobs():
  292. delUserInstanceAction.setEnabled(False)
  293. columnIndex = 0
  294. for columnData in self.model().getColumns():
  295. action = QWidgetAction(columnsMenu)
  296. widget = QCheckBox(columnData.name, columnsMenu)
  297. widget.setTristate(False)
  298. action.setDefaultWidget(widget)
  299. # Set current value
  300. if not self.isColumnHidden(columnIndex):
  301. widget.setChecked(True)
  302. columnsMenu.addAction(action)
  303. widget.stateChanged.connect(
  304. lambda state, index=columnIndex:
  305. self._hideColumn(index, state)
  306. )
  307. columnIndex += 1
  308. action = menu.exec_(self.mapToGlobal(event.pos()))
  309. if action == addToBlacklistAction:
  310. # Blacklist
  311. filter_ = self._filterModel.filter()
  312. indexes = self.selectionModel().selectedRows()
  313. urls = []
  314. forAll = None
  315. for index in indexes:
  316. url = self.model().getByIndex(index.row())
  317. if url in filter_['url']:
  318. # Url is whitelisted, what to do?
  319. if forAll == QMessageBox.YesRole:
  320. __removeFromWhitelist(self, url)
  321. elif forAll == QMessageBox.NoRole:
  322. continue
  323. else:
  324. questionBox = QMessageBox(self)
  325. questionBox.addButton(
  326. _("Yes"), QMessageBox.YesRole
  327. )
  328. questionBox.addButton(
  329. _("No"), QMessageBox.NoRole
  330. )
  331. questionBox.addButton(
  332. _("Cancel"), QMessageBox.RejectRole
  333. )
  334. questionBox.setWindowTitle("Url whitelisted")
  335. questionBox.setText(_(
  336. "{0} found in the <b>whitelist</b>. Would you"
  337. " like to <b>remove</b> it from the"
  338. " <b>whitelist</b> and add it to the"
  339. " <b>blacklist</b>?").format(url)
  340. )
  341. checkBox = QCheckBox(
  342. _("Remember for all"), questionBox
  343. )
  344. questionBox.setCheckBox(checkBox)
  345. questionBox.exec()
  346. answer = questionBox.result()
  347. if answer == 0: # Yes
  348. # Remove url from whitelist and add it to
  349. # the blacklist
  350. __removeFromWhitelist(self, url)
  351. if checkBox.isChecked():
  352. forAll = QMessageBox.YesRole
  353. elif answer == 1 and checkBox.isChecked():
  354. forAll = QMessageBox.NoRole
  355. continue
  356. else:
  357. continue
  358. urls.append(url)
  359. self._filterModel.updateKwargs(
  360. {
  361. 'skipUrl': filter_['skipUrl'] + urls
  362. }
  363. )
  364. elif action == addToTmpBlacklistAction:
  365. index = selectionModel.selectedRows()[0]
  366. url = self.model().getByIndex(index.row())
  367. self._filterModel.putInstanceOnTimeout(url, reason=_("Manual"))
  368. elif action == addToWhitelistAction:
  369. # Whitelist
  370. filter_ = self._filterModel.filter()
  371. indexes = self.selectionModel().selectedRows()
  372. toAdd = []
  373. for index in indexes:
  374. url = self.model().getByIndex(index.row())
  375. if url not in filter_['url']:
  376. toAdd.append(url)
  377. self._filterModel.updateKwargs(
  378. {
  379. 'url': filter_['url'] + toAdd
  380. }
  381. )
  382. elif action == copyUrlAction:
  383. index = selectionModel.selectedRows()[0]
  384. url = self.model().getByIndex(index.row())
  385. clipboard = QGuiApplication.clipboard()
  386. clipboard.setText(url)
  387. elif action == selectAllAction:
  388. self.selectAll()
  389. # User instances actions
  390. if userInstances:
  391. if action == delUserInstanceAction:
  392. indexes = self.selectionModel().selectedRows()
  393. # Confirmation
  394. instancesStr = ""
  395. for index in indexes:
  396. url = self.model().getByIndex(index.row())
  397. instancesStr = "{0}\n{1}".format(instancesStr, url)
  398. confirmDialog = QMessageBox()
  399. confirmDialog.setWindowTitle(
  400. _("Delete instances")
  401. )
  402. confirmDialog.setText(
  403. _("Are you sure you want to delete the following"
  404. " instances?\n") + instancesStr
  405. )
  406. confirmDialog.setStandardButtons(
  407. QMessageBox.Yes | QMessageBox.No
  408. )
  409. confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
  410. confirmDialog.button(QMessageBox.No).setText(_("No"))
  411. if confirmDialog.exec() != QMessageBox.Yes:
  412. return
  413. toRemove = []
  414. for index in indexes:
  415. toRemove.append(self.model().getByIndex(index.row()))
  416. handler.removeMultiInstances(toRemove)
  417. elif action == updateUserInstanceAction:
  418. indexes = self.selectionModel().selectedRows()
  419. handler = self._filterModel.parentModel().handler()
  420. for index in indexes:
  421. handler.updateInstance(
  422. self.model().getByIndex(index.row())
  423. )
  424. class UrlFilterItem(QWidget):
  425. deleted = pyqtSignal(str) # url
  426. def __init__(self, url, parent=None):
  427. QWidget.__init__(self, parent=parent)
  428. layout = QHBoxLayout(self)
  429. layout.setContentsMargins(0, 0, 0, 0)
  430. self._urlStr = url
  431. label = QLabel(url, self)
  432. delButton = Button("X", self)
  433. delButton.setSizePolicy(
  434. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  435. )
  436. layout.addWidget(label)
  437. layout.addWidget(delButton)
  438. delButton.clicked.connect(self.__delMe)
  439. def str(self): return self._urlStr
  440. def __delMe(self):
  441. self.deleted.emit(self.str())
  442. self.deleteLater()
  443. class UrlFilterWidget(QWidget):
  444. def __init__(self, model, key, parent=None):
  445. QWidget.__init__(self, parent=parent)
  446. self._model = model
  447. self._items = {}
  448. self._key = key
  449. QVBoxLayout(self)
  450. self.layout().setContentsMargins(0, 0, 0, 0)
  451. self.layout().setSpacing(0)
  452. self._model.changed.connect(self.__modelChanged)
  453. def __modelChanged(self):
  454. filter = self._model.filter()
  455. # Remove old
  456. for url in list(self._items.keys()):
  457. if url not in filter[self._key]:
  458. self._items[url].deleteLater()
  459. del self._items[url]
  460. # Add new
  461. for url in filter[self._key]:
  462. self._add(url)
  463. def _add(self, url):
  464. if url not in self._items:
  465. item = UrlFilterItem(url, self)
  466. self._items.update({url: item})
  467. item.deleted.connect(self.delete)
  468. self.layout().addWidget(item)
  469. def delete(self, url):
  470. del self._items[url]
  471. self._model.updateKwargs(self.data())
  472. def data(self):
  473. return {self._key: list(self._items.keys())}
  474. class TimeoutFilterWidget(UrlFilterWidget):
  475. def _add(self, url):
  476. UrlFilterWidget._add(self, url)
  477. # Add reason and duration tooltip
  478. filter = self._model.filter()
  479. durationStr = _("Until restart or manual removal.")
  480. timer = filter[self._key][url][0]
  481. if timer:
  482. durationStr = str(timer.interval() / 60000) + "min"
  483. tooltipStr = ("<b>{reasonLabel}</b>: \n{reason}\n<b>{durationLabel}"
  484. "</b>: {duration}".format(
  485. reasonLabel = _("Reason"),
  486. durationLabel = _("Duration"),
  487. reason = filter[self._key][url][1],
  488. duration = durationStr
  489. ))
  490. self._items[url].setToolTip(tooltipStr)
  491. def delete(self, url):
  492. del self._items[url]
  493. self._model.delInstanceFromTimeout(url)
  494. def data(self):
  495. return None
  496. class NetworkFilterWidget(QWidget):
  497. def __init__(self, model, parent=None):
  498. """
  499. @type model: InstancesModel
  500. """
  501. QWidget.__init__(self, parent=parent)
  502. self._model = model
  503. layout = QHBoxLayout(self)
  504. layout.setContentsMargins(0, 0, 0, 0)
  505. layout.setSpacing(0)
  506. self._webBox = QCheckBox("Web", self)
  507. self._torBox = QCheckBox("Tor", self)
  508. self._i2pBox = QCheckBox("i2p", self)
  509. self._webBox.setSizePolicy(
  510. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  511. )
  512. self._torBox.setSizePolicy(
  513. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  514. )
  515. self._i2pBox.setSizePolicy(
  516. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  517. )
  518. layout.addWidget(self._webBox, 0, Qt.AlignLeft)
  519. layout.addWidget(self._torBox, 0, Qt.AlignLeft)
  520. layout.addWidget(self._i2pBox, 1, Qt.AlignLeft)
  521. self._model.changed.connect(self._modelChanged)
  522. self._webBox.stateChanged.connect(self.__changed)
  523. self._torBox.stateChanged.connect(self.__changed)
  524. self._i2pBox.stateChanged.connect(self.__changed)
  525. def __changed(self, state):
  526. self._model.changed.disconnect(self._modelChanged)
  527. self._model.updateKwargs(self.data())
  528. self._model.changed.connect(self._modelChanged)
  529. def _modelChanged(self):
  530. self._webBox.stateChanged.disconnect(self.__changed)
  531. self._torBox.stateChanged.disconnect(self.__changed)
  532. self._i2pBox.stateChanged.disconnect(self.__changed)
  533. self._webBox.setChecked(False)
  534. self._torBox.setChecked(False)
  535. self._i2pBox.setChecked(False)
  536. filter = self._model.filter()
  537. if NetworkTypes.Web in filter['networkTypes']:
  538. self._webBox.setChecked(True)
  539. if NetworkTypes.Tor in filter['networkTypes']:
  540. self._torBox.setChecked(True)
  541. if NetworkTypes.I2P in filter['networkTypes']:
  542. self._i2pBox.setChecked(True)
  543. self._webBox.stateChanged.connect(self.__changed)
  544. self._torBox.stateChanged.connect(self.__changed)
  545. self._i2pBox.stateChanged.connect(self.__changed)
  546. def data(self):
  547. result = {
  548. 'networkTypes': [] # Network Types to pass
  549. }
  550. if self._webBox.isChecked():
  551. result['networkTypes'].append(NetworkTypes.Web)
  552. if self._torBox.isChecked():
  553. result['networkTypes'].append(NetworkTypes.Tor)
  554. if self._i2pBox.isChecked():
  555. result['networkTypes'].append(NetworkTypes.I2P)
  556. return result
  557. class VersionFilter(QWidget):
  558. def __init__(self, model, parent=None):
  559. """
  560. @type model: InstanceModelFilter
  561. """
  562. QWidget.__init__(self, parent=parent)
  563. self._model = model
  564. self._items = {}
  565. layout = QVBoxLayout(self)
  566. layout.setContentsMargins(0, 0, 0, 0)
  567. layout.setSpacing(0)
  568. self.minVersionCombo = QComboBox(self)
  569. self.gitVersionCheck = QCheckBox(_("Development"), self)
  570. self.gitVersionCheck.setToolTip(
  571. _("Include development versions of searx (Git versions) when "
  572. "checked.")
  573. )
  574. # Disable by default since there isn't a version selected by default.
  575. self.gitVersionCheck.setEnabled(False)
  576. self.unknownSuffixVersionCheck = QCheckBox(_("Unknown suffix"), self)
  577. self.unknownSuffixVersionCheck.setToolTip(
  578. _("Include versions that have the '-unknown' suffix when checked.")
  579. )
  580. # Disable by default since there isn't a version selected by default.
  581. self.unknownSuffixVersionCheck.setEnabled(False)
  582. # Add widgets to layout
  583. layout.addWidget(self.minVersionCombo)
  584. layout.addWidget(self.gitVersionCheck)
  585. layout.addWidget(self.unknownSuffixVersionCheck)
  586. # Signal connections
  587. self._model.changed.connect(self.__modelChanged)
  588. self.minVersionCombo.currentTextChanged.connect(
  589. self._minVersionComboChanged
  590. )
  591. self.gitVersionCheck.stateChanged.connect(self._optionCheckBoxChanged)
  592. self.unknownSuffixVersionCheck.stateChanged.connect(
  593. self._optionCheckBoxChanged
  594. )
  595. def _minVersionComboChanged(self, versionStr):
  596. """ From the ComboBox
  597. """
  598. self._model.changed.disconnect(self.__modelChanged)
  599. self._model.updateKwargs(self.data()) # update the model.
  600. self._minVersionChanged()
  601. self._model.changed.connect(self.__modelChanged)
  602. def _minVersionChanged(self):
  603. """ Minimum version has changed, it doesn't matter if it comes from the
  604. ComboBox or the Model.
  605. """
  606. # Enable/Disable the checkboxes based on wheter a minimum version is
  607. # selected or not.
  608. enabled = bool(self.minVersionCombo.currentIndex() != 0)
  609. self.gitVersionCheck.setEnabled(enabled)
  610. self.unknownSuffixVersionCheck.setEnabled(enabled)
  611. def _optionCheckBoxChanged(self, checkState):
  612. """ 'Development' or 'Unknown suffix' CheckBox has changed.
  613. """
  614. self._model.updateKwargs(self.data()) # update the model.
  615. def _compileVersions(self):
  616. """ Fill the combobox with available version strings
  617. """
  618. # Backup selected text so we can find the new index if it still exists.
  619. versionFilter = self._model.filter().get('version', {})
  620. selectedText = versionFilter.get('min', "")
  621. # Clear the combo
  622. self.minVersionCombo.clear()
  623. # Add the default None item
  624. self.minVersionCombo.addItem("")
  625. # Unique
  626. uniqueVersions = []
  627. for url, instance in self._model.parentModel().items():
  628. if (
  629. instance.version.type and
  630. str(instance.version) not in uniqueVersions
  631. ):
  632. uniqueVersions.append(str(instance.version))
  633. # Sort
  634. uniqueVersions.sort()
  635. # Add versions to combobox
  636. selectedIndex = 0
  637. for versionStr in uniqueVersions:
  638. if versionStr == selectedText:
  639. # The selected item is still there, store it's index so we can
  640. # set it back later.
  641. selectedIndex = self.minVersionCombo.count()
  642. self.minVersionCombo.addItem(versionStr)
  643. # Restore the selected index
  644. self.minVersionCombo.setCurrentIndex(selectedIndex)
  645. # Enable/Disable the checkboxes based on wheter a minimum version is
  646. # selected or not.
  647. self._minVersionChanged()
  648. def __modelChanged(self):
  649. # Instances model has changed
  650. # Update versions combo
  651. self.minVersionCombo.currentTextChanged.disconnect(
  652. self._minVersionComboChanged
  653. )
  654. self._compileVersions() # re-generate the combobox items
  655. self.minVersionCombo.currentTextChanged.connect(
  656. self._minVersionComboChanged
  657. )
  658. versionFilter = self._model.filter().get('version', {})
  659. self.gitVersionCheck.setChecked(versionFilter.get('git', True))
  660. self.unknownSuffixVersionCheck.setChecked(
  661. versionFilter.get('unknown', True)
  662. )
  663. def data(self):
  664. result = {
  665. 'version': {
  666. 'min': self.minVersionCombo.currentText(),
  667. 'git': bool(self.gitVersionCheck.checkState()),
  668. 'unknown': bool(self.unknownSuffixVersionCheck.checkState())
  669. }
  670. }
  671. return result
  672. class CheckboxFilter(QCheckBox):
  673. def __init__(self, model, key, parent=None):
  674. """
  675. @type model: InstancesModel
  676. @param key: key to use (from model.filter())
  677. @type key: str
  678. """
  679. QCheckBox.__init__(self, parent=parent)
  680. self._model = model
  681. self._key = key
  682. self._model.changed.connect(self.__modelChanged)
  683. self.stateChanged.connect(self.__stateChanged)
  684. def __modelChanged(self):
  685. """ Model changed, update the checkbox.
  686. """
  687. filter_ = self._model.filter()
  688. state = filter_.get(self._key, True)
  689. self.stateChanged.disconnect(self.__stateChanged)
  690. self.setChecked(state)
  691. self.stateChanged.connect(self.__stateChanged)
  692. def __stateChanged(self):
  693. """ Checkbox state changed, update the model.
  694. """
  695. state = self.isChecked()
  696. self._model.updateKwargs({self._key: state})
  697. class FilterWidget(QWidget):
  698. def __init__(self, model, parent=None):
  699. """
  700. @type model: searxqt.models.instances.InstanceModelFilter
  701. """
  702. QWidget.__init__(self, parent=parent)
  703. self._model = model
  704. layout = QFormLayout(self)
  705. layout.setLabelAlignment(
  706. Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
  707. )
  708. layout.addRow(QLabel(_("<h3>Filter</h3>"), self))
  709. self._networkFilter = NetworkFilterWidget(model, self)
  710. layout.addRow(QLabel(_("Network") + ":"), self._networkFilter)
  711. self._asnPrivacyFilter = None
  712. self._ipv6Filter = None
  713. self._versionFilter = VersionFilter(model, self)
  714. layout.addRow(QLabel(_("Min version") + ":"), self._versionFilter)
  715. self._urlFilterOut = UrlFilterWidget(model, 'skipUrl', self)
  716. layout.addRow(QLabel(_("Blacklist") + ":"), self._urlFilterOut)
  717. self._urlFilter = UrlFilterWidget(model, 'url', self)
  718. layout.addRow(QLabel(_("Whitelist") + ":"), self._urlFilter)
  719. self._tmpBlacklist = TimeoutFilterWidget(model, 'timeout', self)
  720. layout.addRow(QLabel(_("Timeout") + ":"), self._tmpBlacklist)
  721. model.parentModel().typeChanged.connect(self.__modelTypeChanged)
  722. def __modelTypeChanged(self, type_):
  723. layout = self.layout()
  724. if type_ == InstancesModelTypes.Stats2:
  725. if not self._asnPrivacyFilter:
  726. self._asnPrivacyFilter = CheckboxFilter(
  727. self._model,
  728. 'asnPrivacy',
  729. self
  730. )
  731. label = QLabel(_("Require ASN privacy") + ":")
  732. label.setWordWrap(True)
  733. label.setToolTip(_(
  734. "Filter out instances that run their server at a known\n"
  735. "malicious network like Google, CloudFlare, Akamai etc..\n"
  736. "\n"
  737. "This does not give any guarantee, it only filters known\n"
  738. "privacy violators!"
  739. ))
  740. layout.insertRow(
  741. 2,
  742. label,
  743. self._asnPrivacyFilter
  744. )
  745. if not self._ipv6Filter:
  746. self._ipv6Filter = CheckboxFilter(self._model, 'ipv6', self)
  747. label = QLabel(_("Require IPv6") + ":")
  748. label.setWordWrap(True)
  749. label.setToolTip(_(
  750. "Only instances with at least one ipv6-address remain"
  751. " if checked."
  752. ))
  753. layout.insertRow(3, label, self._ipv6Filter)
  754. else:
  755. if self._asnPrivacyFilter:
  756. self._asnPrivacyFilter.deleteLater()
  757. layout.removeRow(2)
  758. self._asnPrivacyFilter = None
  759. if self._ipv6Filter:
  760. self._ipv6Filter.deleteLater()
  761. layout.removeRow(2)
  762. self._ipv6Filter = None
  763. class InstancesStatsWidget(QWidget):
  764. def __init__(self, filterModel, parent=None):
  765. """
  766. @param filterModel:
  767. @type filterModel: searxqt.models.instances.InstanceModelFilter
  768. """
  769. QWidget.__init__(self, parent=parent)
  770. self._filterModel = filterModel
  771. self._currentInstancesType = filterModel.parentModel().Type
  772. self._countLabel = None
  773. self._filterCountLabel = None
  774. self._lastUpdateLabel = None
  775. layout = QFormLayout(self)
  776. layout.setLabelAlignment(
  777. Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
  778. )
  779. self._toggleButton = Button("", self)
  780. layout.addRow(QLabel(_("<h3>Stats</h3>"), self), self._toggleButton)
  781. self.__show()
  782. self._toggleButton.clicked.connect(self.__toggle)
  783. self._filterModel.parentModel().typeChanged.connect(
  784. self.__instancesModelTypeChanged
  785. )
  786. def collapsed(self):
  787. return bool(self._countLabel is None)
  788. def __toggle(self):
  789. if self.collapsed():
  790. self.__show()
  791. else:
  792. self.__hide()
  793. def __show(self):
  794. self._toggleButton.setText(_("Hide"))
  795. layout = self.layout()
  796. self._countLabel = QLabel("0", self)
  797. layout.addRow(QLabel(_("Total count") + ":"), self._countLabel)
  798. self._filterCountLabel = QLabel("0", self)
  799. layout.addRow(
  800. QLabel(_("After filter count") + ":"),
  801. self._filterCountLabel
  802. )
  803. self._lastUpdateLabel = QLabel("0", self)
  804. self._lastUpdateLabel.setWordWrap(True)
  805. layout.addRow(QLabel(_("Last update") + ":"), self._lastUpdateLabel)
  806. self.__modelChanged() # Update count and after filter count
  807. self.__instancesModelTypeChanged(self._filterModel.parentModel().Type)
  808. self._filterModel.changed.connect(self.__modelChanged)
  809. def __hide(self):
  810. self._filterModel.changed.disconnect(self.__modelChanged)
  811. self.__instancesModelTypeChanged(InstancesModelTypes.NotDefined)
  812. self._toggleButton.setText(_("Show"))
  813. layout = self.layout()
  814. while layout.rowCount() > 1:
  815. layout.removeRow(1)
  816. self._countLabel = None
  817. self._filterCountLabel = None
  818. self._lastUpdateLabel = None
  819. """ InstancesType dependant methods below
  820. """
  821. def __instancesModelTypeChanged(self, type_):
  822. """ PersistentInstancesModel emitted changed.
  823. """
  824. if self.collapsed():
  825. return
  826. previousType = self._currentInstancesType
  827. self._currentInstancesType = type_
  828. # Disconnect signals
  829. if previousType == InstancesModelTypes.Stats2:
  830. self._filterModel.changed.disconnect(self.__updateLastUpdated)
  831. elif previousType == InstancesModelTypes.User:
  832. pass
  833. if type_ == InstancesModelTypes.Stats2:
  834. self._filterModel.changed.connect(self.__updateLastUpdated)
  835. self.__updateLastUpdated()
  836. else:
  837. self._lastUpdateLabel.setText("-")
  838. # InstancesModelTypes.Stats2
  839. def __updateLastUpdated(self):
  840. self._lastUpdateLabel.setText(
  841. timeToString(
  842. self._filterModel.parentModel().handler().lastUpdated()
  843. )
  844. )
  845. """ -------------------------------------------
  846. """
  847. def __modelChanged(self):
  848. self._countLabel.setText(str(len(self._filterModel.parentModel())))
  849. self._filterCountLabel.setText(str(len(self._filterModel)))