12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610 |
- ########################################################################
- # Searx-Qt - Lightweight desktop application for Searx.
- # Copyright (C) 2020-2022 CYBERDEViL
- #
- # This file is part of Searx-Qt.
- #
- # Searx-Qt is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Searx-Qt is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- from PyQt5.QtWidgets import (
- QWidget,
- QVBoxLayout,
- QHBoxLayout,
- QLineEdit,
- QTextBrowser,
- QCheckBox,
- QLabel,
- QComboBox,
- QFrame,
- QMenu,
- QWidgetAction,
- QShortcut,
- QSpacerItem,
- QSizePolicy,
- QDialog,
- QListWidget,
- QTableView,
- QSplitter,
- QAbstractItemView,
- QHeaderView,
- QFormLayout,
- QMessageBox
- )
- from PyQt5.QtCore import pyqtSignal, Qt, QVariant, QByteArray, QEvent
- from PyQt5.QtGui import (
- QDesktopServices,
- QStandardItem,
- QStandardItemModel,
- QTextDocument
- )
- from searxqt.core.customAnchorCmd import AnchorCMD
- from searxqt.core.htmlGen import ResultsHtml, FailedResponsesHtml
- from searxqt.core.guard import ConsequenceType
- from searxqt.core.requests import ErrorType
- from searxqt.models.search import (
- SearchStatus,
- UserCategoriesModel,
- CategoriesModel
- )
- from searxqt.models.instances import EnginesTableModel
- from searxqt.widgets.buttons import Button, CheckboxOptionsButton
- from searxqt.thread import Thread
- from searxqt.translations import _
- from searxqt.themes import Themes
- from searxqt.core import log
- class SearchNavigation(QWidget):
- requestPage = pyqtSignal(int) # pageno
- def __init__(self, parent=None):
- QWidget.__init__(self, parent=parent)
- layout = QHBoxLayout(self)
- self.prevPageButton = Button("◄", self)
- self.pageNoLabel = QLabel("1", self)
- self.nextPageButton = Button("►", self)
- layout.addWidget(self.prevPageButton, 0, Qt.AlignLeft)
- layout.addWidget(self.pageNoLabel, 0, Qt.AlignCenter)
- layout.addWidget(self.nextPageButton, 0, Qt.AlignRight)
- self.prevPageButton.setEnabled(False)
- self.nextPageButton.clicked.connect(self._nextPage)
- self.prevPageButton.clicked.connect(self._prevPage)
- self.reset()
- def _updateLabel(self):
- self.pageNoLabel.setText(str(self._pageno))
- def _nextPage(self):
- self._pageno += 1
- if self._pageno > 1 and not self.prevPageButton.isEnabled():
- self.prevPageButton.setEnabled(True)
- self._updateLabel()
- self.requestPage.emit(self._pageno)
- def _prevPage(self):
- self._pageno -= 1
- if self._pageno == 1:
- self.prevPageButton.setEnabled(False)
- self.setNextEnabled(True)
- self._updateLabel()
- self.requestPage.emit(self._pageno)
- def reset(self):
- self._pageno = 1
- self.prevPageButton.setEnabled(False)
- self._updateLabel()
- def setNextEnabled(self, state):
- self.nextPageButton.setEnabled(state)
- class SearchEngines(CheckboxOptionsButton):
- def __init__(self, searchModel, instancesModel, parent=None):
- """
- @param searchModel: needed for getting and setting current
- enabled/disabled engines.
- @type searchModel: SearchModel
- @param instancesModel: needed for listing current available
- engines and update it's current filter
- to filter out instances without atleast
- one of the required engine(s).
- @type instancesModel: InstanceModelFilter
- """
- self._instancesModel = instancesModel
- self._searchModel = searchModel
- CheckboxOptionsButton.__init__(
- self,
- labelName=_("Engines"),
- parent=parent
- )
- instancesModel.parentModel().changed.connect(self.reGenerate)
- def updateFilter(self):
- """ Filter out instances that don't support atleast one of the
- enabled engines.
- """
- self._instancesModel.updateKwargs(
- {'engines': self.getCheckedOptionNames()}
- )
- """ Below are re-implementations.
- """
- def getCheckedOptionNames(self):
- """ Should return a list with checked option names. This will
- be used to generate the label.
- @return: should return a list with strings.
- @rtype: list
- """
- return self._searchModel.engines
- def getOptions(self):
- """ Should return a list with options tuple(key, name, state)
- This will be used to generate the options.
- """
- list_ = []
- tmp = []
- for url, instance in self._instancesModel.items():
- for engine in instance.engines:
- if engine.name not in tmp:
- state = bool(engine.name in self._searchModel.engines)
- list_.append((engine.name, engine.name, state))
- tmp.append(engine.name)
- return sorted(list_)
- def optionToggled(self, key, state):
- if state:
- self._searchModel.engines.append(key)
- else:
- self._searchModel.engines.remove(key)
- self.updateFilter()
- class CategoryEditor(QDialog):
- def __init__(self, enginesModel, categoriesModel,
- userCategoriesModel, parent=None):
- QDialog.__init__(self, parent=parent)
- self._categoriesModel = categoriesModel
- self._userCategoriesModel = userCategoriesModel
- self.setWindowTitle(_("Category manager"))
- layout = QHBoxLayout(self)
- # Splitter to horizontal split the categories widget and the engines
- # widget so their width becomes adjustable.
- self.splitter = QSplitter(self)
- self.splitter.setOrientation(Qt.Horizontal)
- layout.addWidget(self.splitter)
- # Categories
- catWidget = QWidget(self.splitter)
- catLayout = QVBoxLayout(catWidget)
- label = QLabel(f"<h2>{_('Categories')}</h2>", self)
- # Categories toolbuttons
- catToolLayout = QHBoxLayout()
- catAddButton = Button("+", self)
- self._catDelButton = Button("-", self)
- catToolLayout.addWidget(catAddButton, 0, Qt.AlignLeft)
- catToolLayout.addWidget(self._catDelButton, 1, Qt.AlignLeft)
- self._categoryListWidget = QListWidget(self)
- catLayout.addWidget(label)
- catLayout.addLayout(catToolLayout)
- catLayout.addWidget(self._categoryListWidget)
- # Engines
- engWidget = QWidget(self.splitter)
- engLayout = QVBoxLayout(engWidget)
- label = QLabel(f"<h2>{_('Engines')}</h2>", self)
- # Engines filter
- filterLayout = QHBoxLayout()
- self._enginesCategoryFilterBox = QComboBox(self)
- self._enginesCategoryFilterBox.addItem(_("All"))
- for key, cat in categoriesModel.items():
- self._enginesCategoryFilterBox.addItem(cat.name)
- filterLayout.addWidget(
- QLabel(_("Category") + ":", self), 1, Qt.AlignRight
- )
- filterLayout.addWidget(
- self._enginesCategoryFilterBox, 0, Qt.AlignRight
- )
- # Engines table
- self._enginesTableView = QTableView(self)
- self._enginesTableView.setAlternatingRowColors(True)
- self._enginesTableView.setSelectionBehavior(
- QAbstractItemView.SelectRows
- )
- self._enginesTableView.setEditTriggers(
- QAbstractItemView.NoEditTriggers
- )
- self._enginesTableView.setSortingEnabled(True)
- self._enginesTableView.setHorizontalScrollMode(
- QAbstractItemView.ScrollPerPixel
- )
- header = self._enginesTableView.horizontalHeader()
- header.setSectionResizeMode(QHeaderView.ResizeToContents)
- header.setSectionsMovable(True)
- self._enginesTableModel = EnginesTableModel(enginesModel, self)
- self._enginesTableView.setModel(self._enginesTableModel)
- engLayout.addWidget(label)
- engLayout.addLayout(filterLayout)
- engLayout.addWidget(self._enginesTableView)
- # Connections
- catAddButton.clicked.connect(self.__addCategoryClicked)
- self._catDelButton.clicked.connect(self.__delCategoryClicked)
- self._categoryListWidget.currentRowChanged.connect(
- self.__currentUserCategoryChanged
- )
- self._enginesCategoryFilterBox.currentIndexChanged.connect(
- self.__enginesCategoryFilterChanged
- )
- self._enginesTableView.setEnabled(False)
- self._catDelButton.setEnabled(False)
- self.__addUserCategories()
- self.__selectFirst()
- def __currentUserCategoryChanged(self, index):
- if index < 0:
- self._enginesTableView.setEnabled(False)
- self._catDelButton.setEnabled(False)
- else:
- self._enginesTableView.setEnabled(True)
- self._catDelButton.setEnabled(True)
- if self._userCategoriesModel:
- key = list(self._userCategoriesModel.keys())[index]
- self._enginesTableModel.setUserModel(
- self._userCategoriesModel[key]
- )
- def __addUserCategories(self):
- for catKey, cat in self._userCategoriesModel.items():
- self._categoryListWidget.addItem(cat.name)
- def __selectFirst(self):
- if self._categoryListWidget.count():
- self._categoryListWidget.setCurrentRow(0)
- def __selectLast(self):
- self._categoryListWidget.setCurrentRow(
- self._categoryListWidget.count() - 1
- )
- def __reloadUserCategories(self):
- self._categoryListWidget.currentRowChanged.disconnect(
- self.__currentUserCategoryChanged
- )
- self._enginesTableModel.setUserModel(None)
- self._categoryListWidget.clear()
- self.__addUserCategories()
- self._categoryListWidget.currentRowChanged.connect(
- self.__currentUserCategoryChanged
- )
- def __addCategoryClicked(self, state):
- dialog = AddUserCategoryDialog(
- self._userCategoriesModel.keys()
- )
- if dialog.exec():
- self._userCategoriesModel.addCategory(
- dialog.name.lower(),
- dialog.name
- )
- self.__reloadUserCategories()
- self.__selectLast()
- def __delCategoryClicked(self, state):
- index = self._categoryListWidget.currentRow()
- key = list(self._userCategoriesModel.keys())[index]
- confirmDialog = QMessageBox()
- confirmDialog.setWindowTitle(
- _("Delete category")
- )
- categoryName = self._userCategoriesModel[key].name
- confirmDialog.setText(_("Are you sure you want to delete the " \
- f"category `{categoryName}`?"))
- confirmDialog.setStandardButtons(
- QMessageBox.Yes | QMessageBox.No
- )
- confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
- confirmDialog.button(QMessageBox.No).setText(_("No"))
- if confirmDialog.exec() != QMessageBox.Yes:
- return
- self._userCategoriesModel.removeCategory(key)
- self.__reloadUserCategories()
- self.__selectLast()
- self.__currentUserCategoryChanged(
- self._categoryListWidget.count() - 1
- )
- def __enginesCategoryFilterChanged(self, index):
- if not index: # All
- self._enginesTableModel.setCatFilter()
- else:
- key = list(self._categoriesModel.keys())[index-1]
- self._enginesTableModel.setCatFilter(key)
- class AddUserCategoryDialog(QDialog):
- def __init__(self, existingNames=[], text="", parent=None):
- QDialog.__init__(self, parent=parent)
- self._existingNames = existingNames
- layout = QFormLayout(self)
- label = QLabel(_("Name") + ":")
- self._nameEdit = QLineEdit(self)
- if text:
- self._nameEdit.setText(text)
- self._nameEdit.setPlaceholderText(text)
- else:
- self._nameEdit.setPlaceholderText(_("My category"))
- self._cancelButton = Button(_("Cancel"), self)
- self._addButton = Button(_("Add"), self)
- self._addButton.setEnabled(False)
- # Add stuff to layout
- layout.addRow(label, self._nameEdit)
- layout.addRow(self._cancelButton, self._addButton)
- # Connections
- self._nameEdit.textChanged.connect(self.__inputChanged)
- self._addButton.clicked.connect(self.accept)
- self._cancelButton.clicked.connect(self.reject)
- def __inputChanged(self, text):
- if self.isValid():
- self._addButton.setEnabled(True)
- else:
- self._addButton.setEnabled(False)
- def isValid(self):
- name = self._nameEdit.text().lower()
- if not name:
- return False
- for existingName in self._existingNames:
- if name == existingName.lower():
- return False
- return True
- @property
- def name(self):
- return self._nameEdit.text()
- class SearchCategories(CheckboxOptionsButton):
- def __init__(self, categoriesModel, instanceCategoriesModel, enginesModel,
- userCategoriesModel, parent=None):
- """
- @param categoriesModel: Predefined categories (only avaiable when at
- least one instance has the category).
- @type categoriesModel: searxqt.models.search.CategoriesModel
- @param instanceCategoriesModel: Some instances define custom search
- categories.
- @type instanceCategoriesModel: searxqt.models.search.CategoriesModel
- @param enginesModel:
- @type enginesModel: searxqt.models.instances.EnginesModel
- @param userCategoriesModel:
- @type userCategoriesModel: searxqt.models.search.UserCategoriesModel
- @param parent:
- @type parent: QObject or None
- """
- self._categoriesModel = categoriesModel
- self._instanceCategoriesModel = instanceCategoriesModel
- self._enginesModel = enginesModel
- self._userCategoriesModel = userCategoriesModel
- CheckboxOptionsButton.__init__(
- self,
- labelName=_("Categories"),
- parent=parent
- )
- self._categoriesModel.dataChanged.connect(self.__categoryDataChanged)
- def __categoryDataChanged(self):
- # This happends after CategoriesModel.setData
- self.reGenerate()
- def __openUserCategoryEditor(self):
- window = CategoryEditor(
- self._enginesModel,
- self._categoriesModel,
- self._userCategoriesModel, self)
- window.exec()
- def __userCategoryToggled(self, key, state):
- if state:
- self._userCategoriesModel[key].check()
- else:
- self._userCategoriesModel[key].uncheck()
- self.reGenerate()
- def __instanceCategoryToggled(self, key, state):
- if state:
- self._instanceCategoriesModel[key].check()
- else:
- self._instanceCategoriesModel[key].uncheck()
- self.reGenerate()
- """ Below are re-implementations.
- """
- def addCustomWidgetsTop(self, menu):
- # User specified categories
- menu.addSection(_("Custom"))
- action = QWidgetAction(menu)
- manageCustomButton = Button(_("Manage"), menu)
- action.setDefaultWidget(manageCustomButton)
- menu.addAction(action)
- for catKey, cat in self._userCategoriesModel.items():
- action = QWidgetAction(menu)
- widget = QCheckBox(cat.name, menu)
- widget.setTristate(False)
- widget.setChecked(
- cat.isChecked()
- )
- action.setDefaultWidget(widget)
- widget.stateChanged.connect(
- lambda state, key=catKey:
- self.__userCategoryToggled(key, state)
- )
- menu.addAction(action)
- # Custom instance specified categories
- menu.addSection(_("Instances"))
- for catKey, cat in self._instanceCategoriesModel.items():
- action = QWidgetAction(menu)
- widget = QCheckBox(cat.name, menu)
- widget.setTristate(False)
- widget.setChecked(cat.isChecked())
- action.setDefaultWidget(widget)
- widget.stateChanged.connect(
- lambda state, key=catKey:
- self.__instanceCategoryToggled(key, state)
- )
- menu.addAction(action)
- # Predefined Searx categories
- menu.addSection(_("Default"))
- manageCustomButton.clicked.connect(self.__openUserCategoryEditor)
- def hasEnabledCheckedKeys(self):
- """ Same as CheckboxOptionsButton.hasEnabledCheckedKeys(self) but with
- User Categories. Categories don't get enabled/disabled so we can skip
- that check.
- @rtype: bool
- """
- if self._userCategoriesModel.checkedCategories():
- return True
- elif self._instanceCategoriesModel.checkedCategories():
- return True
- return CheckboxOptionsButton.hasEnabledCheckedKeys(self)
- def uncheckAllEnabledKeys(self):
- """ Unchecks all checked keys that are enabled.
- """
- for catKey in self._userCategoriesModel.checkedCategories():
- self._userCategoriesModel[catKey].uncheck()
- for catKey in self._instanceCategoriesModel.checkedCategories():
- self._instanceCategoriesModel[catKey].uncheck()
- CheckboxOptionsButton.uncheckAllEnabledKeys(self)
- def getCheckedOptionNames(self):
- """ Should return a list with checked option names. This will
- be used to generate the label.
- @return: should return a list with strings.
- @rtype: list
- """
- return(
- [
- self._categoriesModel[catKey].name
- for catKey in self._categoriesModel.checkedCategories()
- ] +
- [
- self._instanceCategoriesModel[catKey].name
- for catKey in self._instanceCategoriesModel.checkedCategories()
- ] +
- [
- self._userCategoriesModel[catKey].name
- for catKey in self._userCategoriesModel.checkedCategories()
- ]
- )
- def getOptions(self):
- """ Should return a list with options tuple(key, name, state)
- This will be used to generate the options.
- """
- list_ = []
- for catKey in self._categoriesModel:
- list_.append(
- (
- catKey,
- self._categoriesModel[catKey].name,
- self._categoriesModel[catKey].isChecked()
- )
- )
- return list_
- def optionToggled(self, key, state):
- if state:
- self._categoriesModel[key].check()
- else:
- self._categoriesModel[key].uncheck()
- class SearchPeriod(QComboBox):
- def __init__(self, model, parent=None):
- QComboBox.__init__(self, parent=parent)
- self._model = model
- self.setMinimumContentsLength(2)
- for period in model.Periods:
- self.addItem(model.Periods[period], QVariant(period))
- self.currentIndexChanged.connect(self.__indexChanged)
- def __indexChanged(self, index):
- self._model.timeRange = self.currentData()
- class SearchLanguage(QComboBox):
- def __init__(self, model, parent=None):
- QComboBox.__init__(self, parent=parent)
- self._model = model
- self._favorites = []
- self.setMinimumContentsLength(2)
- self.__itemModel = QStandardItemModel(self)
- self.setModel(self.__itemModel)
- self.currentIndexChanged.connect(self.__indexChanged)
- self.__itemModel.itemChanged.connect(self.__favCheckChanged)
- def __indexChanged(self, index):
- self._model.lang = self.currentData()
- def __favCheckChanged(self, item):
- lang = item.data(Qt.UserRole)
- index = item.row()
- newIndex = 0
- if item.checkState():
- # Language added to favorites.
- self._favorites.append(lang)
- else:
- # Remove language from favorites.
- langList = list(self._model.Languages.keys())
- newIndex = langList.index(lang)
- self._favorites.remove(lang)
- # Index offset
- for favLang in self._favorites:
- if langList.index(favLang) < newIndex:
- newIndex -= 1
- newIndex += len(self._favorites)
- self.__itemModel.takeRow(index)
- self.__itemModel.insertRow(newIndex, item)
- def populate(self):
- self.__itemModel.clear()
- for lang in self._model.Languages:
- newItem = QStandardItem(self._model.Languages[lang])
- newItem.setCheckable(True)
- newItem.setFlags(
- Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled
- )
- newItem.setData(QVariant(lang), Qt.UserRole)
- if lang in self._favorites:
- newItem.setData(Qt.Checked, Qt.CheckStateRole)
- self.__itemModel.insertRow(0, newItem)
- continue
- newItem.setData(Qt.Unchecked, Qt.CheckStateRole)
- self.__itemModel.appendRow(newItem)
- def loadSettings(self, data):
- for lang in data.get('favs', []):
- self._favorites.append(lang)
- self.populate()
- # Find and set the index that matches lang
- self.setCurrentIndex(
- self.findData(QVariant(data.get('lang', '')), Qt.UserRole)
- )
- def saveSettings(self):
- return {
- 'lang': str(self.currentData()),
- 'favs': self._favorites
- }
- class SearchOptionsContainer(QFrame):
- """ Custom QFrame to be able to show or hide certain widgets.
- """
- def __init__(self, searchModel, instancesModel, enginesModel, parent=None):
- """
- @param searchModel:
- @type searchModel: searxqt.models.search.SearchModel
- @param instancesModel:
- @type instancesModel: searxqt.models.instances.InstanceModelFilter
- @param enginesModel:
- @type enginesModel: searxqt.models.instances.EnginesModel
- @param parent:
- @type parent: QObject or None
- """
- QFrame.__init__(self, parent=parent)
- self._enginesModel = enginesModel
- self._searchModel = searchModel
- self._categoriesModel = CategoriesModel()
- self._instanceCategoriesModel = CategoriesModel()
- self._userCategoriesModel = UserCategoriesModel()
- # Backup user checked engines.
- self.__userCheckedBackup = []
- # Keep track of disabled engines (these engines are disabled
- # because they are part of one or more checked categories).
- #
- # @key: engine name (str)
- # @value : list with category keys (str)
- self.__disabledByCat = {}
- layout = QHBoxLayout(self)
- self._widgets = {
- 'categories': SearchCategories(
- self._categoriesModel,
- self._instanceCategoriesModel,
- enginesModel,
- self._userCategoriesModel,
- self
- ),
- 'engines': SearchEngines(searchModel, instancesModel, self),
- 'period': SearchPeriod(searchModel, self),
- 'lang': SearchLanguage(searchModel, self)
- }
- for widget in self._widgets.values():
- layout.addWidget(widget, 0, Qt.AlignTop)
- # Keep widgets left aligned.
- spacer = QSpacerItem(
- 40, 20, QSizePolicy.MinimumExpanding, QSizePolicy.Minimum
- )
- layout.addItem(spacer)
- # Connections
- self._categoriesModel.stateChanged.connect(
- self.__categoriesStateChanged
- )
- self._instanceCategoriesModel.stateChanged.connect(
- self.__categoriesStateChanged
- )
- self._userCategoriesModel.stateChanged.connect(
- self.__userCategoriesStateChanged
- )
- self._userCategoriesModel.changed.connect(self.__userCategoriesChanged)
- self._userCategoriesModel.removed.connect(self.__userCategoryRemoved)
- self._enginesModel.changed.connect(self.__enginesModelChanged)
- def __enginesModelChanged(self):
- """Settings loaded or data updated
- """
- # Remove deleted engines from __disabledByCat
- for engine in list(self.__disabledByCat.keys()):
- if engine not in self._enginesModel:
- del self.__disabledByCat[engine]
- self._widgets['engines'].setKeyEnabled(engine)
- # Add new categories
- for catKey in self._enginesModel.categories():
- if catKey not in self._categoriesModel:
- name = ""
- if catKey in self._searchModel.categories.types:
- # Default pre-defined categories are translatable
- name = self._searchModel.categories.types[catKey][0]
- self._categoriesModel.addCategory(catKey, name)
- else:
- name = catKey.capitalize()
- log.debug(f"Found non default category `{name}`", self)
- self._instanceCategoriesModel.addCategory(catKey, name)
- # Remove old categories
- for catKey in self._categoriesModel.copy():
- if catKey not in self._enginesModel.categories():
- self._categoriesModel.removeCategory(catKey)
- # Release potentialy checked engines
- self.__processCategoriesStateChange(
- [engine.name for engine in
- self._enginesModel.getByCategory(catKey)],
- catKey,
- False
- )
- # Remove old instance specific categories
- for catKey in self._instanceCategoriesModel.copy():
- if (catKey not in self._enginesModel.categories() and
- catKey not in self._categoriesModel):
- self._instanceCategoriesModel.removeCategory(catKey)
- # Release potentialy checked engines
- self.__processCategoriesStateChange(
- [engine.name for engine in
- self._enginesModel.getByCategory(catKey)],
- catKey,
- False
- )
- self._widgets['categories'].reGenerate()
- self.__finalizeCategoriesStateChange()
- def __userCategoryRemoved(self, catKey):
- for engineKey, catList in self.__disabledByCat.copy().items():
- if catKey in catList:
- self.__uncheckEngineByCat(catKey, engineKey)
- self._widgets['categories'].reGenerate()
- self.__finalizeCategoriesStateChange()
- def __userCategoriesChanged(self, catKey):
- """ When the user edited a existing user-category this should
- check freshly added engines to this category and uncheck engines
- that have been removed from this category.
- """
- if self._userCategoriesModel[catKey].isChecked():
- engines = self._userCategoriesModel[catKey].engines
- # Uncheck removed engines
- for engineKey, categories in self.__disabledByCat.copy().items():
- if catKey in categories:
- if engineKey not in engines:
- self.__uncheckEngineByCat(catKey, engineKey)
- # Check newly added engines
- for engineKey in engines:
- if engineKey not in self.__disabledByCat:
- self.__checkEngineByCat(catKey, engineKey)
- self.__finalizeCategoriesStateChange()
- def __checkEngineByCat(self, catKey, engineKey):
- """ This method handles checking of a engine by a category.
- @param catKey: Category key
- @type catKey: str
- @param engineKey: Engine key
- @type engineKey: str
- """
- if engineKey not in self._searchModel.engines:
- # User did not check this engine so we are going to.
- self._searchModel.engines.append(
- engineKey
- )
- elif(engineKey not in self.__userCheckedBackup and
- not self._widgets['engines'].keyDisabled(engineKey)):
- # User did check this engine, so we backup that.
- self.__userCheckedBackup.append(engineKey)
- if not self._widgets['engines'].keyDisabled(engineKey):
- # Disable the engine from being toggled by the user.
- self._widgets['engines'].setKeyDisabled(engineKey)
- if engineKey not in self.__disabledByCat:
- self.__disabledByCat.update({engineKey: []})
- # Backup that this category is blocking this engine from
- # being toggled by the user.
- self.__disabledByCat[engineKey].append(catKey)
- def __uncheckEngineByCat(self, catKey, engineKey):
- """ This method handles the unchecking of a engine by a category.
- @param catKey: Category key
- @type catKey: str
- @param engineKey: Engine key
- @type engineKey: str
- """
- if engineKey in self.__disabledByCat:
- if catKey in self.__disabledByCat[engineKey]:
- # This category no longer blocks this engine from
- # being edited by the user.
- self.__disabledByCat[engineKey].remove(catKey)
- if not self.__disabledByCat[engineKey]:
- # No category left that blocks this engine from
- # user-toggleing.
- self._widgets['engines'].setKeyEnabled(engineKey)
- self.__disabledByCat.pop(engineKey)
- if engineKey not in self.__userCheckedBackup:
- # User didn't check this engine, so we can
- # uncheck it.
- self._searchModel.engines.remove(
- engineKey
- )
- else:
- # User did check this engine before checking
- # this category so we won't uncheck it.
- self.__userCheckedBackup.remove(engineKey)
- def __userCategoriesStateChanged(self, catKey, state):
- """ The user checked or unchecked a user-category.
- @param catKey: Category key
- @type catKey: str
- @param state:Category enabled or disabled (checked or unchecked)
- @type state: bool
- """
- self.__processCategoriesStateChange(
- self._userCategoriesModel[catKey].engines,
- catKey,
- state
- )
- self._widgets['categories'].reGenerate()
- self.__finalizeCategoriesStateChange()
- def __categoriesStateChanged(self, catKey, state):
- """ The user checked or unchecked a default-category.
- @param catKey: Category key
- @type catKey: str
- @param state: Category enabled or disabled (checked or unchecked)
- @type state: bool
- """
- self.__processCategoriesStateChange(
- [engine.name for engine in
- self._enginesModel.getByCategory(catKey)],
- catKey,
- state
- )
- self._widgets['categories'].reGenerate()
- self.__finalizeCategoriesStateChange()
- def __processCategoriesStateChange(self, engines, catKey, state):
- """ The user checked or unchecked a category, depending on the
- `state` variable.
- When a category gets checked all the engines in that category
- will be checked and disabled so that the user can't toggle the
- engine.
- On uncheck of a category all engines in that category should be
- re-enabled. And those engines should be unchecked if they weren't
- checked by the user before checking this category.
- @param engines: A list with engineKeys (str) that are part of the
- category (catKey).
- @type engines: list
- @param catKey: Category key
- @type catKey: str
- @param state: Category enabled or disabled (checked or unchecked)
- @type state: bool
- """
- if state:
- # Category checked.
- for engine in engines:
- self.__checkEngineByCat(catKey, engine)
- else:
- # Category unchecked.
- for engine in engines:
- self.__uncheckEngineByCat(catKey, engine)
- def __finalizeCategoriesStateChange(self):
- # Re-generate the engines label
- self._widgets['engines'].reGenerate()
- # Update the instances filter.
- self._widgets['engines'].updateFilter()
- def saveSettings(self):
- data = {}
- # Store widgets visible state.
- for key, widget in self._widgets.items():
- data.update({
- f"{key}Visible": not widget.isHidden()
- })
- # Store category states and CheckboxOptionsButton states (label
- # expanded or collapsed)
- data.update({
- 'userCatModel': self._userCategoriesModel.data(),
- 'defaultCatModel': self._categoriesModel.data(),
- 'categoriesButton': self._widgets['categories'].saveSettings(),
- 'enginesButton': self._widgets['engines'].saveSettings(),
- 'language': self._widgets['lang'].saveSettings()
- })
- return data
- def loadSettings(self, data):
- # Set widgets visible or hidden depending on their state.
- for key, widget in self._widgets.items():
- if data.get(f"{key}Visible", True):
- widget.show()
- else:
- widget.hide()
- # Load category states
- self._userCategoriesModel.setData(data.get('userCatModel', {}))
- self._categoriesModel.setData(data.get('defaultCatModel', {}))
- # Load CheckboxOptionsButton states (categories and engines label
- # states, expanded or collapsed.)
- self._widgets['categories'].loadSettings(
- data.get('categoriesButton', {})
- )
- self._widgets['engines'].loadSettings(data.get('enginesButton', {}))
- # Load search language.
- self._widgets['lang'].loadSettings(data.get('language', {}))
- def __checkBoxStateChanged(self, key, state):
- if state:
- self._widgets[key].show()
- else:
- self._widgets[key].hide()
- """ QFrame re-implementations
- """
- def contextMenuEvent(self, event):
- menu = QMenu(self)
- menu.addSection(_("Show / Hide"))
- for key, widget in self._widgets.items():
- action = QWidgetAction(menu)
- checkbox = QCheckBox(key, menu)
- checkbox.setTristate(False)
- checkbox.setChecked(not widget.isHidden())
- action.setDefaultWidget(checkbox)
- checkbox.stateChanged.connect(
- lambda state, key=key:
- self.__checkBoxStateChanged(key, state)
- )
- menu.addAction(action)
- menu.exec_(self.mapToGlobal(event.pos()))
- """ Find text in the search results
- Shortcuts:
- - 'Return' find text.
- - 'Shift+Return' find previous text.
-
- """
- class ResultSearcher(QWidget):
- closeRequest = pyqtSignal()
- def __init__(self, resultsContainer, parent):
- QWidget.__init__(self, parent)
- self.__resultsContainer = resultsContainer
- layout = QHBoxLayout(self)
- self.__inputEdit = QLineEdit(self)
- self.__inputEdit.setPlaceholderText(_("Find .."))
- self.__inputEdit.installEventFilter(self)
- self.__caseCheckbox = QCheckBox(_("Case sensitive"), self)
- self.__wholeCheckbox = QCheckBox(_("Whole words"), self)
- nextButton = Button("►", self)
- prevButton = Button("◄", self)
- closeButton = Button("X", self)
- layout.addWidget(self.__inputEdit)
- layout.addWidget(prevButton)
- layout.addWidget(nextButton)
- layout.addWidget(self.__caseCheckbox)
- layout.addWidget(self.__wholeCheckbox)
- layout.addWidget(closeButton)
- closeButton.clicked.connect(self.closeRequest)
- nextButton.clicked.connect(self.__search)
- prevButton.clicked.connect(self.__searchPrev)
- def eventFilter(self, source, event):
- if event.type() == QEvent.KeyPress and source is self.__inputEdit:
- if event.key() == Qt.Key_Return and event.modifiers() == Qt.ShiftModifier:
- self.__search(reverse=True)
- elif event.key() == Qt.Key_Return:
- self.__search()
- elif event.key() == Qt.Key_Escape:
- self.closeRequest.emit()
- return QLineEdit.eventFilter(self, source, event)
- def focusInput(self):
- self.__inputEdit.setFocus()
- def __searchPrev(self):
- self.__search(reverse=True)
- def __search(self, reverse=False):
- text = self.__inputEdit.text()
- flags = QTextDocument.FindFlag(0)
- if reverse:
- flags |= QTextDocument.FindBackward
- if self.__caseCheckbox.isChecked():
- flags |= QTextDocument.FindCaseSensitively
- if self.__wholeCheckbox.isChecked():
- flags |= QTextDocument.FindWholeWords
- self.__resultsContainer.find(text, options=flags)
- class SearchContainer(QWidget):
- def __init__(self, searchModel, instancesModel,
- instanceSelecter, enginesModel, guard, parent=None):
- """
- @type searchModel: models.search.SearchModel
- @type instancesModel: models.instances.InstancesModelFilter
- @type instanceSelecter: models.instances.InstanceSelecterModel
- @type enginesModel: models.instances.EnginesModel
- @type guard: core.guard.Guard
- """
- QWidget.__init__(self, parent=parent)
- layout = QVBoxLayout(self)
- self._model = searchModel
- self._instancesModel = instancesModel
- self._instanceSelecter = instanceSelecter
- self._guard = guard
- self._searchThread = None
- # Maximum other instances to try on fail.
- self._maxSearchFailCount = 10
- # Set `_useFallback` to True to try another instance when the
- # search failed somehow or set to False to try same instance or
- # for pagination which also should use the same instance.
- self._useFallback = True
- # `_fallbackActive` should be False when a fresh list of fallback
- # instances should be picked on failed search and should be True
- # when search(es) fail and `_fallbackInstancesQueue` is beeing
- # used.
- self._fallbackActive = False
- # Used to store errors when fallback is active so they can be listed
- # to the user when even the fallback fails.
- self._fallbackErrors = []
- # Every first request that has `_useFallback` set to True will
- # use this list as a resource for next instance(s) to try until
- # it is out of instance url's. Also on the first request this
- # list will be cleared and filled again with `_maxSearchFailCount`
- # of random instances.
- self._fallbackInstancesQueue = []
- # Set to True to break out of the fallback loop.
- # This is used for the Stop action.
- self._breakFallback = False
- searchLayout = QHBoxLayout()
- layout.addLayout(searchLayout)
- # -- Start search bar
- self.queryEdit = QLineEdit(self)
- self.queryEdit.setPlaceholderText(_("Search for .."))
- searchLayout.addWidget(self.queryEdit)
- self.searchButton = Button(_("Searx"), self)
- self.searchButton.setToolTip(_("Preform search."))
- searchLayout.addWidget(self.searchButton)
- self.reloadButton = Button("♻", self)
- self.reloadButton.setToolTip(_("Reload"))
- searchLayout.addWidget(self.reloadButton)
- self.randomButton = Button("⤳", self)
- self.randomButton.setToolTip(_(
- "Search with random instance.\n"
- "(Obsolete when 'Random Every is checked')"
- ))
- searchLayout.addWidget(self.randomButton)
- rightLayout = QVBoxLayout()
- rightLayout.setSpacing(0)
- searchLayout.addLayout(rightLayout)
- self._fallbackCheck = QCheckBox(_("Fallback"), self)
- self._fallbackCheck.setToolTip(_("Try random other instance on fail."))
- rightLayout.addWidget(self._fallbackCheck)
- self._randomCheckEvery = QCheckBox(_("Random every"), self)
- self._randomCheckEvery.setToolTip(_("Pick a random instance for "
- "every request."))
- rightLayout.addWidget(self._randomCheckEvery)
- # -- End search bar
- # -- Start splitter
- self.splitter = QSplitter(self)
- self.splitter.setOrientation(Qt.Vertical)
- layout.addWidget(self.splitter)
- # ---- Start search options toolbar
- self._optionsContainer = SearchOptionsContainer(
- searchModel, instancesModel, enginesModel, self.splitter
- )
- # ---- End search options toolbar
- # --- Start search results container
- searchBottomWidget = QWidget(self.splitter)
- self.searchBottomWidgetLayout = QVBoxLayout(searchBottomWidget)
- self.resultsContainer = QTextBrowser(self)
- self.resultsContainer.setOpenLinks(False)
- self.resultsContainer.setOpenExternalLinks(False)
- self.resultsContainer.setLineWrapMode(1)
- self.searchBottomWidgetLayout.addWidget(self.resultsContainer)
- self.navBar = SearchNavigation(self)
- self.navBar.setEnabled(False)
- self.searchBottomWidgetLayout.addWidget(self.navBar)
- # --- End search results container
- # -- End splitter
- # Find text
- self.__findShortcut = QShortcut('Ctrl+F', self);
- self.__findShortcut.activated.connect(self.__openFind);
- self.__resultsSearcher = None
- self.queryEdit.textChanged.connect(self.__queryChanged)
- self._model.statusChanged.connect(self.__searchStatusChanged)
- self._model.optionsChanged.connect(self.__searchOptionsChanged)
- self._fallbackCheck.stateChanged.connect(self.__useFallbackChanged)
- self._randomCheckEvery.stateChanged.connect(
- self.__randomEveryRequestChanged)
- self._instanceSelecter.instanceChanged.connect(
- self.__instanceChanged)
- self.queryEdit.returnPressed.connect(self.__searchButtonClicked)
- self.searchButton.clicked.connect(self.__searchButtonClicked)
- self.reloadButton.clicked.connect(self.__reloadButtonClicked)
- self.randomButton.clicked.connect(self.__randomSearchButtonClicked)
- self.navBar.requestPage.connect(self.__navBarRequest)
- self.resultsContainer.anchorClicked.connect(self.__handleAnchorClicked)
- self.__queryChanged("")
- def isBusy(self):
- return bool(self._searchThread is not None)
- def cancelAll(self):
- self._breakFallback = True
- self._searchThread.wait()
- def reset(self):
- self._model.reset()
- self.resultsContainer.setHtml("")
- self.resultsContainer.clearHistory()
- def __openFind(self):
- if self.__resultsSearcher is None:
- self.__resultsSearcher = ResultSearcher(self.resultsContainer, self)
- self.searchBottomWidgetLayout.insertWidget(1, self.__resultsSearcher)
- self.__resultsSearcher.closeRequest.connect(self.__closeFind)
- self.__resultsSearcher.focusInput()
- def __closeFind(self):
- self.searchBottomWidgetLayout.removeWidget(self.__resultsSearcher)
- self.__resultsSearcher.deleteLater()
- self.__resultsSearcher = None
- def __handleAnchorClicked(self, url):
- scheme = url.scheme()
- # Internal from sugestions/corrections
- if scheme == 'search':
- self.queryEdit.setText(url.path())
- self._newSearch(self._instanceSelecter.currentUrl)
- return
- # Custom command
- if (AnchorCMD.handle(scheme, url.toString())):
- return # Handled by custom command
- # Let QDesktopServices handle known schemes
- if scheme in ['http', 'https', 'magnet', 'ftp']:
- QDesktopServices.openUrl(url)
- # Unknown schemes are not doing anything .. :-)
- def __searchButtonClicked(self, checked=0):
- # Set to use fallback
- self._useFallback = self._model.useFallback
- if (self._model.randomEvery or
- (self._useFallback and
- not self._instanceSelecter.currentUrl)):
- self._instanceSelecter.randomInstance()
- self._resetPagination()
- self._newSearch(self._instanceSelecter.currentUrl)
- def __stopButtonClicked(self):
- self._breakFallback = True
- self.searchButton.setEnabled(False)
- def __reloadButtonClicked(self):
- self._useFallback = False
- self._newSearch(self._instanceSelecter.currentUrl)
- def __randomSearchButtonClicked(self):
- self._useFallback = self._model.useFallback
- self._instanceSelecter.randomInstance()
- self._newSearch(self._instanceSelecter.currentUrl)
- def __navBarRequest(self, pageNo):
- self._useFallback = False
- self._model.pageno = pageNo
- self._newSearch(self._instanceSelecter.currentUrl)
- def _resetPagination(self):
- self.navBar.reset()
- self._model.pageno = 1
- def __instanceChanged(self):
- self._resetPagination()
- def __searchOptionsChanged(self):
- """ From the model (on load settings)
- """
- self._randomCheckEvery.stateChanged.disconnect(
- self.__randomEveryRequestChanged)
- self._fallbackCheck.stateChanged.disconnect(
- self.__useFallbackChanged)
- self._randomCheckEvery.setChecked(self._model.randomEvery)
- self._fallbackCheck.setChecked(self._model.useFallback)
- self.__handleRandomButtonState()
- self._randomCheckEvery.stateChanged.connect(
- self.__randomEveryRequestChanged)
- self._fallbackCheck.stateChanged.connect(self.__useFallbackChanged)
- def __handleRandomButtonState(self):
- """ Hides or shows the 'Random search button'.
- We don't need the button when the model it's randomEvery is True.
- """
- if self._model.randomEvery:
- self.randomButton.hide()
- else:
- self.randomButton.show()
- def __randomEveryRequestChanged(self, state):
- """ From the checkbox
- """
- self._model.optionsChanged.disconnect(self.__searchOptionsChanged)
- self._model.randomEvery = bool(state)
- self.__handleRandomButtonState()
- self._model.optionsChanged.connect(self.__searchOptionsChanged)
- def __useFallbackChanged(self, state):
- """ From the checkbox
- """
- self._model.optionsChanged.disconnect(self.__searchOptionsChanged)
- self._model.useFallback = bool(state)
- self._model.optionsChanged.connect(self.__searchOptionsChanged)
- def __queryChanged(self, q):
- if self._model.status() == SearchStatus.Busy:
- return
- if q:
- self.searchButton.setEnabled(True)
- self.reloadButton.setEnabled(True)
- self.randomButton.setEnabled(True)
- else:
- self.searchButton.setEnabled(False)
- self.reloadButton.setEnabled(False)
- self.randomButton.setEnabled(False)
- def _setOptionsState(self, state=True):
- if self._useFallback:
- if state:
- # Search stopped
- self.searchButton.setText(_("Search"))
- self.searchButton.clicked.disconnect(
- self.__stopButtonClicked
- )
- self.searchButton.clicked.connect(self.__searchButtonClicked)
- self.searchButton.setEnabled(True)
- else:
- # Searching
- self.searchButton.setText(_("Stop"))
- self.searchButton.clicked.disconnect(
- self.__searchButtonClicked
- )
- self.searchButton.clicked.connect(self.__stopButtonClicked)
- else:
- self.searchButton.setEnabled(state)
- self.reloadButton.setEnabled(state)
- self.randomButton.setEnabled(state)
- self._randomCheckEvery.setEnabled(state)
- self._fallbackCheck.setEnabled(state)
- self.queryEdit.setEnabled(state)
- self._optionsContainer.setEnabled(state)
- def __searchStatusChanged(self, status):
- if status == SearchStatus.Busy:
- self._setOptionsState(False)
- elif status == SearchStatus.Done:
- self._setOptionsState()
- @property
- def query(self): return self.queryEdit.text()
- def _newSearch(self, url, query=''):
- self.resultsContainer.clear()
- self._search(url, query)
- def _search(self, url, query=''):
- if self._searchThread:
- return
- if not query:
- query = self.query
- if not query:
- self.resultsContainer.setHtml(_("Please enter a search query."))
- return
- if not url:
- self.resultsContainer.setHtml(_("Please select a instance first."))
- return
- self.navBar.setEnabled(False)
- self._model.url = url
- self._model.query = query
- self._searchThread = Thread(
- self._model.search,
- args=[self._model],
- parent=self
- )
- self._searchThread.finished.connect(self._searchFinished)
- self._searchThread.start()
- def _searchFailed(self, result):
- currentUrl = self._instanceSelecter.currentUrl # backup
- # Don't go further on proxy errors.
- # - Guard should not handle proxy errors.
- # - Fallback should be disabled for proxy errors.
- if result.errorType() == ErrorType.ProxyError:
- self._breakFallback = False
- self.resultsContainer.setHtml(
- FailedResponsesHtml.create([result], Themes.htmlCssFail)
- )
- return
- if self._guard.isEnabled():
- # See if the Guard has any consequence for this instance.
- consequence = self._guard.getConsequence(currentUrl)
- if consequence:
- # Apply the consequence.
- if consequence.type == ConsequenceType.Blacklist:
- # Blacklist the instance.
- self._instancesModel.putInstanceOnBlacklist(
- currentUrl,
- reason=result.error()
- )
- else:
- # Put the instance on a timeout.
- self._instancesModel.putInstanceOnTimeout(
- currentUrl,
- duration=consequence.duration,
- reason=result.error()
- )
- self._instancesModel.apply() # Apply the changed filter.
- if self._useFallback: # Re-try another instance
- if self._breakFallback: # Stop button pressed
- self._breakFallback = False
- self._fallbackActive = False
- self._fallbackErrors.append(result)
- self.resultsContainer.setHtml(
- FailedResponsesHtml.create(
- self._fallbackErrors,
- f"{_('Max fail count reached!')} " \
- f"({self._maxSearchFailCount})",
- Themes.htmlCssFail)
- )
- self._fallbackErrors.clear()
- return
- if not self._fallbackActive:
- # Get new list with instances to try same request.
- self._fallbackActive = True
- self._fallbackErrors.clear()
- self._fallbackInstancesQueue.clear()
- self._fallbackInstancesQueue = (
- self._instanceSelecter.getRandomInstances(
- amount=self._maxSearchFailCount))
- if not self._fallbackInstancesQueue:
- self.resultsContainer.setHtml(
- FailedResponsesHtml.create(
- self._fallbackErrors,
- f"{_('Max fail count reached!')} " \
- f"({self._maxSearchFailCount})",
- Themes.htmlCssFail)
- )
- self._fallbackActive = False
- self._fallbackErrors.clear()
- return
- # Append current error to error list
- self._fallbackErrors.append(result)
- # Set next instance url to try.
- self._instanceSelecter.currentUrl = (
- self._fallbackInstancesQueue.pop(0))
- self._search(self._instanceSelecter.currentUrl)
- return
- if self._model.pageno > 1:
- self.navBar.setEnabled(True)
- self.navBar.setNextEnabled(False)
- self.resultsContainer.setHtml(
- FailedResponsesHtml.create([result], _("Search request failed."), Themes.htmlCssFail)
- )
- def _searchFinished(self):
- result = self._searchThread.result()
- self._clearSearchThread()
- # Guard
- if self._guard.isEnabled():
- currentUrl = self._instanceSelecter.currentUrl
- # Report the search result to Guard.
- self._guard.reportSearchResult(currentUrl, result)
- if not bool(result): # Failed
- self._searchFailed(result)
- return
- self._fallbackActive = False
- self.resultsContainer.setHtml(
- ResultsHtml.create(result.json(), Themes.htmlCssResults)
- )
- self.navBar.setEnabled(True)
- self.navBar.setNextEnabled(True)
- def _clearSearchThread(self):
- self._searchThread.finished.disconnect(self._searchFinished)
- # Wait before deleting because the `finished` signal is emited
- # from the thread itself, so this method could be called before the
- # thread is actually finished and result in a crash.
- self._searchThread.wait()
- self._searchThread.deleteLater()
- self._searchThread = None
- def saveSettings(self):
- return {
- 'searchOptions': self._optionsContainer.saveSettings(),
- 'splitterState': self.splitter.saveState()
- }
- def loadSettings(self, data):
- self.queryEdit.setText("")
- self._optionsContainer.loadSettings(
- data.get('searchOptions', {})
- )
- self.splitter.restoreState(
- data.get('splitterState', QByteArray())
- )
|