main.py 14 KB

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