main.py 15 KB

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