123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- ########################################################################
- # Searx-qt - Lightweight desktop application for SearX.
- # Copyright (C) 2020 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,
- QSpacerItem,
- QSizePolicy
- )
- from PyQt5.QtCore import pyqtSignal, QThread, Qt, QVariant
- from searxqt.models.search import SearchStatus
- from searxqt.widgets.buttons import Button, CheckboxOptionsButton
- class Thread(QThread):
- finished = pyqtSignal()
- def __init__(self, func, args=[], kwargs={}, parent=None):
- QThread.__init__(self, parent=parent)
- self._func = func
- self._args = args
- self._kwargs = kwargs
- self._result = None
- def result(self): return self._result
- def run(self):
- self._result = self._func(*self._args, **self._kwargs)
- self.finished.emit()
- 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
- )
- """ Below are re-implementations.
- """
- def getCheckedOptionNames(self):
- """ Should return a list with checked option names.
- @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.enabled and engine.name not in tmp:
- state = engine.name in self.getCheckedOptionNames()
- 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._instancesModel.updateKwargs(
- {'engines': self.getCheckedOptionNames()}
- )
- class SearchCategories(CheckboxOptionsButton):
- def __init__(self, searchModel, parent=None):
- self._searchModel = searchModel
- CheckboxOptionsButton.__init__(
- self,
- labelName="Categories",
- parent=parent
- )
- """ Below are re-implementations.
- """
- def getCheckedOptionNames(self):
- """ Should return a list with checked option names.
- @return: should return a list with strings.
- @rtype: list
- """
- return(
- [value[0] for value in
- self._searchModel.categories.types.values()]
- )
- def getOptions(self):
- """ Should return a list with options tuple(key, name, state)
- This will be used to generate the options.
- """
- list_ = []
- for key, t in self._searchModel.categories.types.items():
- state = self._searchModel.categories.get(key)
- list_.append((key, t[0], state))
- return list_
- def optionToggled(self, key, state):
- self._searchModel.categories.set(key, state)
- 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.setMinimumContentsLength(2)
- for lang in model.Languages:
- self.addItem(model.Languages[lang], QVariant(lang))
- self.currentIndexChanged.connect(self.__indexChanged)
- def __indexChanged(self, index):
- self._model.lang = self.currentData()
- class SearchOptionsContainer(QFrame):
- """ Custom QFrame to be able to show or hide certain widgets.
- """
- def __init__(self, searchModel, instancesModel, parent=None):
- QFrame.__init__(self, parent=parent)
- layout = QHBoxLayout(self)
- self._widgets = {
- 'categories': SearchCategories(searchModel, 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.Expanding, QSizePolicy.Minimum
- )
- layout.addItem(spacer)
- def saveSettings(self):
- data = {}
- for key, widget in self._widgets.items():
- data.update(
- {
- '{0}Visible'.format(key): not widget.isHidden()
- }
- )
- return data
- def loadSettings(self, data):
- for key, widget in self._widgets.items():
- if data.get('{0}Visible'.format(key), True):
- widget.show()
- else:
- widget.hide()
- 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()))
- class SearchContainer(QWidget):
- def __init__(self, searchModel, instancesModel,
- instanceSelecter, parent=None):
- """
- @type searchModel: models.search.SearchModel
- @type instancesModel: models.instances.InstancesModelFilter
- @type instanceSelecter: models.instances.instanceSelecterModel
- """
- QWidget.__init__(self, parent=parent)
- layout = QVBoxLayout(self)
- self._model = searchModel
- self._instancesModel = instancesModel
- self._instanceSelecter = instanceSelecter
- self._searchThread = None
- # Current fail count.
- self._searchFailCount = 0
- # 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
- # 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 search options toolbar
- self._optionsContainer = SearchOptionsContainer(
- searchModel, instancesModel, self
- )
- layout.addWidget(self._optionsContainer)
- # -- End search options toolbar
- self.resultsContainer = QTextBrowser(self)
- self.resultsContainer.setOpenExternalLinks(True)
- self.resultsContainer.setLineWrapMode(1)
- layout.addWidget(self.resultsContainer)
- self.navBar = SearchNavigation(self)
- self.navBar.setEnabled(False)
- layout.addWidget(self.navBar)
- 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.__queryChanged("")
- 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
- kwargs = {
- 'requestKwargs': {
- 'data': {
- 'q': query,
- 'format': 'json'
- },
- }
- }
- kwargs['requestKwargs'].update(self._model.requestSettings.data)
- self._searchThread = Thread(self._model.search, kwargs=kwargs,
- parent=self)
- self._searchThread.finished.connect(self._searchFinished)
- self._searchThread.start()
- def _searchFailed(self, result):
- if self._useFallback: # Re-try another instance
- if self._breakFallback: # Stop button pressed
- self._breakFallback = False
- return
- if not self._fallbackActive:
- # Get new list with instances to try same request.
- self._fallbackActive = True
- self._fallbackInstancesQueue.clear()
- self._fallbackInstancesQueue = (
- self._instanceSelecter.getRandomInstances(
- amount=self._maxSearchFailCount))
- if not self._fallbackInstancesQueue:
- self.resultsContainer.setHtml(
- "Max fail count reached! ({0})".format(
- self._maxSearchFailCount))
- self._fallbackActive = False
- return
- # 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("Search failed: {} {}"
- .format(result.errorType(),
- result.error()))
- def _searchFinished(self):
- result = self._searchThread.result()
- self._clearSearchThread()
- if not bool(result): # Failed
- self._searchFailed(result)
- return
- self._fallbackActive = False
- # Create HTML from results
- elemStr = ""
- for item in result.json().get('results', {}):
- elemStr += self.createResultElement(item)
- self.resultsContainer.setHtml(self.compileHtml(elemStr))
- self.navBar.setEnabled(True)
- self.navBar.setNextEnabled(True)
- def _clearSearchThread(self):
- self._searchThread.finished.disconnect(self._searchFinished)
- self._searchThread.deleteLater()
- self._searchThread = None
- def createResultElement(self, data):
- """result['title']
- result['content']
- result['url']
- result['engine']
- """
- elem = """<div class="result" id="result-{id}">
- <h4 class="result-title"><i>{engine}: </i><a href="{url}">{title}</a></h4>
- <p class="result-description">{content}</p>
- <p class="result-url">{url}</p>
- </div>""".format(id="TODO-id", title=data.get('title', ''),
- url=data.get('url', ''), content=data.get('content', ''),
- engine=data.get('engine', '?'))
- return elem
- def compileHtml(self, elemStr=""):
- return """<html>
- <head>
- <title>Yeah</title>
- </head>
- <body>
- {0}
- </body>
- </html>""".format(elemStr)
- def saveSettings(self):
- return {
- 'searchOptions': self._optionsContainer.saveSettings()
- }
- def loadSettings(self, data):
- self._optionsContainer.loadSettings(
- data.get('searchOptions', {})
- )
|