main.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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 sys
  22. from copy import deepcopy
  23. from PyQt5.QtWidgets import (
  24. QApplication,
  25. QAction,
  26. QSplitter,
  27. QMenuBar,
  28. QMenu,
  29. QMainWindow,
  30. QHBoxLayout,
  31. QVBoxLayout,
  32. QWidget,
  33. QStatusBar,
  34. QLabel,
  35. QMessageBox
  36. )
  37. from PyQt5.QtCore import (
  38. Qt,
  39. QSettings,
  40. QByteArray,
  41. QSize
  42. )
  43. from searxqt.models.instances import (
  44. Stats2InstancesModel,
  45. UserInstancesModel,
  46. Stats2EnginesModel,
  47. UserEnginesModel,
  48. Stats2Model,
  49. InstanceModelFilter,
  50. InstanceSelecterModel,
  51. PersistentInstancesModel,
  52. PersistentEnginesModel,
  53. InstancesModelTypes
  54. )
  55. from searxqt.models.settings import SettingsModel
  56. from searxqt.models.search import SearchModel, UserInstancesHandler
  57. from searxqt.models.profiles import Profiles, ProfileItem
  58. from searxqt.views.instances import InstancesView
  59. from searxqt.views.settings import SettingsWindow
  60. from searxqt.views.search import SearchContainer
  61. from searxqt.views import about
  62. from searxqt.views.profiles import ProfileChooserDialog
  63. from searxqt.core.requests import RequestsHandler
  64. from searxqt.core.guard import Guard
  65. from searxqt.translations import _
  66. from searxqt.version import __version__
  67. from searxqt import PROFILES_PATH, SETTINGS_PATH
  68. from searxqt.core import log
  69. class MainWindow(QMainWindow):
  70. def __init__(self, *args, **kwargs):
  71. QMainWindow.__init__(self, *args, **kwargs)
  72. self.setWindowTitle("SearX-Qt")
  73. self._settingsModel = SettingsModel(self)
  74. self._handler = None
  75. self._settingsWindow = None
  76. # Request handler
  77. self._requestHandler = RequestsHandler(self._settingsModel.requests)
  78. # Persistent models
  79. self._persistantInstancesModel = PersistentInstancesModel()
  80. self._persistantEnginesModel = PersistentEnginesModel()
  81. # Profiles
  82. self._profiles = Profiles()
  83. self.instanceFilter = InstanceModelFilter(
  84. self._persistantInstancesModel, self
  85. )
  86. self.instanceSelecter = InstanceSelecterModel(self.instanceFilter)
  87. self._searchModel = SearchModel(self._requestHandler, self)
  88. # Guard
  89. self._guard = Guard()
  90. # -- Menu bar
  91. menubar = QMenuBar(self)
  92. # Menu file
  93. menuFile = QMenu(menubar)
  94. menuFile.setTitle(_("File"))
  95. saveAction = QAction(_("Save"), menuFile)
  96. menuFile.addAction(saveAction)
  97. saveAction.setShortcut('Ctrl+S')
  98. saveAction.triggered.connect(self.saveSettings)
  99. actionExit = QAction(_("Exit"), menuFile)
  100. menuFile.addAction(actionExit)
  101. actionExit.setShortcut('Ctrl+Q')
  102. actionExit.triggered.connect(self.close)
  103. menubar.addAction(menuFile.menuAction())
  104. # Menu settings
  105. settingsAction = QAction(_("Settings"), menubar)
  106. menubar.addAction(settingsAction)
  107. settingsAction.triggered.connect(self._openSettingsWindow)
  108. # Menu profiles
  109. profilesAction = QAction(_("Profiles"), menubar)
  110. menubar.addAction(profilesAction)
  111. profilesAction.triggered.connect(self._openProfileChooser)
  112. # Menu about dialog
  113. aboutAction = QAction(_("About"), menubar)
  114. menubar.addAction(aboutAction)
  115. aboutAction.triggered.connect(self._openAboutDialog)
  116. self.setMenuBar(menubar)
  117. # -- End menu bar
  118. # -- Status bar
  119. self.statusBar = QStatusBar(self)
  120. statusWidget = QWidget(self)
  121. statusLayout = QHBoxLayout(statusWidget)
  122. self._handlerThreadStatusLabel = QLabel(self)
  123. statusLayout.addWidget(self._handlerThreadStatusLabel)
  124. self._statusInstanceLabel = QLabel(self)
  125. statusLayout.addWidget(self._statusInstanceLabel)
  126. self.statusBar.addPermanentWidget(statusWidget)
  127. self.setStatusBar(self.statusBar)
  128. # -- End status bar
  129. centralWidget = QWidget(self)
  130. layout = QVBoxLayout(centralWidget)
  131. self.setCentralWidget(centralWidget)
  132. self.splitter = QSplitter(centralWidget)
  133. self.splitter.setOrientation(Qt.Horizontal)
  134. layout.addWidget(self.splitter)
  135. self.searchContainer = SearchContainer(
  136. self._searchModel,
  137. self.instanceFilter,
  138. self.instanceSelecter,
  139. self._persistantEnginesModel,
  140. self._guard,
  141. self.splitter
  142. )
  143. self.instancesWidget = InstancesView(
  144. self.instanceFilter,
  145. self.instanceSelecter,
  146. self.splitter
  147. )
  148. self.instanceSelecter.instanceChanged.connect(self.__instanceChanged)
  149. self.__profileChooserInit()
  150. self.resize(800, 600)
  151. self.loadSharedSettings()
  152. self.loadProfile()
  153. def __instanceChanged(self, url):
  154. self._statusInstanceLabel.setText(
  155. "<b>{0}:</b> {1}".format(_("Instance"), url)
  156. )
  157. def closeEvent(self, event=None):
  158. # Disable everything.
  159. self.setEnabled(False)
  160. # Wait till all threads finished
  161. if self.searchContainer.isBusy():
  162. log.info("- Waiting for search thread to finish...", self)
  163. self.searchContainer.cancelAll()
  164. log.info("- Search thread finished.")
  165. if self._handler and self._handler.hasActiveJobs():
  166. log.info(
  167. "- Waiting for update instances thread to finish...",
  168. self
  169. )
  170. self._handler.cancelAll()
  171. log.info("- Instances update thread finished.", self)
  172. self.saveSettings()
  173. log.info("- Settings saved.", self)
  174. # Remove currently active profile id from the active list.
  175. self._profiles.setProfile(
  176. self._profiles.settings(),
  177. ProfileItem()
  178. )
  179. QApplication.closeAllWindows()
  180. log.info("Bye!", self)
  181. def _openAboutDialog(self):
  182. about.show(self)
  183. def __execProfileChooser(self):
  184. """ This only sets the profile, it does not load it.
  185. Returns True on success, False when something went wrong.
  186. """
  187. profiles = self._profiles
  188. profilesSettings = profiles.settings()
  189. profiles.loadProfiles(profilesSettings) # read profiles.conf
  190. dialog = ProfileChooserDialog(profiles)
  191. if dialog.exec():
  192. currentProfile = profiles.current()
  193. selectedProfile = dialog.selectedProfile()
  194. # Save current profile if one is set.
  195. if currentProfile.id:
  196. self.saveProfile()
  197. profiles.setProfile(
  198. profilesSettings,
  199. selectedProfile
  200. )
  201. else:
  202. self.__finalizeProfileChooser(dialog)
  203. return False
  204. self.__finalizeProfileChooser(dialog)
  205. return True
  206. def __profileChooserInit(self):
  207. profiles = self._profiles
  208. profilesSettings = profiles.settings()
  209. profiles.loadProfiles(profilesSettings) # read profiles.conf
  210. activeProfiles = profiles.getActiveProfiles(profilesSettings)
  211. defaultProfile = profiles.default()
  212. if not defaultProfile.id or defaultProfile.id in activeProfiles:
  213. if not self.__execProfileChooser():
  214. sys.exit()
  215. else:
  216. # Load default profile.
  217. profiles.setProfile(
  218. profilesSettings,
  219. defaultProfile
  220. )
  221. def _openProfileChooser(self):
  222. if self._handler and self._handler.hasActiveJobs():
  223. QMessageBox.information(
  224. self,
  225. _("Instances update thread active"),
  226. _("Please wait until instances finished updating before\n"
  227. "switching profiles.")
  228. )
  229. return
  230. if self.__execProfileChooser():
  231. self.loadProfile()
  232. def __finalizeProfileChooser(self, dialog):
  233. """ Profiles may have been added or removed.
  234. - Store profiles.conf
  235. - Remove removed profile conf files
  236. """
  237. self._profiles.saveProfiles()
  238. self._profiles.removeProfileFiles(dialog.removedProfiles())
  239. def _openSettingsWindow(self):
  240. if not self._settingsWindow:
  241. self._settingsWindow = SettingsWindow(
  242. self._settingsModel,
  243. self._guard
  244. )
  245. self._settingsWindow.resize(self.__lastSettingsWindowSize)
  246. self._settingsWindow.show()
  247. self._settingsWindow.activateWindow() # Bring it to front
  248. self._settingsWindow.closed.connect(self._delSettingsWindow)
  249. def _delSettingsWindow(self):
  250. self._settingsWindow.closed.disconnect()
  251. self._settingsWindow.deleteLater()
  252. self._settingsWindow = None
  253. def __handlerThreadChanged(self):
  254. self._handlerThreadStatusLabel.setText(
  255. self._handler.currentJobStr()
  256. )
  257. def loadProfile(self):
  258. profile = self._profiles.current()
  259. self.setWindowTitle(f"SearX-Qt - {profile.name}")
  260. profileSettings = QSettings(PROFILES_PATH, profile.id, self)
  261. # Migrate old config to new one, if needed.
  262. from searxqt.config import upgradeProfileConfig
  263. upgradeProfileConfig(profileSettings)
  264. del upgradeProfileConfig
  265. # Clean previous stuff
  266. if self._handler:
  267. self._handler.threadStarted.disconnect(
  268. self.__handlerThreadChanged
  269. )
  270. self._handler.threadFinished.disconnect(
  271. self.__handlerThreadChanged
  272. )
  273. self._handler.deleteLater()
  274. self._handler = None
  275. if self._settingsWindow:
  276. self._delSettingsWindow()
  277. self._searchModel.reset()
  278. # Set new models
  279. if profile.type == InstancesModelTypes.Stats2:
  280. self._handler = Stats2Model(self._requestHandler, self)
  281. instancesModel = Stats2InstancesModel(self._handler, parent=self)
  282. enginesModel = Stats2EnginesModel(self._handler, parent=self)
  283. self._settingsModel.loadSettings(
  284. profileSettings.value('settings', dict(), dict),
  285. stats2=True
  286. )
  287. elif profile.type == InstancesModelTypes.User:
  288. self._handler = UserInstancesHandler(self._requestHandler, self)
  289. instancesModel = UserInstancesModel(self._handler, parent=self)
  290. enginesModel = UserEnginesModel(self._handler, parent=self)
  291. self._settingsModel.loadSettings(
  292. profileSettings.value('settings', dict(), dict),
  293. stats2=False
  294. )
  295. self._persistantInstancesModel.setModel(instancesModel)
  296. self._persistantEnginesModel.setModel(enginesModel)
  297. self._handler.setData(
  298. profileSettings.value('data', dict(), dict)
  299. )
  300. self._handler.threadStarted.connect(self.__handlerThreadChanged)
  301. self._handler.threadFinished.connect(self.__handlerThreadChanged)
  302. # Load settings
  303. self.instanceFilter.loadSettings(
  304. profileSettings.value('instanceFilter', dict(), dict)
  305. )
  306. self.instancesWidget.loadSettings(
  307. profileSettings.value('instancesView', dict(), dict)
  308. )
  309. self.instanceSelecter.loadSettings(
  310. profileSettings.value('instanceSelecter', dict(), dict)
  311. )
  312. self.searchContainer.loadSettings(
  313. profileSettings.value('searchContainer', dict(), dict)
  314. )
  315. self._searchModel.loadSettings(
  316. profileSettings.value('searchModel', dict(), dict)
  317. )
  318. # Guard
  319. self._guard.deserialize(profileSettings.value('Guard', dict(), dict))
  320. # Load main window splitter state (between search and instances)
  321. self.splitter.restoreState(
  322. profileSettings.value('splitterState', QByteArray(), QByteArray)
  323. )
  324. # Load defaults for new profiles.
  325. if profile.preset:
  326. from searxqt import defaults
  327. preset = defaults.Presets[profile.preset]
  328. # Settings
  329. if profile.type == InstancesModelTypes.Stats2:
  330. self._settingsModel.loadSettings(
  331. deepcopy(preset.get('settings', {})),
  332. stats2=True
  333. )
  334. elif profile.type == InstancesModelTypes.User:
  335. self._settingsModel.loadSettings(
  336. deepcopy(preset.get('settings', {})),
  337. stats2=False
  338. )
  339. # Guard
  340. self._guard.deserialize(
  341. deepcopy(preset.get('guard', {}))
  342. )
  343. # InstancesView
  344. self.instancesWidget.loadSettings(
  345. deepcopy(preset.get('instancesView', {}))
  346. )
  347. # InstancesFilter
  348. self.instanceFilter.loadSettings(
  349. deepcopy(preset.get('instancesFilter', {}))
  350. )
  351. # Instances
  352. self._handler.setData(
  353. deepcopy(preset.get('data', {}))
  354. )
  355. del preset
  356. del defaults
  357. def saveProfile(self):
  358. """ Save current profile
  359. """
  360. profile = self._profiles.current()
  361. profileSettings = QSettings(PROFILES_PATH, profile.id, self)
  362. profileSettings.setValue(
  363. 'settings', self._settingsModel.saveSettings()
  364. )
  365. profileSettings.setValue(
  366. 'instanceFilter', self.instanceFilter.saveSettings()
  367. )
  368. profileSettings.setValue(
  369. 'instancesView', self.instancesWidget.saveSettings()
  370. )
  371. profileSettings.setValue(
  372. 'instanceSelecter', self.instanceSelecter.saveSettings()
  373. )
  374. profileSettings.setValue(
  375. 'searchContainer', self.searchContainer.saveSettings()
  376. )
  377. profileSettings.setValue(
  378. 'searchModel', self._searchModel.saveSettings()
  379. )
  380. # Guard
  381. profileSettings.setValue('Guard', self._guard.serialize())
  382. # Store the main window splitter state (between search and instances)
  383. profileSettings.setValue('splitterState', self.splitter.saveState())
  384. # Store searx-qt version (for backward compatibility)
  385. profileSettings.setValue('version', __version__)
  386. if self._handler:
  387. profileSettings.setValue('data', self._handler.data())
  388. def loadSharedSettings(self):
  389. """ Load shared settings
  390. """
  391. settings = QSettings(SETTINGS_PATH, 'shared', self)
  392. self.resize(
  393. settings.value('windowSize', QSize(), QSize)
  394. )
  395. self.__lastSettingsWindowSize = settings.value(
  396. 'settingsWindowSize',
  397. QSize(400, 400),
  398. QSize
  399. )
  400. def saveSettings(self):
  401. # save current profile
  402. if self._profiles.current().id:
  403. self.saveProfile()
  404. # shared.conf
  405. settings = QSettings(SETTINGS_PATH, 'shared', self)
  406. settings.setValue('windowSize', self.size())
  407. if self._settingsWindow:
  408. settings.setValue(
  409. 'settingsWindowSize',
  410. self._settingsWindow.size()
  411. )