oqtopus 0.2.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@
24
24
 
25
25
 
26
26
  import os
27
+ import shutil
27
28
  import sys
28
29
 
29
30
  from qgis.PyQt.QtCore import QUrl
@@ -31,8 +32,10 @@ from qgis.PyQt.QtGui import QAction, QDesktopServices
31
32
  from qgis.PyQt.QtWidgets import (
32
33
  QDialog,
33
34
  QMenuBar,
35
+ QMessageBox,
34
36
  )
35
37
 
38
+ from ..core.module_package import ModulePackage
36
39
  from ..utils.plugin_utils import PluginUtils, logger
37
40
  from .about_dialog import AboutDialog
38
41
  from .settings_dialog import SettingsDialog
@@ -93,10 +96,18 @@ class MainDialog(QDialog, DIALOG_UI):
93
96
  self.menubar.setNativeMenuBar(False)
94
97
  self.layout().setMenuBar(self.menubar)
95
98
 
96
- # Settings action
97
- settings_action = QAction(self.tr("Settings"), self)
98
- settings_action.triggered.connect(self.__open_settings_dialog)
99
- self.menubar.addAction(settings_action)
99
+ # Settings menu
100
+ settings_menu = self.menubar.addMenu(self.tr("Settings"))
101
+
102
+ # Settings dialog action
103
+ settings_dialog_action = QAction(self.tr("Preferences..."), self)
104
+ settings_dialog_action.triggered.connect(self.__open_settings_dialog)
105
+ settings_menu.addAction(settings_dialog_action)
106
+
107
+ # Cache cleanup action
108
+ cleanup_cache_action = QAction(self.tr("Cleanup Cache"), self)
109
+ cleanup_cache_action.triggered.connect(self.__cleanup_cache)
110
+ settings_menu.addAction(cleanup_cache_action)
100
111
 
101
112
  # Help menu
102
113
  help_menu = self.menubar.addMenu(self.tr("Help"))
@@ -127,13 +138,34 @@ class MainDialog(QDialog, DIALOG_UI):
127
138
  )
128
139
  self.__databaseConnectionWidget_connectionChanged()
129
140
 
130
- self.module_tab.setEnabled(False)
131
- self.plugin_tab.setEnabled(False)
132
- self.project_tab.setEnabled(False)
141
+ self.__disable_module_tabs()
133
142
 
134
143
  logger.info("Ready.")
135
144
 
145
+ def closeEvent(self, event):
146
+ """Handle window close event (X button) to properly cleanup threads."""
147
+ if self.__moduleWidget.isOperationRunning():
148
+ QMessageBox.warning(
149
+ self,
150
+ self.tr("Operation in progress"),
151
+ self.tr("Cannot close the dialog while an operation is running."),
152
+ )
153
+ event.ignore()
154
+ return
155
+ self.__moduleWidget.close()
156
+ self.__moduleSelectionWidget.close()
157
+ self.__logsWidget.close()
158
+ event.accept()
159
+
136
160
  def __closeDialog(self):
161
+ if self.__moduleWidget.isOperationRunning():
162
+ QMessageBox.warning(
163
+ self,
164
+ self.tr("Operation in progress"),
165
+ self.tr("Cannot close the dialog while an operation is running."),
166
+ )
167
+ return
168
+ self.__moduleWidget.close()
137
169
  self.__moduleSelectionWidget.close()
138
170
  self.__logsWidget.close()
139
171
  self.accept()
@@ -147,44 +179,99 @@ class MainDialog(QDialog, DIALOG_UI):
147
179
  dlg = SettingsDialog(self)
148
180
  dlg.exec()
149
181
 
182
+ def __cleanup_cache(self):
183
+ """Delete all cached data (downloaded packages and GitHub API cache)."""
184
+ cache_paths = PluginUtils.get_all_cache_paths()
185
+
186
+ if not cache_paths:
187
+ QMessageBox.information(
188
+ self,
189
+ self.tr("Cache Cleanup"),
190
+ self.tr("No cache directories found. Nothing to clean up."),
191
+ )
192
+ return
193
+
194
+ # Format paths for display
195
+ paths_display = "\n".join(f" • {p}" for p in cache_paths)
196
+
197
+ # Ask for confirmation
198
+ reply = QMessageBox.question(
199
+ self,
200
+ self.tr("Cleanup Cache"),
201
+ self.tr(
202
+ f"This will delete all cached data from:\n{paths_display}\n\n"
203
+ "Downloaded module packages and API cache will need to be re-fetched.\n\n"
204
+ "Are you sure you want to continue?"
205
+ ),
206
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
207
+ QMessageBox.StandardButton.No,
208
+ )
209
+
210
+ if reply != QMessageBox.StandardButton.Yes:
211
+ return
212
+
213
+ errors = []
214
+ for cache_dir in cache_paths:
215
+ try:
216
+ shutil.rmtree(cache_dir)
217
+ except Exception as e:
218
+ errors.append(f"{cache_dir}: {e}")
219
+
220
+ if errors:
221
+ QMessageBox.warning(
222
+ self,
223
+ self.tr("Cache Cleanup Warning"),
224
+ self.tr("Some cache directories could not be deleted:\n") + "\n".join(errors),
225
+ )
226
+ else:
227
+ QMessageBox.information(
228
+ self,
229
+ self.tr("Cache Cleanup"),
230
+ self.tr("Cache has been successfully cleaned up."),
231
+ )
232
+
150
233
  def __show_about_dialog(self):
151
234
  dialog = self.__about_dialog_cls(self)
152
235
  dialog.exec()
153
236
 
154
- def __moduleSelection_loadingStarted(self):
155
- self.db_groupBox.setEnabled(False)
237
+ def __disable_module_tabs(self):
238
+ """Disable all module-related tabs."""
156
239
  self.module_tab.setEnabled(False)
157
240
  self.plugin_tab.setEnabled(False)
158
241
  self.project_tab.setEnabled(False)
159
242
 
160
- def __moduleSelection_loadingFinished(self):
161
- self.db_groupBox.setEnabled(True)
162
-
163
- module_package = self.__moduleSelectionWidget.getSelectedModulePackage()
164
- if module_package is None:
165
- return
243
+ def __enable_module_tabs(self, module_package: ModulePackage):
244
+ """Enable module tabs based on available assets."""
245
+ self.module_tab.setEnabled(True)
246
+ self.plugin_tab.setEnabled(module_package.asset_plugin is not None)
247
+ self.project_tab.setEnabled(module_package.asset_project is not None)
166
248
 
167
- if self.__moduleSelectionWidget.lastError() is not None:
168
- return
249
+ def __clear_module_packages(self):
250
+ """Clear module package state from all widgets."""
251
+ self.__moduleWidget.clearModulePackage()
252
+ self.__projectWidget.clearModulePackage()
253
+ self.__pluginWidget.clearModulePackage()
169
254
 
170
- self.module_tab.setEnabled(True)
255
+ def __set_module_packages(self, module_package: ModulePackage):
256
+ """Set module package in all widgets."""
257
+ self.__moduleWidget.setModulePackage(module_package)
258
+ self.__projectWidget.setModulePackage(module_package)
259
+ self.__pluginWidget.setModulePackage(module_package)
171
260
 
172
- if module_package.asset_plugin is not None:
173
- self.plugin_tab.setEnabled(True)
261
+ def __moduleSelection_loadingStarted(self):
262
+ self.db_groupBox.setEnabled(False)
263
+ self.__disable_module_tabs()
264
+ self.__clear_module_packages()
174
265
 
175
- if module_package.asset_project is not None:
176
- self.project_tab.setEnabled(True)
266
+ def __moduleSelection_loadingFinished(self):
267
+ self.db_groupBox.setEnabled(True)
177
268
 
178
- self.__moduleWidget.setModulePackage(
179
- self.__moduleSelectionWidget.getSelectedModulePackage()
180
- )
181
- self.__projectWidget.setModulePackage(
182
- self.__moduleSelectionWidget.getSelectedModulePackage()
183
- )
269
+ module_package = self.__moduleSelectionWidget.getSelectedModulePackage()
270
+ if module_package is None or self.__moduleSelectionWidget.lastError() is not None:
271
+ return
184
272
 
185
- self.__pluginWidget.setModulePackage(
186
- self.__moduleSelectionWidget.getSelectedModulePackage()
187
- )
273
+ self.__enable_module_tabs(module_package)
274
+ self.__set_module_packages(module_package)
188
275
 
189
276
  def __databaseConnectionWidget_connectionChanged(self):
190
277
  self.__moduleWidget.setDatabaseConnection(self.__databaseConnectionWidget.getConnection())
@@ -15,7 +15,7 @@ DIALOG_UI = PluginUtils.get_ui_class("module_selection_widget.ui")
15
15
 
16
16
  class ModuleSelectionWidget(QWidget, DIALOG_UI):
17
17
 
18
- module_package_SPECIAL_LOAD_DEVELOPMENT = "Load development versions"
18
+ module_package_SPECIAL_LOAD_DEVELOPMENT = "Load pre-releases and development branches"
19
19
 
20
20
  signal_loadingStarted = pyqtSignal()
21
21
  signal_loadingFinished = pyqtSignal()
@@ -49,8 +49,10 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
49
49
  for config_module in self.__modules_config.modules:
50
50
  module = Module(
51
51
  name=config_module.name,
52
+ id=config_module.id,
52
53
  organisation=config_module.organisation,
53
54
  repository=config_module.repository,
55
+ exclude_releases=config_module.exclude_releases,
54
56
  parent=self,
55
57
  )
56
58
  self.module_module_comboBox.addItem(module.name, module)
@@ -62,8 +64,8 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
62
64
  self.module_latestVersion_label.setText("")
63
65
  QtUtils.setForegroundColor(self.module_latestVersion_label, PluginUtils.COLOR_GREEN)
64
66
 
65
- self.module_package_comboBox.clear()
66
- self.module_package_comboBox.addItem(self.tr("Please select a version"), None)
67
+ self.__reset_package_selection()
68
+ self.module_seeChangeLog_pushButton.setEnabled(False)
67
69
 
68
70
  self.module_zipPackage_groupBox.setVisible(False)
69
71
 
@@ -80,6 +82,14 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
80
82
 
81
83
  def close(self):
82
84
  if self.__packagePrepareTask.isRunning():
85
+ # Disconnect signals first to prevent crashes when emitting to destroyed widgets
86
+ try:
87
+ self.__packagePrepareTask.signalPackagingProgress.disconnect()
88
+ self.__packagePrepareTask.finished.disconnect()
89
+ except TypeError:
90
+ # Already disconnected
91
+ pass
92
+
83
93
  self.__packagePrepareTask.cancel()
84
94
  self.__packagePrepareTask.wait()
85
95
 
@@ -93,22 +103,51 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
93
103
  """
94
104
  return self.__packagePrepareTask.lastError
95
105
 
106
+ def __reset_package_selection(self):
107
+ """Reset package selection combo box to initial state."""
108
+ self.module_package_comboBox.clear()
109
+ self.module_package_comboBox.addItem(self.tr("Please select a version"), None)
110
+ self.module_package_comboBox.setEnabled(False)
111
+ # Disable this placeholder item so it can't be selected once a version is chosen
112
+ model = self.module_package_comboBox.model()
113
+ item = model.item(0)
114
+ if item:
115
+ item.setEnabled(False)
116
+
117
+ def __enable_package_selection(self):
118
+ """Enable package selection combo box."""
119
+ self.module_package_comboBox.setEnabled(True)
120
+
96
121
  def __moduleChanged(self, index):
122
+ logger.debug(f"__moduleChanged START, index={index}")
97
123
  if self.module_module_comboBox.currentData() == self.__current_module:
124
+ logger.debug("Same module selected, returning")
98
125
  return
99
126
 
127
+ logger.debug(f"Module changed to: {self.module_module_comboBox.currentText()}")
100
128
  self.__current_module = self.module_module_comboBox.currentData()
101
129
 
130
+ logger.debug("Resetting labels and UI")
102
131
  self.module_latestVersion_label.setText("")
103
- self.module_package_comboBox.clear()
104
- self.module_package_comboBox.addItem(self.tr("Please select a version"), None)
132
+ self.__reset_package_selection()
133
+ self.module_seeChangeLog_pushButton.setEnabled(False)
105
134
 
106
135
  if self.__current_module is None:
136
+ logger.debug("No module selected")
107
137
  return
108
138
 
139
+ logger.debug(f"Module versions list length: {len(self.__current_module.versions)}")
109
140
  if self.__current_module.versions == list():
141
+ logger.debug("Versions empty, starting load")
142
+ # Emit signal first to allow UI to update before showing wait cursor
143
+ self.signal_loadingStarted.emit()
110
144
  QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
111
145
  self.__current_module.start_load_versions()
146
+ else:
147
+ logger.debug("Versions already loaded, populating UI")
148
+ # Versions already loaded (from cache or previous selection) - populate UI
149
+ self.__loadVersionsFinished("")
150
+ logger.debug("__moduleChanged END")
112
151
 
113
152
  def __moduleVersionChanged(self, index):
114
153
 
@@ -126,20 +165,35 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
126
165
 
127
166
  self.__current_module_package = self.module_package_comboBox.currentData()
128
167
  if self.__current_module_package is None:
168
+ self.module_seeChangeLog_pushButton.setEnabled(False)
169
+ # Clear module information when placeholder is selected
170
+ self.module_information_label.setText(self.tr("Please select a version"))
171
+ QtUtils.resetForegroundColor(self.module_information_label)
172
+ self.module_informationProject_label.setText("-")
173
+ self.module_informationPlugin_label.setText("-")
174
+ # Emit signal to clear module widgets
175
+ self.signal_loadingFinished.emit()
129
176
  return
130
177
 
178
+ # Enable changelog button for valid selections
179
+ self.module_seeChangeLog_pushButton.setEnabled(True)
180
+
131
181
  if self.__current_module_package.type == self.__current_module_package.Type.FROM_ZIP:
132
182
  self.module_zipPackage_groupBox.setVisible(True)
133
183
  return
134
184
  else:
135
185
  self.module_zipPackage_groupBox.setVisible(False)
136
186
 
137
- loading_text = self.tr(
138
- f"Loading packages for module '{self.module_module_comboBox.currentText()}' version '{self.__current_module_package.display_name()}'..."
139
- )
187
+ loading_text = self.tr("Loading package...")
140
188
  self.module_information_label.setText(loading_text)
189
+ self.module_information_label.setToolTip(
190
+ f"{self.module_module_comboBox.currentText()} - {self.__current_module_package.display_name()}"
191
+ )
141
192
  QtUtils.resetForegroundColor(self.module_information_label)
142
- logger.info(loading_text)
193
+ logger.info(
194
+ f"Loading packages for module '{self.module_module_comboBox.currentText()}' "
195
+ f"version '{self.__current_module_package.display_name()}'..."
196
+ )
143
197
 
144
198
  self.module_informationProject_label.setText("-")
145
199
  self.module_informationPlugin_label.setText("-")
@@ -147,6 +201,8 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
147
201
  self.__packagePrepareTask.startFromModulePackage(self.__current_module_package)
148
202
 
149
203
  self.signal_loadingStarted.emit()
204
+ self.module_progressBar.setMaximum(100)
205
+ self.module_progressBar.setValue(0)
150
206
  self.module_progressBar.setVisible(True)
151
207
 
152
208
  def __loadDevelopmentVersions(self):
@@ -154,6 +210,8 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
154
210
  return
155
211
 
156
212
  if self.__current_module.development_versions == list():
213
+ # Emit signal first to allow UI to update before showing wait cursor
214
+ self.signal_loadingStarted.emit()
157
215
  QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
158
216
  self.__current_module.start_load_development_versions()
159
217
 
@@ -185,6 +243,8 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
185
243
  self.__packagePrepareTask.startFromZip(self.__current_module_package, filename)
186
244
 
187
245
  self.signal_loadingStarted.emit()
246
+ self.module_progressBar.setMaximum(100)
247
+ self.module_progressBar.setValue(0)
188
248
  self.module_progressBar.setVisible(True)
189
249
 
190
250
  def __packagePrepareTaskFinished(self):
@@ -211,29 +271,43 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
211
271
  package_dir = self.module_package_comboBox.currentData().source_package_dir
212
272
  logger.info(f"Package loaded into '{package_dir}'")
213
273
  QtUtils.resetForegroundColor(self.module_information_label)
214
- self.module_information_label.setText(
215
- f"<a href='file://{package_dir}'>{package_dir}</a>",
216
- )
274
+ QtUtils.setPathLinkWithEllipsis(self.module_information_label, package_dir)
217
275
 
218
276
  asset_project = self.module_package_comboBox.currentData().asset_project
219
277
  if asset_project:
220
- self.module_informationProject_label.setText(
221
- f"<a href='file://{asset_project.package_dir}'>{asset_project.package_dir}</a>",
278
+ QtUtils.setPathLinkWithEllipsis(
279
+ self.module_informationProject_label, asset_project.package_dir
222
280
  )
223
281
  else:
224
282
  self.module_informationProject_label.setText("No asset available")
283
+ self.module_informationProject_label.setToolTip("")
225
284
 
226
285
  asset_plugin = self.module_package_comboBox.currentData().asset_plugin
227
286
  if asset_plugin:
228
- self.module_informationPlugin_label.setText(
229
- f"<a href='file://{asset_plugin.package_dir}'>{asset_plugin.package_dir}</a>",
287
+ QtUtils.setPathLinkWithEllipsis(
288
+ self.module_informationPlugin_label, asset_plugin.package_dir
230
289
  )
231
290
  else:
232
291
  self.module_informationPlugin_label.setText("No asset available")
292
+ self.module_informationPlugin_label.setToolTip("")
293
+
294
+ def __packagePrepareTaskProgress(self, progress, bytes_downloaded):
295
+ if progress < 0:
296
+ # Indeterminate progress (size unknown)
297
+ self.module_progressBar.setMaximum(0)
298
+ self.module_progressBar.setValue(0)
299
+ if bytes_downloaded > 0:
300
+ mb_downloaded = bytes_downloaded / (1024 * 1024)
301
+ loading_text = self.tr(f"Downloading package... {mb_downloaded:.1f} MB")
302
+ else:
303
+ loading_text = self.tr("Downloading package...")
304
+ else:
305
+ # Determinate progress (0-100%)
306
+ self.module_progressBar.setMaximum(100)
307
+ self.module_progressBar.setValue(int(progress))
308
+ mb_downloaded = bytes_downloaded / (1024 * 1024)
309
+ loading_text = self.tr(f"Downloading... {mb_downloaded:.1f} MB ({progress:.0f}%)")
233
310
 
234
- def __packagePrepareTaskProgress(self, progress):
235
- loading_text = self.tr("Load package task running...")
236
- logger.info(loading_text)
237
311
  self.module_information_label.setText(loading_text)
238
312
 
239
313
  def __seeChangeLogClicked(self):
@@ -300,6 +374,9 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
300
374
  return
301
375
 
302
376
  for module_package in self.__current_module.versions:
377
+ # Skip pre-releases in the main list (they'll be shown in development versions)
378
+ if module_package.prerelease is True:
379
+ continue
303
380
  self.module_package_comboBox.addItem(module_package.display_name(), module_package)
304
381
 
305
382
  if self.__current_module.latest_version is not None:
@@ -321,10 +398,19 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
321
398
  )
322
399
 
323
400
  self.module_package_comboBox.insertSeparator(self.module_package_comboBox.count())
324
- self.module_package_comboBox.addItem(
325
- self.tr("Load development branches"), self.module_package_SPECIAL_LOAD_DEVELOPMENT
326
- )
327
401
 
402
+ # If development versions were already loaded, add them directly
403
+ # Otherwise show the option to load them
404
+ if self.__current_module.development_versions:
405
+ for module_package in self.__current_module.development_versions:
406
+ self.module_package_comboBox.addItem(module_package.display_name(), module_package)
407
+ else:
408
+ self.module_package_comboBox.addItem(
409
+ self.tr("Load pre-releases and development branches"),
410
+ self.module_package_SPECIAL_LOAD_DEVELOPMENT,
411
+ )
412
+
413
+ self.__enable_package_selection()
328
414
  self.module_progressBar.setVisible(False)
329
415
  logger.info(f"Versions loaded for module '{self.__current_module.name}'.")
330
416
 
@@ -373,3 +459,5 @@ class ModuleSelectionWidget(QWidget, DIALOG_UI):
373
459
 
374
460
  for module_package in self.__current_module.development_versions:
375
461
  self.module_package_comboBox.addItem(module_package.display_name(), module_package)
462
+
463
+ self.__enable_package_selection()