main.py 13 KB

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