123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- ########################################################################
- # 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/>.
- #
- ########################################################################
- import sys
- from copy import deepcopy
- from PyQt5.QtWidgets import (
- QApplication,
- QAction,
- QSplitter,
- QMenuBar,
- QMenu,
- QMainWindow,
- QHBoxLayout,
- QVBoxLayout,
- QWidget,
- QStatusBar,
- QLabel,
- QMessageBox
- )
- from PyQt5.QtCore import (
- Qt,
- QSettings,
- QByteArray,
- QSize
- )
- from searxqt.models.instances import (
- Stats2InstancesModel,
- UserInstancesModel,
- Stats2EnginesModel,
- UserEnginesModel,
- Stats2Model,
- InstanceModelFilter,
- InstanceSelecterModel,
- PersistentInstancesModel,
- PersistentEnginesModel,
- InstancesModelTypes
- )
- from searxqt.models.settings import SettingsModel
- from searxqt.models.search import (
- SearchModel,
- UserInstancesHandler,
- SearchStatus
- )
- from searxqt.models.profiles import Profiles, ProfileItem
- from searxqt.views.instances import InstancesView
- from searxqt.views.settings import SettingsWindow
- from searxqt.views.search import SearchContainer
- from searxqt.views import about
- from searxqt.views.profiles import ProfileChooserDialog
- from searxqt.core.customAnchorCmd import AnchorCMD
- from searxqt.core.requests import RequestsHandler
- from searxqt.core.guard import Guard
- from searxqt.translations import _
- from searxqt.version import __version__
- from searxqt import PROFILES_PATH, SETTINGS_PATH
- from searxqt.core import log
- class MainWindow(QMainWindow):
- def __init__(self, *args, **kwargs):
- QMainWindow.__init__(self, *args, **kwargs)
- self.setWindowTitle("Searx-Qt")
- self._handler = None
- self._settingsWindow = None
- # Request handler
- self._requestHandler = RequestsHandler()
- self._settingsModel = SettingsModel(self._requestHandler.settings, self)
- # Persistent models
- self._persistantInstancesModel = PersistentInstancesModel()
- self._persistantEnginesModel = PersistentEnginesModel()
- # Profiles
- self._profiles = Profiles()
- self.instanceFilter = InstanceModelFilter(
- self._persistantInstancesModel, self
- )
- self.instanceSelecter = InstanceSelecterModel(self.instanceFilter)
- self._searchModel = SearchModel(self._requestHandler, self)
- # Guard
- self._guard = Guard()
- # -- Menu bar
- menubar = QMenuBar(self)
- # Menu file
- menuFile = QMenu(menubar)
- menuFile.setTitle(_("File"))
- saveAction = QAction(_("Save"), menuFile)
- menuFile.addAction(saveAction)
- saveAction.setShortcut('Ctrl+S')
- saveAction.triggered.connect(self.saveSettings)
- actionExit = QAction(_("Exit"), menuFile)
- menuFile.addAction(actionExit)
- actionExit.setShortcut('Ctrl+Q')
- actionExit.triggered.connect(self.close)
- menubar.addAction(menuFile.menuAction())
- # Menu settings
- settingsAction = QAction(_("Settings"), menubar)
- menubar.addAction(settingsAction)
- settingsAction.triggered.connect(self._openSettingsWindow)
- # Menu profiles
- profilesAction = QAction(_("Profiles"), menubar)
- menubar.addAction(profilesAction)
- profilesAction.triggered.connect(self._openProfileChooser)
- # Menu about dialog
- aboutAction = QAction(_("About"), menubar)
- menubar.addAction(aboutAction)
- aboutAction.triggered.connect(self._openAboutDialog)
- self.setMenuBar(menubar)
- # -- End menu bar
- # -- Status bar
- self.statusBar = QStatusBar(self)
- statusWidget = QWidget(self)
- statusLayout = QHBoxLayout(statusWidget)
- self._handlerThreadStatusLabel = QLabel(self)
- statusLayout.addWidget(self._handlerThreadStatusLabel)
- self._statusInstanceLabel = QLabel(self)
- statusLayout.addWidget(self._statusInstanceLabel)
- self.statusBar.addPermanentWidget(statusWidget)
- self.setStatusBar(self.statusBar)
- # -- End status bar
- centralWidget = QWidget(self)
- layout = QVBoxLayout(centralWidget)
- self.setCentralWidget(centralWidget)
- self.splitter = QSplitter(centralWidget)
- self.splitter.setOrientation(Qt.Horizontal)
- layout.addWidget(self.splitter)
- self.searchContainer = SearchContainer(
- self._searchModel,
- self.instanceFilter,
- self.instanceSelecter,
- self._persistantEnginesModel,
- self._guard,
- self.splitter
- )
- self.instancesWidget = InstancesView(
- self.instanceFilter,
- self.instanceSelecter,
- self.splitter
- )
- self.instanceSelecter.instanceChanged.connect(self.__instanceChanged)
- self._searchModel.statusChanged.connect(self.__searchStatusChanged)
- self.__profileChooserInit()
- self.resize(800, 600)
- self.loadSharedSettings()
- self.loadProfile()
- def __instanceChanged(self, url):
- self._statusInstanceLabel.setText(f"<b>{_('Instance')}:</b> {url}")
- # Disable/enable instances view on search status change.
- def __searchStatusChanged(self, status):
- if status == SearchStatus.Busy:
- self.instancesWidget.setEnabled(False)
- else:
- self.instancesWidget.setEnabled(True)
- def closeEvent(self, event=None):
- # Disable everything.
- self.setEnabled(False)
- # Wait till all threads finished
- if self.searchContainer.isBusy():
- log.info("- Waiting for search thread to finish...", self)
- self.searchContainer.cancelAll()
- log.info("- Search thread finished.")
- if self._handler and self._handler.hasActiveJobs():
- log.info(
- "- Waiting for update instances thread to finish...",
- self
- )
- self._handler.cancelAll()
- log.info("- Instances update thread finished.", self)
- self.saveSettings()
- log.info("- Settings saved.", self)
- # Remove currently active profile id from the active list.
- self._profiles.setProfile(
- self._profiles.settings(),
- ProfileItem()
- )
- QApplication.closeAllWindows()
- log.info("Bye!", self)
- def _openAboutDialog(self):
- about.show(self)
- def __execProfileChooser(self):
- """ This only sets the profile, it does not load it.
- Returns True on success, False when something went wrong.
- """
- profiles = self._profiles
- profilesSettings = profiles.settings()
- profiles.loadProfiles(profilesSettings) # read profiles.conf
- dialog = ProfileChooserDialog(profiles)
- if dialog.exec():
- currentProfile = profiles.current()
- selectedProfile = dialog.selectedProfile()
- # Save current profile if one is set.
- if currentProfile.id:
- self.saveProfile()
- profiles.setProfile(
- profilesSettings,
- selectedProfile
- )
- else:
- self.__finalizeProfileChooser(dialog)
- return False
- self.__finalizeProfileChooser(dialog)
- return True
- def __profileChooserInit(self):
- profiles = self._profiles
- profilesSettings = profiles.settings()
- profiles.loadProfiles(profilesSettings) # read profiles.conf
- activeProfiles = profiles.getActiveProfiles(profilesSettings)
- defaultProfile = profiles.default()
- if defaultProfile is None or defaultProfile.id in activeProfiles:
- if not self.__execProfileChooser():
- sys.exit()
- else:
- # Load default profile.
- profiles.setProfile(
- profilesSettings,
- defaultProfile
- )
- def _openProfileChooser(self):
- if self._handler and self._handler.hasActiveJobs():
- QMessageBox.information(
- self,
- _("Instances update thread active"),
- _("Please wait until instances finished updating before\n"
- "switching profiles.")
- )
- return
- if self.__execProfileChooser():
- self.loadProfile()
- def __finalizeProfileChooser(self, dialog):
- """ Profiles may have been added or removed.
- - Store profiles.conf
- - Remove removed profile conf files
- """
- self._profiles.saveProfiles()
- self._profiles.removeProfileFiles(dialog.removedProfiles())
- def _openSettingsWindow(self):
- if not self._settingsWindow:
- self._settingsWindow = SettingsWindow(
- self._settingsModel,
- self._searchModel,
- self._guard
- )
- self._settingsWindow.resize(self.__lastSettingsWindowSize)
- self._settingsWindow.show()
- self._settingsWindow.activateWindow() # Bring it to front
- self._settingsWindow.closed.connect(self._delSettingsWindow)
- def _delSettingsWindow(self):
- self._settingsWindow.closed.disconnect()
- self._settingsWindow.deleteLater()
- self._settingsWindow = None
- def __handlerThreadChanged(self):
- self._handlerThreadStatusLabel.setText(
- self._handler.currentJobStr()
- )
- def loadProfile(self):
- profile = self._profiles.current()
- self.setWindowTitle(f"Searx-Qt - {profile.name}")
- profileSettings = QSettings(PROFILES_PATH, profile.id, self)
- # Clean previous stuff
- if self._handler:
- self._handler.threadStarted.disconnect(
- self.__handlerThreadChanged
- )
- self._handler.threadFinished.disconnect(
- self.__handlerThreadChanged
- )
- self._handler.deleteLater()
- self._handler = None
- if self._settingsWindow:
- self._delSettingsWindow()
- self.searchContainer.reset()
- # Set new models
- if profile.type == InstancesModelTypes.Stats2:
- self._handler = Stats2Model(self._requestHandler, self)
- instancesModel = Stats2InstancesModel(self._handler, parent=self)
- enginesModel = Stats2EnginesModel(self._handler, parent=self)
- self._settingsModel.loadSettings(
- profileSettings.value('settings', dict(), dict),
- stats2=True
- )
- elif profile.type == InstancesModelTypes.User:
- self._handler = UserInstancesHandler(self._requestHandler, self)
- instancesModel = UserInstancesModel(self._handler, parent=self)
- enginesModel = UserEnginesModel(self._handler, parent=self)
- self._settingsModel.loadSettings(
- profileSettings.value('settings', dict(), dict),
- stats2=False
- )
- else:
- print(f"ERROR unknown profile type '{profile.type}'")
- sys.exit(1)
- self._persistantInstancesModel.setModel(instancesModel)
- self._persistantEnginesModel.setModel(enginesModel)
- self._handler.setData(
- profileSettings.value('data', dict(), dict)
- )
- self._handler.threadStarted.connect(self.__handlerThreadChanged)
- self._handler.threadFinished.connect(self.__handlerThreadChanged)
- # Load settings
- self.instanceFilter.loadSettings(
- profileSettings.value('instanceFilter', dict(), dict)
- )
- self.instancesWidget.loadSettings(
- profileSettings.value('instancesView', dict(), dict)
- )
- self.instanceSelecter.loadSettings(
- profileSettings.value('instanceSelecter', dict(), dict)
- )
- self.searchContainer.loadSettings(
- profileSettings.value('searchContainer', dict(), dict)
- )
- self._searchModel.loadSettings(
- profileSettings.value('searchModel', dict(), dict)
- )
- # Guard
- self._guard.deserialize(profileSettings.value('Guard', dict(), dict))
- # Load main window splitter state (between search and instances)
- self.splitter.restoreState(
- profileSettings.value('splitterState', QByteArray(), QByteArray)
- )
- # Custom Anchor commands
- AnchorCMD.deserialize(
- profileSettings.value('customAnchorCmd', dict(), dict)
- )
- # Load defaults for new profiles.
- if profile.preset:
- from searxqt import defaults
- preset = defaults.Presets[profile.preset]
- # Settings
- if profile.type == InstancesModelTypes.Stats2:
- self._settingsModel.loadSettings(
- deepcopy(preset.get('settings', {})),
- stats2=True
- )
- elif profile.type == InstancesModelTypes.User:
- self._settingsModel.loadSettings(
- deepcopy(preset.get('settings', {})),
- stats2=False
- )
- # Guard
- self._guard.deserialize(
- deepcopy(preset.get('guard', {}))
- )
- # InstancesView
- self.instancesWidget.loadSettings(
- deepcopy(preset.get('instancesView', {}))
- )
- # InstancesFilter
- self.instanceFilter.loadSettings(
- deepcopy(preset.get('instancesFilter', {}))
- )
- # Instances
- self._handler.setData(
- deepcopy(preset.get('data', {}))
- )
- del preset
- del defaults
- def saveProfile(self):
- """ Save current profile
- """
- profile = self._profiles.current()
- profileSettings = QSettings(PROFILES_PATH, profile.id, self)
- profileSettings.setValue(
- 'settings', self._settingsModel.saveSettings()
- )
- profileSettings.setValue(
- 'instanceFilter', self.instanceFilter.saveSettings()
- )
- profileSettings.setValue(
- 'instancesView', self.instancesWidget.saveSettings()
- )
- profileSettings.setValue(
- 'instanceSelecter', self.instanceSelecter.saveSettings()
- )
- profileSettings.setValue(
- 'searchContainer', self.searchContainer.saveSettings()
- )
- profileSettings.setValue(
- 'searchModel', self._searchModel.saveSettings()
- )
- # Guard
- profileSettings.setValue('Guard', self._guard.serialize())
- # Store the main window splitter state (between search and instances)
- profileSettings.setValue('splitterState', self.splitter.saveState())
- # Custom Anchor commands
- profileSettings.setValue('customAnchorCmd', AnchorCMD.serialize())
- # Store searx-qt version (for backward compatibility)
- profileSettings.setValue('version', __version__)
- if self._handler:
- profileSettings.setValue('data', self._handler.data())
- def loadSharedSettings(self):
- """ Load shared settings
- """
- settings = QSettings(SETTINGS_PATH, 'shared', self)
- self.resize(
- settings.value('windowSize', QSize(), QSize)
- )
- self.__lastSettingsWindowSize = settings.value(
- 'settingsWindowSize',
- QSize(400, 400),
- QSize
- )
- def saveSettings(self):
- # save current profile
- if self._profiles.current().id:
- self.saveProfile()
- # shared.conf
- settings = QSettings(SETTINGS_PATH, 'shared', self)
- settings.setValue('windowSize', self.size())
- if self._settingsWindow:
- settings.setValue(
- 'settingsWindowSize',
- self._settingsWindow.size()
- )
|