instances.py 37 KB

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