instances.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  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. import time
  22. from PyQt5.QtWidgets import (
  23. QWidget,
  24. QVBoxLayout,
  25. QLabel,
  26. QTableView,
  27. QHeaderView,
  28. QAbstractItemView,
  29. QHBoxLayout,
  30. QFormLayout,
  31. QCheckBox,
  32. QMenu,
  33. QComboBox,
  34. QSizePolicy,
  35. QScrollArea,
  36. QSplitter,
  37. QFrame,
  38. QWidgetAction
  39. )
  40. from PyQt5.QtCore import pyqtSignal, Qt
  41. from PyQt5.QtGui import QGuiApplication
  42. from searxqt.widgets.groupBox import CollapsableGroupBox
  43. from searxqt.widgets.buttons import Button
  44. from searxqt.models.instances import InstanceTableModel
  45. class InstancesView(QWidget):
  46. def __init__(self, model, instanceSelecter, parent=None):
  47. """
  48. @type model: InstanceModelFilter
  49. @type instanceSelecter: InstanceSelecterModel
  50. @type parent: QObject
  51. """
  52. QWidget.__init__(self, parent=parent)
  53. layout = QVBoxLayout(self)
  54. layout.setContentsMargins(0, 0, 0, 0)
  55. layout.setSpacing(0)
  56. self._model = model
  57. self._instanceSelecter = instanceSelecter
  58. self._tableModel = InstanceTableModel(model, self)
  59. headLabel = QLabel("<h2>Instances</h2>", self)
  60. layout.addWidget(headLabel)
  61. # Splitter
  62. self._splitter = QSplitter(self)
  63. self._splitter.setOrientation(Qt.Vertical)
  64. layout.addWidget(self._splitter)
  65. # Filter scroll area
  66. self._scrollArea = QScrollArea(self._splitter)
  67. self._scrollContentWidget = QWidget(self._scrollArea)
  68. scrollLayout = QVBoxLayout(self._scrollContentWidget)
  69. self.filterWidget = FilterWidget(model, self._scrollContentWidget)
  70. self._scrollArea.setWidget(self._scrollContentWidget)
  71. self._scrollArea.setFrameShape(QFrame.NoFrame)
  72. self._scrollArea.setWidgetResizable(True)
  73. self._scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
  74. self._scrollArea.setAlignment(
  75. Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
  76. )
  77. scrollLayout.addWidget(self.filterWidget, 0, Qt.AlignTop)
  78. # Bottom widgets
  79. bottomWidget = QWidget(self._splitter)
  80. bottomLayout = QVBoxLayout(bottomWidget)
  81. self.statsWidget = InstancesStatsWidget(self._model, bottomWidget)
  82. bottomLayout.addWidget(self.statsWidget)
  83. updateButton = Button("Update", bottomWidget)
  84. bottomLayout.addWidget(updateButton)
  85. self._tableView = InstanceTableView(model, self._tableModel, self)
  86. bottomLayout.addWidget(self._tableView)
  87. updateButton.clicked.connect(self.__updateClicked)
  88. self.filterWidget.changed.connect(self.__filterChanged)
  89. self._tableView.selectionModel().selectionChanged.connect(
  90. self.__selectionChanged)
  91. self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)
  92. def __instanceChanged(self, url):
  93. """ Select and scroll to table item with passed url.
  94. @param url: Instance url to select and scroll to.
  95. @type url: str
  96. """
  97. if url:
  98. rowIndex = self._tableModel.getByUrl(url)
  99. modelIndex = self._tableModel.index(rowIndex, 0)
  100. self._tableView.setCurrentIndex(modelIndex)
  101. self._tableView.scrollTo(
  102. modelIndex,
  103. QAbstractItemView.PositionAtCenter
  104. )
  105. def __selectionChanged(self):
  106. """ Let the InstanceSelecter know that the current instance has
  107. changed. (Selected by the user.)
  108. """
  109. self._instanceSelecter.instanceChanged.disconnect(
  110. self.__instanceChanged)
  111. selectionModel = self._tableView.selectionModel()
  112. if selectionModel.hasSelection():
  113. rows = selectionModel.selectedRows()
  114. selectedIndex = rows[0].row()
  115. self._instanceSelecter.currentUrl = (
  116. self._tableModel.getByIndex(selectedIndex)
  117. )
  118. else:
  119. self._instanceSelecter.currentUrl = ""
  120. self._instanceSelecter.instanceChanged.connect(self.__instanceChanged)
  121. def __updateClicked(self, checked):
  122. self._model.update()
  123. def __filterChanged(self):
  124. self._model.updateKwargs(self.filterWidget.data())
  125. def saveSettings(self):
  126. header = self._tableView.horizontalHeader()
  127. data = {
  128. 'hiddenColumnIndexes': self._tableView.getHiddenColumnIndexes(),
  129. 'headerState': header.saveState()
  130. }
  131. return data
  132. def loadSettings(self, data):
  133. self._tableView.setHiddenColumnIndexes(
  134. data.get('hiddenColumnIndexes', [])
  135. )
  136. if 'headerState' in data:
  137. header = self._tableView.horizontalHeader()
  138. header.restoreState(data['headerState'])
  139. class InstanceTableView(QTableView):
  140. def __init__(self, filterModel, model=None, parent=None):
  141. QTableView.__init__(self, parent=parent)
  142. self._filterModel = filterModel
  143. if model:
  144. self.setModel(model)
  145. self.setAlternatingRowColors(True)
  146. self.setSelectionBehavior(QAbstractItemView.SelectRows)
  147. self.setEditTriggers(QAbstractItemView.NoEditTriggers)
  148. self.setSortingEnabled(True)
  149. self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
  150. # Horizontal header
  151. header = self.horizontalHeader()
  152. header.setSectionResizeMode(QHeaderView.ResizeToContents)
  153. header.setSectionsMovable(True)
  154. def _hideColumn(self, index, state):
  155. state = state == 0
  156. self.setColumnHidden(index, state)
  157. def getHiddenColumnIndexes(self):
  158. columnIndex = 0
  159. indexes = []
  160. for columnData in self.model().getColumns():
  161. # Set current value
  162. if self.isColumnHidden(columnIndex):
  163. indexes.append(columnIndex)
  164. columnIndex += 1
  165. return indexes
  166. def setHiddenColumnIndexes(self, indexes):
  167. for index in indexes:
  168. self._hideColumn(index, True)
  169. """ Re-implementations
  170. """
  171. def contextMenuEvent(self, event):
  172. selectionModel = self.selectionModel()
  173. if selectionModel.hasSelection():
  174. menu = QMenu(self)
  175. # Add to blacklist
  176. addToBlacklistAction = menu.addAction("Add to blacklist")
  177. # Copy URL
  178. copyUrlAction = menu.addAction("Copy URL")
  179. menu.addSeparator()
  180. # Select all
  181. selectAllAction = menu.addAction("Select All")
  182. menu.addSeparator()
  183. # Column options
  184. columnsMenu = QMenu("Columns", self)
  185. menu.addMenu(columnsMenu)
  186. columnIndex = 0
  187. for columnData in self.model().getColumns():
  188. action = QWidgetAction(columnsMenu)
  189. widget = QCheckBox(columnData.name, columnsMenu)
  190. widget.setTristate(False)
  191. action.setDefaultWidget(widget)
  192. # Set current value
  193. if not self.isColumnHidden(columnIndex):
  194. widget.setChecked(True)
  195. columnsMenu.addAction(action)
  196. widget.stateChanged.connect(
  197. lambda state, index=columnIndex:
  198. self._hideColumn(index, state)
  199. )
  200. columnIndex += 1
  201. action = menu.exec_(self.mapToGlobal(event.pos()))
  202. if action == addToBlacklistAction:
  203. indexes = selectionModel.selectedRows()
  204. filter_ = self._filterModel.filter()
  205. toAdd = []
  206. for index in indexes:
  207. url = self.model().getByIndex(index.row())
  208. toAdd.append(url)
  209. self._filterModel.updateKwargs(
  210. {
  211. 'skipUrl': filter_['skipUrl'] + toAdd
  212. }
  213. )
  214. elif action == copyUrlAction:
  215. index = selectionModel.selectedRows()[0]
  216. url = self.model().getByIndex(index.row())
  217. clipboard = QGuiApplication.clipboard()
  218. clipboard.setText(url)
  219. elif action == selectAllAction:
  220. self.selectAll()
  221. class UrlBlacklistItem(QWidget):
  222. deleted = pyqtSignal(str) # version string
  223. def __init__(self, url, parent=None):
  224. QWidget.__init__(self, parent=parent)
  225. layout = QHBoxLayout(self)
  226. layout.setContentsMargins(0, 0, 0, 0)
  227. self._urlStr = url
  228. label = QLabel(url, self)
  229. delButton = Button("X", self)
  230. delButton.setSizePolicy(
  231. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  232. )
  233. layout.addWidget(label)
  234. layout.addWidget(delButton)
  235. delButton.clicked.connect(self.__delMe)
  236. def str(self): return self._urlStr
  237. def __delMe(self):
  238. self.deleted.emit(self.str())
  239. self.deleteLater()
  240. class UrlFilterWidget(QWidget):
  241. def __init__(self, model, parent=None):
  242. QWidget.__init__(self, parent=parent)
  243. self._model = model
  244. self._items = []
  245. QVBoxLayout(self)
  246. self.layout().setContentsMargins(0, 0, 0, 0)
  247. self.layout().setSpacing(0)
  248. self._model.changed.connect(self.__modelChanged)
  249. def __modelChanged(self):
  250. filter = self._model.filter()
  251. for url in filter['skipUrl']:
  252. self.__add(url, updateModel=False)
  253. def __add(self, url, updateModel=True):
  254. if url not in self._items:
  255. item = UrlBlacklistItem(url, self)
  256. self._items.append(url)
  257. item.deleted.connect(self.delete)
  258. self.layout().addWidget(item)
  259. def delete(self, url):
  260. self._items.remove(url)
  261. self._model.updateKwargs(self.data())
  262. def data(self):
  263. return {'skipUrl': self._items.copy()}
  264. class NetworkFilterWidget(QWidget):
  265. def __init__(self, model, parent=None):
  266. """
  267. @type model: InstancesModel
  268. """
  269. QWidget.__init__(self, parent=parent)
  270. self._model = model
  271. layout = QHBoxLayout(self)
  272. layout.setContentsMargins(0, 0, 0, 0)
  273. layout.setSpacing(0)
  274. self._webBox = QCheckBox("Web", self)
  275. self._torBox = QCheckBox("Tor", self)
  276. self._i2pBox = QCheckBox("i2p", self)
  277. self._webBox.setSizePolicy(
  278. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  279. )
  280. self._torBox.setSizePolicy(
  281. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  282. )
  283. self._i2pBox.setSizePolicy(
  284. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  285. )
  286. layout.addWidget(self._webBox, 0, Qt.AlignLeft)
  287. layout.addWidget(self._torBox, 0, Qt.AlignLeft)
  288. layout.addWidget(self._i2pBox, 1, Qt.AlignLeft)
  289. self._model.changed.connect(self._modelChanged)
  290. self._webBox.stateChanged.connect(self.__changed)
  291. self._torBox.stateChanged.connect(self.__changed)
  292. self._i2pBox.stateChanged.connect(self.__changed)
  293. def __changed(self, state):
  294. self._model.changed.disconnect(self._modelChanged)
  295. self._model.updateKwargs(self.data())
  296. self._model.changed.connect(self._modelChanged)
  297. def _modelChanged(self):
  298. self._webBox.stateChanged.disconnect(self.__changed)
  299. self._torBox.stateChanged.disconnect(self.__changed)
  300. self._i2pBox.stateChanged.disconnect(self.__changed)
  301. self._webBox.setChecked(False)
  302. self._torBox.setChecked(False)
  303. self._i2pBox.setChecked(False)
  304. filter = self._model.filter()
  305. # Nothing is checked
  306. if not filter['ext'] and not filter['skipExt']:
  307. self._webBox.setChecked(True)
  308. self._torBox.setChecked(True)
  309. self._i2pBox.setChecked(True)
  310. # Only web checked
  311. elif 'onion' in filter['skipExt'] and 'i2p' in filter['skipExt']:
  312. self._webBox.setChecked(True)
  313. # Web + Tor
  314. elif 'i2p' in filter['skipExt'] and not filter['ext']:
  315. self._webBox.setChecked(True)
  316. self._torBox.setChecked(True)
  317. # Web + i2p
  318. elif 'onion' in filter['skipExt'] and not filter['ext']:
  319. self._webBox.setChecked(True)
  320. self._torBox.setChecked(True)
  321. # Tor + i2p
  322. elif 'onion' in filter['ext'] and 'i2p' in filter['ext']:
  323. self._i2pBox.setChecked(True)
  324. self._torBox.setChecked(True)
  325. # Tor
  326. elif 'onion' in filter['ext']:
  327. self._torBox.setChecked(True)
  328. # i2p
  329. elif 'i2p' in filter['ext']:
  330. self._i2pBox.setChecked(True)
  331. self._webBox.stateChanged.connect(self.__changed)
  332. self._torBox.stateChanged.connect(self.__changed)
  333. self._i2pBox.stateChanged.connect(self.__changed)
  334. def data(self):
  335. result = {
  336. 'ext': [], # required
  337. 'skipExt': [] # skip
  338. }
  339. """
  340. [ ] web [ ] tor [ ] i2p
  341. [x] web [x] tor [x] i2p
  342. result = {
  343. 'ext': [],
  344. 'skipExt': []
  345. }
  346. [x] web
  347. [ ] tor
  348. [ ] i2p
  349. result = {
  350. 'ext': [],
  351. 'skipExt': ['onion', 'i2p']
  352. }
  353. [x] web
  354. [x] tor
  355. [ ] i2p
  356. result = {
  357. 'ext': [],
  358. 'skipExt': ['i2p']
  359. }
  360. [x] web
  361. [ ] tor
  362. [x] i2p
  363. result = {
  364. 'ext': [],
  365. 'skipExt': ['onion']
  366. }
  367. [ ] web
  368. [x] tor
  369. [x] i2p
  370. result = {
  371. 'ext': ['onion', 'i2p'],
  372. 'skipExt': []
  373. }
  374. [ ] web
  375. [ ] tor
  376. [x] i2p
  377. result = {
  378. 'ext': ['i2p'],
  379. 'skipExt': []
  380. }
  381. [ ] web
  382. [x] tor
  383. [ ] i2p
  384. result = {
  385. 'ext': ['onion'],
  386. 'skipExt': []
  387. }
  388. """
  389. # Nothing or all checked
  390. if ((not self._webBox.isChecked()
  391. and not self._torBox.isChecked()
  392. and not self._i2pBox.isChecked())
  393. or (self._webBox.isChecked()
  394. and self._torBox.isChecked()
  395. and self._i2pBox.isChecked())):
  396. return result
  397. # Only web checked
  398. if (self._webBox.isChecked()
  399. and not self._torBox.isChecked()
  400. and not self._i2pBox.isChecked()):
  401. result['skipExt'].append('onion')
  402. result['skipExt'].append('i2p')
  403. return result
  404. # Web + Tor
  405. if (self._webBox.isChecked()
  406. and self._torBox.isChecked()
  407. and not self._i2pBox.isChecked()):
  408. result['skipExt'].append('i2p')
  409. return result
  410. # Web + i2p
  411. if (self._webBox.isChecked()
  412. and not self._torBox.isChecked()
  413. and self._i2pBox.isChecked()):
  414. result['skipExt'].append('onion')
  415. return result
  416. # Tor + i2p
  417. if (not self._webBox.isChecked()
  418. and self._torBox.isChecked()
  419. and self._i2pBox.isChecked()):
  420. result['ext'].append('onion')
  421. result['ext'].append('i2p')
  422. return result
  423. # Tor
  424. if (not self._webBox.isChecked()
  425. and self._torBox.isChecked()
  426. and not self._i2pBox.isChecked()):
  427. result['ext'].append('onion')
  428. return result
  429. # i2p
  430. if (not self._webBox.isChecked()
  431. and not self._torBox.isChecked()
  432. and self._i2pBox.isChecked()):
  433. result['ext'].append('i2p')
  434. return result
  435. return result
  436. class VersionItem(QWidget):
  437. deleted = pyqtSignal(str) # version string
  438. def __init__(self, versionStr, parent=None):
  439. QWidget.__init__(self, parent=parent)
  440. layout = QHBoxLayout(self)
  441. layout.setContentsMargins(0, 0, 0, 0)
  442. layout.setSpacing(0)
  443. self._versionStr = versionStr
  444. label = QLabel(versionStr, self)
  445. delButton = Button("X", self)
  446. delButton.setSizePolicy(QSizePolicy(
  447. QSizePolicy.Maximum, QSizePolicy.Fixed))
  448. layout.addWidget(label)
  449. layout.addWidget(delButton)
  450. delButton.clicked.connect(self.__delMe)
  451. def str(self): return self._versionStr
  452. def __delMe(self):
  453. self.deleted.emit(self.str())
  454. self.deleteLater()
  455. class VersionFilter(QWidget):
  456. def __init__(self, model, parent=None):
  457. """
  458. @type model: InstanceModelFilter
  459. """
  460. QWidget.__init__(self, parent=parent)
  461. self._model = model
  462. self._items = {}
  463. layout = QVBoxLayout(self)
  464. layout.setContentsMargins(0, 0, 0, 0)
  465. layout.setSpacing(0)
  466. # Add new
  467. horizontalLayout = QHBoxLayout()
  468. layout.addLayout(horizontalLayout)
  469. self.comboBox = QComboBox(self)
  470. self.addButton = Button("Add", self)
  471. self.addButton.setSizePolicy(
  472. QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
  473. )
  474. horizontalLayout.addWidget(self.comboBox)
  475. horizontalLayout.addWidget(self.addButton)
  476. self.addButton.clicked.connect(self.__addClicked)
  477. self._model.changed.connect(self.__modelChanged)
  478. def __addClicked(self):
  479. """ Add button clicked -- will add the current selected version
  480. of the combobox to the version filter list.
  481. This wil trigger the following:
  482. 1) self.__add
  483. Adds the version string as a key to self._items
  484. 2) self._model.updateKwargs
  485. Update the model
  486. 3) self.__modelChanged
  487. Model has been changed so self.__modelChanged is called. This
  488. will re-generate the view.
  489. """
  490. self.__add(self.comboBox.currentText())
  491. self._model.updateKwargs(self.data()) # update the model.
  492. def __fromItemDelete(self, versionStr):
  493. """ Called when a VersionItem emits deleted (It's 'X' aka delete
  494. button has been pressed).
  495. This will trigger the following:
  496. 1) self.delete
  497. Deletes the version (key) from self._items and removes from
  498. the widget from the view.
  499. 2) self._model.updateKwargs
  500. Update the model.
  501. 3) Model has been changed so self.__modelChanged is called. This
  502. will re-generate the view.
  503. """
  504. self.delete(versionStr)
  505. self._model.updateKwargs(self.data())
  506. def _compileVersions(self):
  507. """ Fill the combobox with available version strings
  508. """
  509. allVersions = []
  510. self.comboBox.clear()
  511. for url, instance in self._model.parentModel().items():
  512. if instance.version and instance.version not in allVersions:
  513. allVersions.append(instance.version)
  514. if instance.version not in self._items.keys():
  515. self.comboBox.addItem(instance.version)
  516. def __modelChanged(self):
  517. self.clear() # clear the versions list
  518. filter = self._model.filter()
  519. for versionStr in filter['version']:
  520. # Add the version to the list
  521. self.add(versionStr)
  522. self._compileVersions() # re-generate the combobox items
  523. def data(self):
  524. result = {
  525. 'version': [], # required
  526. 'skipVersion': [] # skip (TODO unused)
  527. }
  528. for versionStr in self._items.keys():
  529. result['version'].append(versionStr)
  530. return result
  531. def clear(self):
  532. """ Clears added version items (on the view side only)
  533. """
  534. keys = list(self._items.keys())
  535. for versionStr in keys:
  536. self.delete(versionStr)
  537. def __add(self, versionStr):
  538. self._items.update({versionStr: None})
  539. def add(self, versionStr):
  540. item = VersionItem(versionStr, self)
  541. self._items.update({versionStr: item})
  542. item.deleted.connect(self.__fromItemDelete)
  543. self.layout().addWidget(item)
  544. def delete(self, versionStr):
  545. if self._items[versionStr]:
  546. # delete the widget if exists.
  547. self._items[versionStr].deleteLater()
  548. del self._items[versionStr]
  549. class CheckboxFilter(QCheckBox):
  550. def __init__(self, model, key, parent=None):
  551. """
  552. @type model: InstancesModel
  553. @param key: key to use (from model.filter())
  554. @type key: str
  555. """
  556. QCheckBox.__init__(self, parent=parent)
  557. self._model = model
  558. self._key = key
  559. self._model.changed.connect(self.__modelChanged)
  560. self.stateChanged.connect(self.__stateChanged)
  561. def __modelChanged(self):
  562. """ Model changed, update the checkbox.
  563. """
  564. filter_ = self._model.filter()
  565. state = filter_.get(self._key, True)
  566. self.setChecked(state)
  567. def __stateChanged(self):
  568. """ Checkbox state changed, update the model.
  569. """
  570. state = self.isChecked()
  571. self._model.updateKwargs({self._key: state})
  572. class FilterWidget(CollapsableGroupBox):
  573. changed = pyqtSignal()
  574. def __init__(self, model, parent=None):
  575. CollapsableGroupBox.__init__(self, txt="Filter", parent=parent)
  576. layout = QFormLayout(self.contentWidget)
  577. layout.setLabelAlignment(
  578. Qt.AlignLeading | Qt.AlignLeft | Qt.AlignTop
  579. )
  580. self._networkFilter = NetworkFilterWidget(model, self)
  581. layout.addRow(QLabel("Network:"), self._networkFilter)
  582. self._asnPrivacyFilter = CheckboxFilter(model, 'asnPrivacy', self)
  583. label = QLabel("Require ASN privacy:")
  584. label.setWordWrap(True)
  585. label.setToolTip(
  586. "Filter out instances that run their server at a known\n"
  587. "malicious network like Google, CloudFlare, Akamai etc..\n"
  588. "\n"
  589. "This does not give any guarantee, it only filters known\n"
  590. "privacy violators!"
  591. )
  592. layout.addRow(label, self._asnPrivacyFilter)
  593. self._ipv6Filter = CheckboxFilter(model, 'ipv6', self)
  594. label = QLabel("Require IPv6:")
  595. label.setWordWrap(True)
  596. label.setToolTip(
  597. "Only instances with at least one ipv6-address remain if checked"
  598. )
  599. layout.addRow(label, self._ipv6Filter)
  600. self._versionFilter = VersionFilter(model, self)
  601. layout.addRow(QLabel("Version:"), self._versionFilter)
  602. self._urlFilter = UrlFilterWidget(model, self)
  603. layout.addRow(QLabel("Blacklist:"), self._urlFilter)
  604. class InstancesStatsWidget(CollapsableGroupBox):
  605. def __init__(self, model, parent=None):
  606. CollapsableGroupBox.__init__(self, txt="Stats", parent=parent)
  607. self._model = model
  608. layout = QFormLayout(self.contentWidget)
  609. self._countLabel = QLabel("0", self)
  610. layout.addRow(QLabel("Total count:"), self._countLabel)
  611. self._filterCountLabel = QLabel("0", self)
  612. layout.addRow(
  613. QLabel("After filter count:"),
  614. self._filterCountLabel
  615. )
  616. self._lastUpdateLabel = QLabel("0", self)
  617. self._lastUpdateLabel.setWordWrap(True)
  618. layout.addRow(QLabel("Last update:"), self._lastUpdateLabel)
  619. self._model.changed.connect(self.__modelChanged)
  620. def __modelChanged(self):
  621. self._countLabel.setText(str(len(self._model.parentModel())))
  622. self._filterCountLabel.setText(str(len(self._model)))
  623. self._lastUpdateLabel.setText(
  624. time.asctime(
  625. time.localtime(
  626. self._model.parentModel().lastUpdated()
  627. )
  628. )
  629. )
  630. def setCount(self, count):
  631. self._countLabel.setText(str(count))