Skip to content

Commit 819c971

Browse files
committed
Modernize custom open project dialog
- Make browsing folders much quicker when mounted or network drives are present - Show busy cursor when expanding folders
1 parent 957e914 commit 819c971

1 file changed

Lines changed: 80 additions & 43 deletions

File tree

spinetoolbox/widgets/open_project_dialog.py

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"""Contains a class for a widget that represents a 'Open Project Directory' dialog."""
1414

1515
import os
16-
from PySide6.QtCore import QDir, QModelIndex, QStandardPaths, Qt, Slot
16+
from PySide6.QtCore import QDir, QModelIndex, QStandardPaths, Qt, Slot, QPoint, QTimer
1717
from PySide6.QtGui import QAction, QKeySequence, QValidator
18-
from PySide6.QtWidgets import QAbstractItemView, QComboBox, QDialog, QFileSystemModel
18+
from PySide6.QtWidgets import QAbstractItemView, QComboBox, QDialog, QFileSystemModel, QApplication
1919
from spinetoolbox.helpers import ProjectDirectoryIconProvider
2020
from spinetoolbox.widgets.custom_menus import OpenProjectDialogComboBoxContextMenu
2121
from spinetoolbox.widgets.notification import Notification
@@ -33,14 +33,14 @@ def __init__(self, toolbox):
3333
"""
3434
from ..ui import open_project_dialog # pylint: disable=import-outside-toplevel
3535

36-
super().__init__(parent=toolbox, f=Qt.Dialog) # Setting the parent inherits the stylesheet
36+
super().__init__(parent=toolbox, f=Qt.WindowType.Dialog) # Setting the parent inherits the stylesheet
3737
self._qsettings = toolbox.qsettings()
3838
# Set up the user interface from Designer file
3939
self.ui = open_project_dialog.Ui_Dialog()
4040
self.ui.setupUi(self)
4141
self.combobox_context_menu = None
4242
# Ensure this dialog is garbage-collected when closed
43-
self.setAttribute(Qt.WA_DeleteOnClose)
43+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
4444
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
4545
# QActions for keyboard shortcuts
4646
self.go_root_action = QAction(self)
@@ -51,12 +51,13 @@ def __init__(self, toolbox):
5151
self.selected_path = ""
5252
self.cb_ss = self.ui.comboBox_current_path.styleSheet()
5353
self.file_model = CustomQFileSystemModel()
54-
self.file_model.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
54+
self.file_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.NoDotAndDotDot)
5555
self.icon_provider = ProjectDirectoryIconProvider()
5656
self.file_model.setIconProvider(self.icon_provider)
57-
self.file_model.setRootPath(QDir.rootPath())
57+
self.file_model.setRootPath(QDir.homePath())
58+
self.ui.treeView_file_system.setUniformRowHeights(True)
5859
self.ui.treeView_file_system.setModel(self.file_model)
59-
self.file_model.sort(0, Qt.AscendingOrder)
60+
self.file_model.sort(0, Qt.SortOrder.AscendingOrder)
6061
# Enable validator (experimental, not very useful here)
6162
# Validator prevents typing Invalid strings to combobox. (not in use)
6263
# When text in combobox is Intermediate, the validator prevents emitting
@@ -81,8 +82,10 @@ def __init__(self, toolbox):
8182
self.ui.comboBox_current_path.setCurrentIndex(-1)
8283
self.file_model.directoryLoaded.connect(self.expand_and_resize)
8384
# Start browsing to start index immediately when dialog is shown
84-
self.start_path = self.file_model.filePath(start_index)
85-
self.starting_up = True
85+
self._start_path = self.file_model.filePath(start_index)
86+
self._starting_up = True
87+
self._loading = False
88+
self._closing = False
8689
self.ui.treeView_file_system.setCurrentIndex(start_index)
8790
self.connect_signals()
8891

@@ -110,30 +113,59 @@ def connect_signals(self):
110113
self.ui.treeView_file_system.clicked.connect(self.set_selected_path)
111114
self.ui.treeView_file_system.doubleClicked.connect(self.open_project)
112115
self.ui.treeView_file_system.selectionModel().currentChanged.connect(self.current_changed)
116+
self.ui.treeView_file_system.expanded.connect(self.on_expanded)
117+
self.file_model.rowsInserted.connect(self.on_rows_inserted)
118+
self.file_model.layoutChanged.connect(self.on_layout_changed)
113119
self.go_root_action.triggered.connect(self.go_root)
114120
self.go_home_action.triggered.connect(self.go_home)
115121
self.go_documents_action.triggered.connect(self.go_documents)
116122
self.go_desktop_action.triggered.connect(self.go_desktop)
117123

124+
def start_busy(self):
125+
if not self._loading:
126+
self._loading = True
127+
# prevent stacking
128+
if QApplication.overrideCursor() is None:
129+
QApplication.setOverrideCursor(Qt.CursorShape.BusyCursor)
130+
QTimer.singleShot(2000, self.stop_busy)
131+
132+
def stop_busy(self):
133+
if self._loading:
134+
self._loading = False
135+
if QApplication.overrideCursor() is not None:
136+
QApplication.restoreOverrideCursor()
137+
138+
@Slot(QModelIndex)
139+
def on_expanded(self, index):
140+
if self._closing:
141+
return
142+
self.start_busy()
143+
144+
@Slot(QModelIndex, int, int)
145+
def on_rows_inserted(self, parent, start, end):
146+
self.stop_busy()
147+
148+
@Slot(list, str)
149+
def on_layout_changed(self):
150+
self.stop_busy()
151+
118152
@Slot(str)
119153
def expand_and_resize(self, p):
120154
"""Expands, resizes, and scrolls the tree view to the current directory
121-
when the file model has finished loading the path. Slot for the file
122-
model's directoryLoaded signal. The directoryLoaded signal is emitted only
123-
if the directory has not been cached already. Note, that this is
124-
only used when the open project dialog is opened
155+
when the file model has finished loading the initial path. The directoryLoaded
156+
signal is emitted only if the directory has not been cached already.
125157
126158
Args:
127159
p (str): Directory that has been loaded
128160
"""
129-
if self.starting_up:
161+
if self._starting_up:
130162
current_index = self.ui.treeView_file_system.currentIndex()
131-
self.ui.treeView_file_system.scrollTo(current_index, hint=QAbstractItemView.PositionAtTop)
163+
self.ui.treeView_file_system.scrollTo(current_index, hint=QAbstractItemView.ScrollHint.PositionAtTop)
132164
self.ui.treeView_file_system.expand(current_index)
133-
if p == self.start_path:
165+
if p == self._start_path:
134166
self.ui.treeView_file_system.resizeColumnToContents(0)
135167
self.set_selected_path(current_index)
136-
self.starting_up = False
168+
self._starting_up = False
137169

138170
@Slot()
139171
def validator_state_changed(self):
@@ -162,12 +194,9 @@ def current_index_changed(self, i):
162194
self.remove_directory_from_recents(p, self._qsettings)
163195
return
164196
fm_index = self.file_model.index(p)
165-
self.ui.treeView_file_system.collapseAll()
166-
self.ui.treeView_file_system.setCurrentIndex(fm_index)
167-
self.ui.treeView_file_system.expand(fm_index)
168-
self.ui.treeView_file_system.scrollTo(fm_index, hint=QAbstractItemView.PositionAtTop)
197+
self.collapse_and_expand(fm_index)
169198

170-
@Slot("QModelIndex", "QModelIndex", name="current_changed")
199+
@Slot(QModelIndex, QModelIndex, name="current_changed")
171200
def current_changed(self, current, previous):
172201
"""Processed when the current item in file system tree view has been
173202
changed with keyboard or mouse. Updates the text in combobox.
@@ -178,7 +207,7 @@ def current_changed(self, current, previous):
178207
"""
179208
self.set_selected_path(current)
180209

181-
@Slot("QModelIndex", name="set_selected_path")
210+
@Slot(QModelIndex, name="set_selected_path")
182211
def set_selected_path(self, index):
183212
"""Sets the text in the combobox as the selected path in the file system tree view.
184213
@@ -211,56 +240,56 @@ def go_root(self, checked=False):
211240
"""
212241
self.ui.comboBox_current_path.setCurrentIndex(-1)
213242
root_index = self.file_model.index(QDir.rootPath())
214-
self.ui.treeView_file_system.collapseAll()
215-
self.ui.treeView_file_system.setCurrentIndex(root_index)
216-
self.ui.treeView_file_system.expand(root_index)
217-
self.ui.treeView_file_system.scrollTo(root_index, hint=QAbstractItemView.PositionAtTop)
243+
self.collapse_and_expand(root_index)
218244

219245
@Slot(bool, name="go_home")
220246
def go_home(self, checked=False):
221247
"""Slot for the 'Home' button. Scrolls the treeview to show and select the user's home directory."""
222248
self.ui.comboBox_current_path.setCurrentIndex(-1)
223249
home_index = self.file_model.index(QDir.homePath())
224-
self.ui.treeView_file_system.collapseAll()
225-
self.ui.treeView_file_system.setCurrentIndex(home_index)
226-
self.ui.treeView_file_system.expand(home_index)
227-
self.ui.treeView_file_system.scrollTo(home_index, hint=QAbstractItemView.PositionAtTop)
250+
self.collapse_and_expand(home_index)
228251

229252
@Slot(bool, name="go_documents")
230253
def go_documents(self, checked=False):
231254
"""Slot for the 'Documents' button. Scrolls the treeview to show and select the user's documents directory."""
232-
docs = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
255+
docs = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DocumentsLocation)
233256
if not docs:
234257
return
235258
self.ui.comboBox_current_path.setCurrentIndex(-1)
236259
docs_index = self.file_model.index(docs)
237-
self.ui.treeView_file_system.collapseAll()
238-
self.ui.treeView_file_system.setCurrentIndex(docs_index)
239-
self.ui.treeView_file_system.expand(docs_index)
240-
self.ui.treeView_file_system.scrollTo(docs_index, hint=QAbstractItemView.PositionAtTop)
260+
self.collapse_and_expand(docs_index)
241261

242262
@Slot(bool, name="go_desktop")
243263
def go_desktop(self, checked=False):
244264
"""Slot for the 'Desktop' button. Scrolls the treeview to show and select the user's desktop directory."""
245-
desktop = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) # Return a list
265+
desktop = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DesktopLocation) # Return a list
246266
if not desktop:
247267
return
248268
self.ui.comboBox_current_path.setCurrentIndex(-1)
249269
desktop_index = self.file_model.index(desktop)
270+
self.collapse_and_expand(desktop_index)
271+
272+
def collapse_and_expand(self, index):
273+
"""Collapses all open branches, sets current index to given index, expands the branch, and
274+
scrolls the view to the new index.
275+
276+
Args:
277+
index (QModelIndex): New index
278+
"""
250279
self.ui.treeView_file_system.collapseAll()
251-
self.ui.treeView_file_system.setCurrentIndex(desktop_index)
252-
self.ui.treeView_file_system.expand(desktop_index)
253-
self.ui.treeView_file_system.scrollTo(desktop_index, hint=QAbstractItemView.PositionAtTop)
280+
self.ui.treeView_file_system.setCurrentIndex(index)
281+
self.ui.treeView_file_system.expand(index)
282+
self.ui.treeView_file_system.scrollTo(index, hint=QAbstractItemView.ScrollHint.PositionAtTop)
254283

255-
@Slot("QModelIndex")
284+
@Slot(QModelIndex)
256285
def open_project(self, index):
257286
"""Opens project if index contains a valid Spine Toolbox project.
258287
Slot for the mouse doubleClicked signal. Prevents showing the
259288
'Not a valid spine toolbox project' notification if user just wants
260289
to collapse a directory.
261290
262291
Args:
263-
index (QModelIndex): File model index which was double clicked
292+
index (QModelIndex): File model index which was double-clicked
264293
"""
265294
if not index.isValid():
266295
return
@@ -277,6 +306,8 @@ def done(self, r):
277306
Args:
278307
r (int) Return code
279308
"""
309+
self._closing = True
310+
self._force_reset_cursor()
280311
if r == QDialog.DialogCode.Accepted:
281312
if not os.path.isdir(self.selection()):
282313
notification = Notification(self, "Path does not exist")
@@ -292,6 +323,12 @@ def done(self, r):
292323
self.update_recents(os.path.abspath(os.path.join(self.selection(), os.path.pardir)), self._qsettings)
293324
super().done(r)
294325

326+
def _force_reset_cursor(self):
327+
"""Forces the cursor back to normal when the dialog is closed by double-clicking a project."""
328+
self._loading = False
329+
while QApplication.overrideCursor() is not None:
330+
QApplication.restoreOverrideCursor()
331+
295332
@staticmethod
296333
def update_recents(entry, qsettings):
297334
"""Adds a new entry to QSettings variable that remembers the five most recent project storages.
@@ -340,7 +377,7 @@ def remove_directory_from_recents(p, qsettings):
340377
qsettings.setValue("appSettings/recentProjectStorages", updated_recents)
341378
qsettings.sync() # Commit change immediately
342379

343-
@Slot("QPoint")
380+
@Slot(QPoint)
344381
def show_context_menu(self, pos):
345382
"""Shows the context menu for the QCombobox with a 'Clear history' entry.
346383

0 commit comments

Comments
 (0)