39 |
42 |
40 |
43 |
41 class ProjectBrowserItemMixin: |
44 class ProjectBrowserItemMixin: |
42 """ |
45 """ |
43 Class implementing common methods of project browser items. |
46 Class implementing common methods of project browser items. |
44 |
47 |
45 It is meant to be used as a mixin class. |
48 It is meant to be used as a mixin class. |
46 """ |
49 """ |
|
50 |
47 def __init__(self, type_, bold=False): |
51 def __init__(self, type_, bold=False): |
48 """ |
52 """ |
49 Constructor |
53 Constructor |
50 |
54 |
51 @param type_ type of file/directory in the project |
55 @param type_ type of file/directory in the project |
52 @param bold flag indicating a highlighted font |
56 @param bold flag indicating a highlighted font |
53 """ |
57 """ |
54 self._projectTypes = [type_] |
58 self._projectTypes = [type_] |
55 self.bold = bold |
59 self.bold = bold |
56 self.vcsState = " " |
60 self.vcsState = " " |
57 |
61 |
58 def getTextColor(self): |
62 def getTextColor(self): |
59 """ |
63 """ |
60 Public method to get the items text color. |
64 Public method to get the items text color. |
61 |
65 |
62 @return text color (QColor) |
66 @return text color (QColor) |
63 """ |
67 """ |
64 if self.bold: |
68 if self.bold: |
65 return Preferences.getProjectBrowserColour("Highlighted") |
69 return Preferences.getProjectBrowserColour("Highlighted") |
66 else: |
70 else: |
67 return None |
71 return None |
68 |
72 |
69 def setVcsState(self, state): |
73 def setVcsState(self, state): |
70 """ |
74 """ |
71 Public method to set the items VCS state. |
75 Public method to set the items VCS state. |
72 |
76 |
73 @param state VCS state (one of A, C, M, U or " ") (string) |
77 @param state VCS state (one of A, C, M, U or " ") (string) |
74 """ |
78 """ |
75 self.vcsState = state |
79 self.vcsState = state |
76 |
80 |
77 def addVcsStatus(self, vcsStatus): |
81 def addVcsStatus(self, vcsStatus): |
78 """ |
82 """ |
79 Public method to add the VCS status. |
83 Public method to add the VCS status. |
80 |
84 |
81 @param vcsStatus VCS status text (string) |
85 @param vcsStatus VCS status text (string) |
82 """ |
86 """ |
83 self.itemData.append(vcsStatus) |
87 self.itemData.append(vcsStatus) |
84 |
88 |
85 def setVcsStatus(self, vcsStatus): |
89 def setVcsStatus(self, vcsStatus): |
86 """ |
90 """ |
87 Public method to set the VCS status. |
91 Public method to set the VCS status. |
88 |
92 |
89 @param vcsStatus VCS status text (string) |
93 @param vcsStatus VCS status text (string) |
90 """ |
94 """ |
91 self.itemData[1] = vcsStatus |
95 self.itemData[1] = vcsStatus |
92 |
96 |
93 def getProjectTypes(self): |
97 def getProjectTypes(self): |
94 """ |
98 """ |
95 Public method to get the project type. |
99 Public method to get the project type. |
96 |
100 |
97 @return project type |
101 @return project type |
98 """ |
102 """ |
99 return self._projectTypes[:] |
103 return self._projectTypes[:] |
100 |
104 |
101 def addProjectType(self, type_): |
105 def addProjectType(self, type_): |
102 """ |
106 """ |
103 Public method to add a type to the list. |
107 Public method to add a type to the list. |
104 |
108 |
105 @param type_ type to add to the list |
109 @param type_ type to add to the list |
106 """ |
110 """ |
107 self._projectTypes.append(type_) |
111 self._projectTypes.append(type_) |
108 |
112 |
109 |
113 |
110 class ProjectBrowserSimpleDirectoryItem(BrowserItem, ProjectBrowserItemMixin): |
114 class ProjectBrowserSimpleDirectoryItem(BrowserItem, ProjectBrowserItemMixin): |
111 """ |
115 """ |
112 Class implementing the data structure for project browser simple directory |
116 Class implementing the data structure for project browser simple directory |
113 items. |
117 items. |
114 """ |
118 """ |
|
119 |
115 def __init__(self, parent, projectType, text, path=""): |
120 def __init__(self, parent, projectType, text, path=""): |
116 """ |
121 """ |
117 Constructor |
122 Constructor |
118 |
123 |
119 @param parent parent item |
124 @param parent parent item |
120 @param projectType type of file/directory in the project |
125 @param projectType type of file/directory in the project |
121 @param text text to be displayed (string) |
126 @param text text to be displayed (string) |
122 @param path path of the directory (string) |
127 @param path path of the directory (string) |
123 """ |
128 """ |
124 BrowserItem.__init__(self, parent, text) |
129 BrowserItem.__init__(self, parent, text) |
125 ProjectBrowserItemMixin.__init__(self, projectType) |
130 ProjectBrowserItemMixin.__init__(self, projectType) |
126 |
131 |
127 self._dirName = path |
132 self._dirName = path |
128 if not os.path.isdir(self._dirName): |
133 if not os.path.isdir(self._dirName): |
129 self._dirName = os.path.dirname(self._dirName) |
134 self._dirName = os.path.dirname(self._dirName) |
130 |
135 |
131 self.type_ = ProjectBrowserItemSimpleDirectory |
136 self.type_ = ProjectBrowserItemSimpleDirectory |
132 if os.path.lexists(self._dirName) and os.path.islink(self._dirName): |
137 if os.path.lexists(self._dirName) and os.path.islink(self._dirName): |
133 self.symlink = True |
138 self.symlink = True |
134 self.icon = UI.PixmapCache.getSymlinkIcon("dirClosed") |
139 self.icon = UI.PixmapCache.getSymlinkIcon("dirClosed") |
135 else: |
140 else: |
136 self.icon = UI.PixmapCache.getIcon("dirClosed") |
141 self.icon = UI.PixmapCache.getIcon("dirClosed") |
137 |
142 |
138 def setName(self, dinfo, full=True): |
143 def setName(self, dinfo, full=True): |
139 """ |
144 """ |
140 Public method to set the directory name. |
145 Public method to set the directory name. |
141 |
146 |
142 @param dinfo dinfo is the string for the directory (string) |
147 @param dinfo dinfo is the string for the directory (string) |
143 @param full flag indicating full pathname should be displayed (boolean) |
148 @param full flag indicating full pathname should be displayed (boolean) |
144 """ |
149 """ |
145 self._dirName = os.path.abspath(dinfo) |
150 self._dirName = os.path.abspath(dinfo) |
146 self.itemData[0] = os.path.basename(self._dirName) |
151 self.itemData[0] = os.path.basename(self._dirName) |
147 |
152 |
148 def dirName(self): |
153 def dirName(self): |
149 """ |
154 """ |
150 Public method returning the directory name. |
155 Public method returning the directory name. |
151 |
156 |
152 @return directory name (string) |
157 @return directory name (string) |
153 """ |
158 """ |
154 return self._dirName |
159 return self._dirName |
155 |
160 |
156 def name(self): |
161 def name(self): |
157 """ |
162 """ |
158 Public method to return the name of the item. |
163 Public method to return the name of the item. |
159 |
164 |
160 @return name of the item (string) |
165 @return name of the item (string) |
161 """ |
166 """ |
162 return self._dirName |
167 return self._dirName |
163 |
168 |
164 def lessThan(self, other, column, order): |
169 def lessThan(self, other, column, order): |
165 """ |
170 """ |
166 Public method to check, if the item is less than the other one. |
171 Public method to check, if the item is less than the other one. |
167 |
172 |
168 @param other reference to item to compare against (BrowserItem) |
173 @param other reference to item to compare against (BrowserItem) |
169 @param column column number to use for the comparison (integer) |
174 @param column column number to use for the comparison (integer) |
170 @param order sort order (Qt.SortOrder) (for special sorting) |
175 @param order sort order (Qt.SortOrder) (for special sorting) |
171 @return true, if this item is less than other (boolean) |
176 @return true, if this item is less than other (boolean) |
172 """ |
177 """ |
173 if ( |
178 if issubclass(other.__class__, BrowserFileItem) and Preferences.getUI( |
174 issubclass(other.__class__, BrowserFileItem) and |
179 "BrowsersListFoldersFirst" |
175 Preferences.getUI("BrowsersListFoldersFirst") |
|
176 ): |
180 ): |
177 return order == Qt.SortOrder.AscendingOrder |
181 return order == Qt.SortOrder.AscendingOrder |
178 |
182 |
179 return BrowserItem.lessThan(self, other, column, order) |
183 return BrowserItem.lessThan(self, other, column, order) |
180 |
184 |
181 |
185 |
182 class ProjectBrowserDirectoryItem(BrowserDirectoryItem, |
186 class ProjectBrowserDirectoryItem(BrowserDirectoryItem, ProjectBrowserItemMixin): |
183 ProjectBrowserItemMixin): |
|
184 """ |
187 """ |
185 Class implementing the data structure for project browser directory items. |
188 Class implementing the data structure for project browser directory items. |
186 """ |
189 """ |
|
190 |
187 def __init__(self, parent, dinfo, projectType, full=True, bold=False): |
191 def __init__(self, parent, dinfo, projectType, full=True, bold=False): |
188 """ |
192 """ |
189 Constructor |
193 Constructor |
190 |
194 |
191 @param parent parent item |
195 @param parent parent item |
192 @param dinfo dinfo is the string for the directory (string) |
196 @param dinfo dinfo is the string for the directory (string) |
193 @param projectType type of file/directory in the project |
197 @param projectType type of file/directory in the project |
194 @param full flag indicating full pathname should be displayed (boolean) |
198 @param full flag indicating full pathname should be displayed (boolean) |
195 @param bold flag indicating a highlighted font (boolean) |
199 @param bold flag indicating a highlighted font (boolean) |
196 """ |
200 """ |
197 BrowserDirectoryItem.__init__(self, parent, dinfo, full) |
201 BrowserDirectoryItem.__init__(self, parent, dinfo, full) |
198 ProjectBrowserItemMixin.__init__(self, projectType, bold) |
202 ProjectBrowserItemMixin.__init__(self, projectType, bold) |
199 |
203 |
200 self.type_ = ProjectBrowserItemDirectory |
204 self.type_ = ProjectBrowserItemDirectory |
201 |
205 |
202 |
206 |
203 class ProjectBrowserFileItem(BrowserFileItem, ProjectBrowserItemMixin): |
207 class ProjectBrowserFileItem(BrowserFileItem, ProjectBrowserItemMixin): |
204 """ |
208 """ |
205 Class implementing the data structure for project browser file items. |
209 Class implementing the data structure for project browser file items. |
206 """ |
210 """ |
207 def __init__(self, parent, finfo, projectType, full=True, bold=False, |
211 |
208 sourceLanguage=""): |
212 def __init__( |
|
213 self, parent, finfo, projectType, full=True, bold=False, sourceLanguage="" |
|
214 ): |
209 """ |
215 """ |
210 Constructor |
216 Constructor |
211 |
217 |
212 @param parent parent item |
218 @param parent parent item |
213 @param finfo the string for the file (string) |
219 @param finfo the string for the file (string) |
214 @param projectType type of file/directory in the project |
220 @param projectType type of file/directory in the project |
215 @param full flag indicating full pathname should be displayed (boolean) |
221 @param full flag indicating full pathname should be displayed (boolean) |
216 @param bold flag indicating a highlighted font (boolean) |
222 @param bold flag indicating a highlighted font (boolean) |
217 @param sourceLanguage source code language of the project (string) |
223 @param sourceLanguage source code language of the project (string) |
218 """ |
224 """ |
219 BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage) |
225 BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage) |
220 ProjectBrowserItemMixin.__init__(self, projectType, bold) |
226 ProjectBrowserItemMixin.__init__(self, projectType, bold) |
221 |
227 |
222 self.type_ = ProjectBrowserItemFile |
228 self.type_ = ProjectBrowserItemFile |
223 |
229 |
224 |
230 |
225 class ProjectBrowserModel(BrowserModel): |
231 class ProjectBrowserModel(BrowserModel): |
226 """ |
232 """ |
227 Class implementing the project browser model. |
233 Class implementing the project browser model. |
228 |
234 |
229 @signal vcsStateChanged(str) emitted after the VCS state has changed |
235 @signal vcsStateChanged(str) emitted after the VCS state has changed |
230 """ |
236 """ |
|
237 |
231 vcsStateChanged = pyqtSignal(str) |
238 vcsStateChanged = pyqtSignal(str) |
232 |
239 |
233 def __init__(self, parent): |
240 def __init__(self, parent): |
234 """ |
241 """ |
235 Constructor |
242 Constructor |
236 |
243 |
237 @param parent reference to parent object (Project.Project) |
244 @param parent reference to parent object (Project.Project) |
238 """ |
245 """ |
239 super().__init__(parent, nopopulate=True) |
246 super().__init__(parent, nopopulate=True) |
240 |
247 |
241 rootData = self.tr("Name") |
248 rootData = self.tr("Name") |
242 self.rootItem = BrowserItem(None, rootData) |
249 self.rootItem = BrowserItem(None, rootData) |
243 self.rootItem.itemData.append(self.tr("VCS Status")) |
250 self.rootItem.itemData.append(self.tr("VCS Status")) |
244 |
251 |
245 self.progDir = None |
252 self.progDir = None |
246 self.project = parent |
253 self.project = parent |
247 |
254 |
248 self.watchedItems = {} |
255 self.watchedItems = {} |
249 self.__watcherActive = True |
256 self.__watcherActive = True |
250 self.watcher = QFileSystemWatcher(self) |
257 self.watcher = QFileSystemWatcher(self) |
251 self.watcher.directoryChanged.connect(self.directoryChanged) |
258 self.watcher.directoryChanged.connect(self.directoryChanged) |
252 |
259 |
253 self.inRefresh = False |
260 self.inRefresh = False |
254 |
261 |
255 self.projectBrowserTypes = { |
262 self.projectBrowserTypes = { |
256 "SOURCES": ProjectBrowserSourceType, |
263 "SOURCES": ProjectBrowserSourceType, |
257 "FORMS": ProjectBrowserFormType, |
264 "FORMS": ProjectBrowserFormType, |
258 "RESOURCES": ProjectBrowserResourceType, |
265 "RESOURCES": ProjectBrowserResourceType, |
259 "INTERFACES": ProjectBrowserInterfaceType, |
266 "INTERFACES": ProjectBrowserInterfaceType, |
260 "PROTOCOLS": ProjectBrowserProtocolsType, |
267 "PROTOCOLS": ProjectBrowserProtocolsType, |
261 "TRANSLATIONS": ProjectBrowserTranslationType, |
268 "TRANSLATIONS": ProjectBrowserTranslationType, |
262 "OTHERS": ProjectBrowserOthersType, |
269 "OTHERS": ProjectBrowserOthersType, |
263 } |
270 } |
264 |
271 |
265 self.colorNames = { |
272 self.colorNames = { |
266 "A": "VcsAdded", |
273 "A": "VcsAdded", |
267 "M": "VcsModified", |
274 "M": "VcsModified", |
268 "O": "VcsRemoved", |
275 "O": "VcsRemoved", |
269 "R": "VcsReplaced", |
276 "R": "VcsReplaced", |
277 "O": Preferences.getProjectBrowserColour(self.colorNames["O"]), |
284 "O": Preferences.getProjectBrowserColour(self.colorNames["O"]), |
278 "R": Preferences.getProjectBrowserColour(self.colorNames["R"]), |
285 "R": Preferences.getProjectBrowserColour(self.colorNames["R"]), |
279 "U": Preferences.getProjectBrowserColour(self.colorNames["U"]), |
286 "U": Preferences.getProjectBrowserColour(self.colorNames["U"]), |
280 "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]), |
287 "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]), |
281 } |
288 } |
282 |
289 |
283 self.highLightColor = Preferences.getProjectBrowserColour( |
290 self.highLightColor = Preferences.getProjectBrowserColour("Highlighted") |
284 "Highlighted") |
|
285 # needed by preferencesChanged() |
291 # needed by preferencesChanged() |
286 |
292 |
287 self.vcsStatusReport = {} |
293 self.vcsStatusReport = {} |
288 |
294 |
289 def data(self, index, role): |
295 def data(self, index, role): |
290 """ |
296 """ |
291 Public method to get data of an item. |
297 Public method to get data of an item. |
292 |
298 |
293 @param index index of the data to retrieve (QModelIndex) |
299 @param index index of the data to retrieve (QModelIndex) |
294 @param role role of data (Qt.ItemDataRole) |
300 @param role role of data (Qt.ItemDataRole) |
295 @return requested data |
301 @return requested data |
296 """ |
302 """ |
297 if not index.isValid(): |
303 if not index.isValid(): |
298 return None |
304 return None |
299 |
305 |
300 if role == Qt.ItemDataRole.ForegroundRole: |
306 if role == Qt.ItemDataRole.ForegroundRole: |
301 if index.column() == 0: |
307 if index.column() == 0: |
302 try: |
308 try: |
303 return index.internalPointer().getTextColor() |
309 return index.internalPointer().getTextColor() |
304 except AttributeError: |
310 except AttributeError: |
305 return None |
311 return None |
306 elif role == Qt.ItemDataRole.BackgroundRole: |
312 elif role == Qt.ItemDataRole.BackgroundRole: |
307 try: |
313 try: |
308 col = self.itemBackgroundColors[ |
314 col = self.itemBackgroundColors[index.internalPointer().vcsState] |
309 index.internalPointer().vcsState] |
|
310 if col.isValid(): |
315 if col.isValid(): |
311 return col |
316 return col |
312 else: |
317 else: |
313 return None |
318 return None |
314 except AttributeError: |
319 except AttributeError: |
315 return None |
320 return None |
316 except KeyError: |
321 except KeyError: |
317 return None |
322 return None |
318 |
323 |
319 return BrowserModel.data(self, index, role) |
324 return BrowserModel.data(self, index, role) |
320 |
325 |
321 def populateItem(self, parentItem, repopulate=False): |
326 def populateItem(self, parentItem, repopulate=False): |
322 """ |
327 """ |
323 Public method to populate an item's subtree. |
328 Public method to populate an item's subtree. |
324 |
329 |
325 @param parentItem reference to the item to be populated |
330 @param parentItem reference to the item to be populated |
326 @param repopulate flag indicating a repopulation (boolean) |
331 @param repopulate flag indicating a repopulation (boolean) |
327 """ |
332 """ |
328 if parentItem.type() == ProjectBrowserItemSimpleDirectory: |
333 if parentItem.type() == ProjectBrowserItemSimpleDirectory: |
329 return # nothing to do |
334 return # nothing to do |
335 BrowserModel.populateItem(self, parentItem, repopulate) |
340 BrowserModel.populateItem(self, parentItem, repopulate) |
336 |
341 |
337 def populateProjectDirectoryItem(self, parentItem, repopulate=False): |
342 def populateProjectDirectoryItem(self, parentItem, repopulate=False): |
338 """ |
343 """ |
339 Public method to populate a directory item's subtree. |
344 Public method to populate a directory item's subtree. |
340 |
345 |
341 @param parentItem reference to the directory item to be populated |
346 @param parentItem reference to the directory item to be populated |
342 @param repopulate flag indicating a repopulation (boolean) |
347 @param repopulate flag indicating a repopulation (boolean) |
343 """ |
348 """ |
344 self._addWatchedItem(parentItem) |
349 self._addWatchedItem(parentItem) |
345 |
350 |
346 qdir = QDir(parentItem.dirName()) |
351 qdir = QDir(parentItem.dirName()) |
347 |
352 |
348 fileFilter = ( |
353 fileFilter = ( |
349 ( |
354 (QDir.Filter.AllEntries | QDir.Filter.Hidden | QDir.Filter.NoDotAndDotDot) |
350 QDir.Filter.AllEntries | |
355 if Preferences.getProject("BrowsersListHiddenFiles") |
351 QDir.Filter.Hidden | |
356 else QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot |
352 QDir.Filter.NoDotAndDotDot |
|
353 ) |
|
354 if Preferences.getProject("BrowsersListHiddenFiles") else |
|
355 QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot |
|
356 ) |
357 ) |
357 entryInfoList = qdir.entryInfoList(fileFilter) |
358 entryInfoList = qdir.entryInfoList(fileFilter) |
358 |
359 |
359 if len(entryInfoList) > 0: |
360 if len(entryInfoList) > 0: |
360 if repopulate: |
361 if repopulate: |
361 self.beginInsertRows(self.createIndex( |
362 self.beginInsertRows( |
362 parentItem.row(), 0, parentItem), |
363 self.createIndex(parentItem.row(), 0, parentItem), |
363 0, len(entryInfoList) - 1) |
364 0, |
|
365 len(entryInfoList) - 1, |
|
366 ) |
364 states = {} |
367 states = {} |
365 if self.project.vcs is not None: |
368 if self.project.vcs is not None: |
366 for f in entryInfoList: |
369 for f in entryInfoList: |
367 fname = f.absoluteFilePath() |
370 fname = f.absoluteFilePath() |
368 states[os.path.normcase(fname)] = 0 |
371 states[os.path.normcase(fname)] = 0 |
369 dname = parentItem.dirName() |
372 dname = parentItem.dirName() |
370 self.project.vcs.clearStatusCache() |
373 self.project.vcs.clearStatusCache() |
371 states = self.project.vcs.vcsAllRegisteredStates(states, dname) |
374 states = self.project.vcs.vcsAllRegisteredStates(states, dname) |
372 |
375 |
373 for f in entryInfoList: |
376 for f in entryInfoList: |
374 node = ( |
377 node = ( |
375 ProjectBrowserDirectoryItem( |
378 ProjectBrowserDirectoryItem( |
376 parentItem, |
379 parentItem, |
377 Utilities.toNativeSeparators(f.absoluteFilePath()), |
380 Utilities.toNativeSeparators(f.absoluteFilePath()), |
378 parentItem.getProjectTypes()[0], False) |
381 parentItem.getProjectTypes()[0], |
379 if f.isDir() else |
382 False, |
380 ProjectBrowserFileItem( |
383 ) |
|
384 if f.isDir() |
|
385 else ProjectBrowserFileItem( |
381 parentItem, |
386 parentItem, |
382 Utilities.toNativeSeparators(f.absoluteFilePath()), |
387 Utilities.toNativeSeparators(f.absoluteFilePath()), |
383 parentItem.getProjectTypes()[0]) |
388 parentItem.getProjectTypes()[0], |
|
389 ) |
384 ) |
390 ) |
385 if self.project.vcs is not None: |
391 if self.project.vcs is not None: |
386 fname = f.absoluteFilePath() |
392 fname = f.absoluteFilePath() |
387 if ( |
393 if ( |
388 states[os.path.normcase(fname)] == |
394 states[os.path.normcase(fname)] |
389 self.project.vcs.canBeCommitted |
395 == self.project.vcs.canBeCommitted |
390 ): |
396 ): |
391 node.addVcsStatus(self.project.vcs.vcsName()) |
397 node.addVcsStatus(self.project.vcs.vcsName()) |
392 self.project.clearStatusMonitorCachedState( |
398 self.project.clearStatusMonitorCachedState(f.absoluteFilePath()) |
393 f.absoluteFilePath()) |
|
394 else: |
399 else: |
395 node.addVcsStatus(self.tr("local")) |
400 node.addVcsStatus(self.tr("local")) |
396 else: |
401 else: |
397 node.addVcsStatus("") |
402 node.addVcsStatus("") |
398 self._addItem(node, parentItem) |
403 self._addItem(node, parentItem) |
402 def projectClosed(self): |
407 def projectClosed(self): |
403 """ |
408 """ |
404 Public method called after a project has been closed. |
409 Public method called after a project has been closed. |
405 """ |
410 """ |
406 self.__vcsStatus = {} |
411 self.__vcsStatus = {} |
407 |
412 |
408 self.watchedItems = {} |
413 self.watchedItems = {} |
409 watchedDirs = self.watcher.directories() |
414 watchedDirs = self.watcher.directories() |
410 if watchedDirs: |
415 if watchedDirs: |
411 self.watcher.removePaths(watchedDirs) |
416 self.watcher.removePaths(watchedDirs) |
412 |
417 |
413 self.rootItem.removeChildren() |
418 self.rootItem.removeChildren() |
414 self.beginResetModel() |
419 self.beginResetModel() |
415 self.endResetModel() |
420 self.endResetModel() |
416 |
421 |
417 # reset the module parser cache |
422 # reset the module parser cache |
418 Utilities.ModuleParser.resetParsedModules() |
423 Utilities.ModuleParser.resetParsedModules() |
419 |
424 |
420 def projectOpened(self): |
425 def projectOpened(self): |
421 """ |
426 """ |
422 Public method used to populate the model after a project has been |
427 Public method used to populate the model after a project has been |
423 opened. |
428 opened. |
424 """ |
429 """ |
425 self.__vcsStatus = {} |
430 self.__vcsStatus = {} |
426 states = {} |
431 states = {} |
427 keys = list(self.projectBrowserTypes.keys())[:] |
432 keys = list(self.projectBrowserTypes.keys())[:] |
428 |
433 |
429 if self.project.vcs is not None: |
434 if self.project.vcs is not None: |
430 for key in keys: |
435 for key in keys: |
431 for fn in self.project.pdata[key]: |
436 for fn in self.project.pdata[key]: |
432 states[os.path.normcase( |
437 states[os.path.normcase(os.path.join(self.project.ppath, fn))] = 0 |
433 os.path.join(self.project.ppath, fn))] = 0 |
438 |
434 |
|
435 self.project.vcs.clearStatusCache() |
439 self.project.vcs.clearStatusCache() |
436 states = self.project.vcs.vcsAllRegisteredStates( |
440 states = self.project.vcs.vcsAllRegisteredStates(states, self.project.ppath) |
437 states, self.project.ppath) |
441 |
438 |
|
439 self.inRefresh = True |
442 self.inRefresh = True |
440 for key in keys: |
443 for key in keys: |
441 # Show the entry in bold in the others browser to make it more |
444 # Show the entry in bold in the others browser to make it more |
442 # distinguishable |
445 # distinguishable |
443 bold = key == "OTHERS" |
446 bold = key == "OTHERS" |
444 sourceLanguage = ( |
447 sourceLanguage = ( |
445 self.project.getProjectLanguage() if key == "SOURCES" else "" |
448 self.project.getProjectLanguage() if key == "SOURCES" else "" |
446 ) |
449 ) |
447 |
450 |
448 for fn in self.project.pdata[key]: |
451 for fn in self.project.pdata[key]: |
449 fname = os.path.join(self.project.ppath, fn) |
452 fname = os.path.join(self.project.ppath, fn) |
450 parentItem, dt = self.findParentItemByName( |
453 parentItem, dt = self.findParentItemByName( |
451 self.projectBrowserTypes[key], fn) |
454 self.projectBrowserTypes[key], fn |
|
455 ) |
452 itm = ( |
456 itm = ( |
453 ProjectBrowserDirectoryItem( |
457 ProjectBrowserDirectoryItem( |
454 parentItem, fname, self.projectBrowserTypes[key], |
458 parentItem, fname, self.projectBrowserTypes[key], False, bold |
455 False, bold) |
459 ) |
456 if os.path.isdir(fname) else |
460 if os.path.isdir(fname) |
457 ProjectBrowserFileItem( |
461 else ProjectBrowserFileItem( |
458 parentItem, fname, self.projectBrowserTypes[key], |
462 parentItem, |
459 False, bold, sourceLanguage=sourceLanguage) |
463 fname, |
|
464 self.projectBrowserTypes[key], |
|
465 False, |
|
466 bold, |
|
467 sourceLanguage=sourceLanguage, |
|
468 ) |
460 ) |
469 ) |
461 self._addItem(itm, parentItem) |
470 self._addItem(itm, parentItem) |
462 if self.project.vcs is not None: |
471 if self.project.vcs is not None: |
463 if ( |
472 if ( |
464 states[os.path.normcase(fname)] == |
473 states[os.path.normcase(fname)] |
465 self.project.vcs.canBeCommitted |
474 == self.project.vcs.canBeCommitted |
466 ): |
475 ): |
467 itm.addVcsStatus(self.project.vcs.vcsName()) |
476 itm.addVcsStatus(self.project.vcs.vcsName()) |
468 else: |
477 else: |
469 itm.addVcsStatus(self.tr("local")) |
478 itm.addVcsStatus(self.tr("local")) |
470 else: |
479 else: |
474 self.endResetModel() |
483 self.endResetModel() |
475 |
484 |
476 def findParentItemByName(self, type_, name, dontSplit=False): |
485 def findParentItemByName(self, type_, name, dontSplit=False): |
477 """ |
486 """ |
478 Public method to find an item given its name. |
487 Public method to find an item given its name. |
479 |
488 |
480 <b>Note</b>: This method creates all necessary parent items, if they |
489 <b>Note</b>: This method creates all necessary parent items, if they |
481 don't exist. |
490 don't exist. |
482 |
491 |
483 @param type_ type of the item |
492 @param type_ type of the item |
484 @param name name of the item (string) |
493 @param name name of the item (string) |
485 @param dontSplit flag indicating the name should not be split (boolean) |
494 @param dontSplit flag indicating the name should not be split (boolean) |
486 @return reference to the item found and the new display name (string) |
495 @return reference to the item found and the new display name (string) |
487 """ |
496 """ |
488 if dontSplit: |
497 if dontSplit: |
489 pathlist = [] |
498 pathlist = [] |
490 pathlist.append(name) |
499 pathlist.append(name) |
491 pathlist.append("ignore_me") |
500 pathlist.append("ignore_me") |
492 else: |
501 else: |
493 pathlist = re.split(r'/|\\', name) |
502 pathlist = re.split(r"/|\\", name) |
494 |
503 |
495 if len(pathlist) > 1: |
504 if len(pathlist) > 1: |
496 olditem = self.rootItem |
505 olditem = self.rootItem |
497 path = self.project.ppath |
506 path = self.project.ppath |
498 for p in pathlist[:-1]: |
507 for p in pathlist[:-1]: |
499 itm = self.findChildItem(p, 0, olditem) |
508 itm = self.findChildItem(p, 0, olditem) |
500 path = os.path.join(path, p) |
509 path = os.path.join(path, p) |
501 if itm is None: |
510 if itm is None: |
502 itm = ProjectBrowserSimpleDirectoryItem( |
511 itm = ProjectBrowserSimpleDirectoryItem(olditem, type_, p, path) |
503 olditem, type_, p, path) |
|
504 self.__addVCSStatus(itm, path) |
512 self.__addVCSStatus(itm, path) |
505 if self.inRefresh: |
513 if self.inRefresh: |
506 self._addItem(itm, olditem) |
514 self._addItem(itm, olditem) |
507 else: |
515 else: |
508 if olditem == self.rootItem: |
516 if olditem == self.rootItem: |
509 oldindex = QModelIndex() |
517 oldindex = QModelIndex() |
510 else: |
518 else: |
511 oldindex = self.createIndex( |
519 oldindex = self.createIndex(olditem.row(), 0, olditem) |
512 olditem.row(), 0, olditem) |
|
513 self.addItem(itm, oldindex) |
520 self.addItem(itm, oldindex) |
514 else: |
521 else: |
515 if type_ and type_ not in itm.getProjectTypes(): |
522 if type_ and type_ not in itm.getProjectTypes(): |
516 itm.addProjectType(type_) |
523 itm.addProjectType(type_) |
517 olditem = itm |
524 olditem = itm |
518 return (itm, pathlist[-1]) |
525 return (itm, pathlist[-1]) |
519 else: |
526 else: |
520 return (self.rootItem, name) |
527 return (self.rootItem, name) |
521 |
528 |
522 def findChildItem(self, text, column, parentItem=None): |
529 def findChildItem(self, text, column, parentItem=None): |
523 """ |
530 """ |
524 Public method to find a child item given some text. |
531 Public method to find a child item given some text. |
525 |
532 |
526 @param text text to search for (string) |
533 @param text text to search for (string) |
527 @param column column to search in (integer) |
534 @param column column to search in (integer) |
528 @param parentItem reference to parent item |
535 @param parentItem reference to parent item |
529 @return reference to the item found |
536 @return reference to the item found |
530 """ |
537 """ |
531 if parentItem is None: |
538 if parentItem is None: |
532 parentItem = self.rootItem |
539 parentItem = self.rootItem |
533 |
540 |
534 for itm in parentItem.children(): |
541 for itm in parentItem.children(): |
535 if itm.data(column) == text: |
542 if itm.data(column) == text: |
536 return itm |
543 return itm |
537 |
544 |
538 return None |
545 return None |
539 |
546 |
540 def addNewItem(self, typeString, name, additionalTypeStrings=None): |
547 def addNewItem(self, typeString, name, additionalTypeStrings=None): |
541 """ |
548 """ |
542 Public method to add a new item to the model. |
549 Public method to add a new item to the model. |
543 |
550 |
544 @param typeString string denoting the type of the new item (string) |
551 @param typeString string denoting the type of the new item (string) |
545 @param name name of the new item (string) |
552 @param name name of the new item (string) |
546 @param additionalTypeStrings names of additional types (list of string) |
553 @param additionalTypeStrings names of additional types (list of string) |
547 """ |
554 """ |
548 # Show the entry in bold in the others browser to make it more |
555 # Show the entry in bold in the others browser to make it more |
549 # distinguishable |
556 # distinguishable |
550 bold = typeString == "OTHERS" |
557 bold = typeString == "OTHERS" |
551 |
558 |
552 fname = os.path.join(self.project.ppath, name) |
559 fname = os.path.join(self.project.ppath, name) |
553 parentItem, dt = self.findParentItemByName( |
560 parentItem, dt = self.findParentItemByName( |
554 self.projectBrowserTypes[typeString], name) |
561 self.projectBrowserTypes[typeString], name |
|
562 ) |
555 parentIndex = ( |
563 parentIndex = ( |
556 QModelIndex() |
564 QModelIndex() |
557 if parentItem == self.rootItem else |
565 if parentItem == self.rootItem |
558 self.createIndex(parentItem.row(), 0, parentItem) |
566 else self.createIndex(parentItem.row(), 0, parentItem) |
559 ) |
567 ) |
560 if os.path.isdir(fname): |
568 if os.path.isdir(fname): |
561 itm = ProjectBrowserDirectoryItem( |
569 itm = ProjectBrowserDirectoryItem( |
562 parentItem, fname, self.projectBrowserTypes[typeString], |
570 parentItem, fname, self.projectBrowserTypes[typeString], False, bold |
563 False, bold) |
571 ) |
564 else: |
572 else: |
565 if typeString == "SOURCES": |
573 if typeString == "SOURCES": |
566 sourceLanguage = self.project.getProjectLanguage() |
574 sourceLanguage = self.project.getProjectLanguage() |
567 else: |
575 else: |
568 sourceLanguage = "" |
576 sourceLanguage = "" |
569 itm = ProjectBrowserFileItem( |
577 itm = ProjectBrowserFileItem( |
570 parentItem, fname, self.projectBrowserTypes[typeString], |
578 parentItem, |
571 False, bold, sourceLanguage=sourceLanguage) |
579 fname, |
|
580 self.projectBrowserTypes[typeString], |
|
581 False, |
|
582 bold, |
|
583 sourceLanguage=sourceLanguage, |
|
584 ) |
572 self.__addVCSStatus(itm, fname) |
585 self.__addVCSStatus(itm, fname) |
573 if additionalTypeStrings: |
586 if additionalTypeStrings: |
574 for additionalTypeString in additionalTypeStrings: |
587 for additionalTypeString in additionalTypeStrings: |
575 type_ = self.projectBrowserTypes[additionalTypeString] |
588 type_ = self.projectBrowserTypes[additionalTypeString] |
576 itm.addProjectType(type_) |
589 itm.addProjectType(type_) |
577 self.addItem(itm, parentIndex) |
590 self.addItem(itm, parentIndex) |
578 |
591 |
579 def renameItem(self, name, newFilename): |
592 def renameItem(self, name, newFilename): |
580 """ |
593 """ |
581 Public method to rename an item. |
594 Public method to rename an item. |
582 |
595 |
583 @param name the old display name (string) |
596 @param name the old display name (string) |
584 @param newFilename new filename of the item (string) |
597 @param newFilename new filename of the item (string) |
585 """ |
598 """ |
586 itm = self.findItem(name) |
599 itm = self.findItem(name) |
587 if itm is None: |
600 if itm is None: |
588 return |
601 return |
589 |
602 |
590 index = self.createIndex(itm.row(), 0, itm) |
603 index = self.createIndex(itm.row(), 0, itm) |
591 itm.setName(newFilename, full=False) |
604 itm.setName(newFilename, full=False) |
592 self.dataChanged.emit(index, index) |
605 self.dataChanged.emit(index, index) |
593 self.repopulateItem(newFilename) |
606 self.repopulateItem(newFilename) |
594 |
607 |
595 def findItem(self, name): |
608 def findItem(self, name): |
596 """ |
609 """ |
597 Public method to find an item given its name. |
610 Public method to find an item given its name. |
598 |
611 |
599 @param name name of the item (string) |
612 @param name name of the item (string) |
600 @return reference to the item found |
613 @return reference to the item found |
601 """ |
614 """ |
602 if QDir.isAbsolutePath(name): |
615 if QDir.isAbsolutePath(name): |
603 name = self.project.getRelativePath(name) |
616 name = self.project.getRelativePath(name) |
604 pathlist = re.split(r'/|\\', name) |
617 pathlist = re.split(r"/|\\", name) |
605 if len(pathlist) > 0: |
618 if len(pathlist) > 0: |
606 olditem = self.rootItem |
619 olditem = self.rootItem |
607 for p in pathlist: |
620 for p in pathlist: |
608 itm = self.findChildItem(p, 0, olditem) |
621 itm = self.findChildItem(p, 0, olditem) |
609 if itm is None: |
622 if itm is None: |
610 return None |
623 return None |
611 olditem = itm |
624 olditem = itm |
612 return itm |
625 return itm |
613 else: |
626 else: |
614 return None |
627 return None |
615 |
628 |
616 def itemIndexByName(self, name): |
629 def itemIndexByName(self, name): |
617 """ |
630 """ |
618 Public method to find an item's index given its name. |
631 Public method to find an item's index given its name. |
619 |
632 |
620 @param name name of the item (string) |
633 @param name name of the item (string) |
621 @return index of the item found (QModelIndex) |
634 @return index of the item found (QModelIndex) |
622 """ |
635 """ |
623 itm = self.findItem(name) |
636 itm = self.findItem(name) |
624 index = self.createIndex(itm.row(), 0, itm) if itm else QModelIndex() |
637 index = self.createIndex(itm.row(), 0, itm) if itm else QModelIndex() |
625 return index |
638 return index |
626 |
639 |
627 def itemIndexByNameAndLine(self, name, lineno): |
640 def itemIndexByNameAndLine(self, name, lineno): |
628 """ |
641 """ |
629 Public method to find an item's index given its name. |
642 Public method to find an item's index given its name. |
630 |
643 |
631 @param name name of the item (string) |
644 @param name name of the item (string) |
632 @param lineno one based line number of the item (integer) |
645 @param lineno one based line number of the item (integer) |
633 @return index of the item found (QModelIndex) |
646 @return index of the item found (QModelIndex) |
634 """ |
647 """ |
635 index = QModelIndex() |
648 index = QModelIndex() |
636 itm = self.findItem(name) |
649 itm = self.findItem(name) |
637 if ( |
650 if itm is not None and isinstance(itm, ProjectBrowserFileItem): |
638 itm is not None and |
|
639 isinstance(itm, ProjectBrowserFileItem) |
|
640 ): |
|
641 olditem = itm |
651 olditem = itm |
642 autoPopulate = Preferences.getProject("AutoPopulateItems") |
652 autoPopulate = Preferences.getProject("AutoPopulateItems") |
643 while itm is not None: |
653 while itm is not None: |
644 if not itm.isPopulated(): |
654 if not itm.isPopulated(): |
645 if itm.isLazyPopulated() and autoPopulate: |
655 if itm.isLazyPopulated() and autoPopulate: |
648 break |
658 break |
649 for child in itm.children(): |
659 for child in itm.children(): |
650 with contextlib.suppress(AttributeError): |
660 with contextlib.suppress(AttributeError): |
651 start, end = child.boundaries() |
661 start, end = child.boundaries() |
652 if end == -1: |
662 if end == -1: |
653 end = 1000000 # assume end of file |
663 end = 1000000 # assume end of file |
654 if start <= lineno <= end: |
664 if start <= lineno <= end: |
655 itm = child |
665 itm = child |
656 break |
666 break |
657 else: |
667 else: |
658 itm = None |
668 itm = None |
659 if itm: |
669 if itm: |
660 olditem = itm |
670 olditem = itm |
661 index = self.createIndex(olditem.row(), 0, olditem) |
671 index = self.createIndex(olditem.row(), 0, olditem) |
662 |
672 |
663 return index |
673 return index |
664 |
674 |
665 def startFileSystemMonitoring(self): |
675 def startFileSystemMonitoring(self): |
666 """ |
676 """ |
667 Public method to (re)start monitoring the project file system. |
677 Public method to (re)start monitoring the project file system. |
668 """ |
678 """ |
669 self.__watcherActive = True |
679 self.__watcherActive = True |
670 |
680 |
671 def stopFileSystemMonitoring(self): |
681 def stopFileSystemMonitoring(self): |
672 """ |
682 """ |
673 Public method to stop monitoring the project file system. |
683 Public method to stop monitoring the project file system. |
674 """ |
684 """ |
675 self.__watcherActive = False |
685 self.__watcherActive = False |
676 |
686 |
677 def directoryChanged(self, path): |
687 def directoryChanged(self, path): |
678 """ |
688 """ |
679 Public slot to handle the directoryChanged signal of the watcher. |
689 Public slot to handle the directoryChanged signal of the watcher. |
680 |
690 |
681 @param path path of the directory (string) |
691 @param path path of the directory (string) |
682 """ |
692 """ |
683 if not self.__watcherActive: |
693 if not self.__watcherActive: |
684 return |
694 return |
685 |
695 |
686 if path not in self.watchedItems: |
696 if path not in self.watchedItems: |
687 # just ignore the situation we don't have a reference to the item |
697 # just ignore the situation we don't have a reference to the item |
688 return |
698 return |
689 |
699 |
690 fileFilter = ( |
700 fileFilter = ( |
691 ( |
701 (QDir.Filter.AllEntries | QDir.Filter.Hidden | QDir.Filter.NoDotAndDotDot) |
692 QDir.Filter.AllEntries | |
702 if Preferences.getProject("BrowsersListHiddenFiles") |
693 QDir.Filter.Hidden | |
703 else QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot |
694 QDir.Filter.NoDotAndDotDot |
|
695 ) |
|
696 if Preferences.getProject("BrowsersListHiddenFiles") else |
|
697 QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot |
|
698 ) |
704 ) |
699 |
705 |
700 for itm in self.watchedItems[path]: |
706 for itm in self.watchedItems[path]: |
701 oldCnt = itm.childCount() |
707 oldCnt = itm.childCount() |
702 |
708 |
703 qdir = QDir(itm.dirName()) |
709 qdir = QDir(itm.dirName()) |
704 |
710 |
705 entryInfoList = qdir.entryInfoList(fileFilter) |
711 entryInfoList = qdir.entryInfoList(fileFilter) |
706 |
712 |
707 # step 1: check for new entries |
713 # step 1: check for new entries |
708 children = itm.children() |
714 children = itm.children() |
709 for f in entryInfoList: |
715 for f in entryInfoList: |
710 fpath = Utilities.toNativeSeparators(f.absoluteFilePath()) |
716 fpath = Utilities.toNativeSeparators(f.absoluteFilePath()) |
711 childFound = False |
717 childFound = False |
798 parentItem = item.parent() |
804 parentItem = item.parent() |
799 if name and parentItem is not self.rootItem: |
805 if name and parentItem is not self.rootItem: |
800 self.__updateVCSStatus(parentItem, name, recursive) |
806 self.__updateVCSStatus(parentItem, name, recursive) |
801 else: |
807 else: |
802 item.setVcsStatus("") |
808 item.setVcsStatus("") |
803 |
809 |
804 index = self.createIndex(item.row(), 0, item) |
810 index = self.createIndex(item.row(), 0, item) |
805 self.dataChanged.emit(index, index) |
811 self.dataChanged.emit(index, index) |
806 |
812 |
807 def updateVCSStatus(self, name, recursive=True): |
813 def updateVCSStatus(self, name, recursive=True): |
808 """ |
814 """ |
809 Public method used to update the vcs status of a node. |
815 Public method used to update the vcs status of a node. |
810 |
816 |
811 @param name filename belonging to this item (string) |
817 @param name filename belonging to this item (string) |
812 @param recursive flag indicating a recursive update (boolean) |
818 @param recursive flag indicating a recursive update (boolean) |
813 """ |
819 """ |
814 item = self.findItem(name) |
820 item = self.findItem(name) |
815 if item: |
821 if item: |
816 self.__updateVCSStatus(item, name, recursive) |
822 self.__updateVCSStatus(item, name, recursive) |
817 |
823 |
818 def removeItem(self, name): |
824 def removeItem(self, name): |
819 """ |
825 """ |
820 Public method to remove a named item. |
826 Public method to remove a named item. |
821 |
827 |
822 @param name file or directory name of the item (string). |
828 @param name file or directory name of the item (string). |
823 """ |
829 """ |
824 fname = os.path.basename(name) |
830 fname = os.path.basename(name) |
825 parentItem = self.findParentItemByName(0, name)[0] |
831 parentItem = self.findParentItemByName(0, name)[0] |
826 parentIndex = ( |
832 parentIndex = ( |
827 QModelIndex() |
833 QModelIndex() |
828 if parentItem == self.rootItem else |
834 if parentItem == self.rootItem |
829 self.createIndex(parentItem.row(), 0, parentItem) |
835 else self.createIndex(parentItem.row(), 0, parentItem) |
830 ) |
836 ) |
831 childItem = self.findChildItem(fname, 0, parentItem) |
837 childItem = self.findChildItem(fname, 0, parentItem) |
832 if childItem is not None: |
838 if childItem is not None: |
833 self.beginRemoveRows(parentIndex, childItem.row(), childItem.row()) |
839 self.beginRemoveRows(parentIndex, childItem.row(), childItem.row()) |
834 parentItem.removeChild(childItem) |
840 parentItem.removeChild(childItem) |
835 self.endRemoveRows() |
841 self.endRemoveRows() |
836 |
842 |
837 def repopulateItem(self, name): |
843 def repopulateItem(self, name): |
838 """ |
844 """ |
839 Public method to repopulate an item. |
845 Public method to repopulate an item. |
840 |
846 |
841 @param name name of the file relative to the project root (string) |
847 @param name name of the file relative to the project root (string) |
842 """ |
848 """ |
843 itm = self.findItem(name) |
849 itm = self.findItem(name) |
844 if itm is None: |
850 if itm is None: |
845 return |
851 return |
846 |
852 |
847 if itm.isLazyPopulated(): |
853 if itm.isLazyPopulated(): |
848 if not itm.isPopulated(): |
854 if not itm.isPopulated(): |
849 # item is not populated yet, nothing to do |
855 # item is not populated yet, nothing to do |
850 return |
856 return |
851 |
857 |
852 if itm.childCount(): |
858 if itm.childCount(): |
853 index = self.createIndex(itm.row(), 0, itm) |
859 index = self.createIndex(itm.row(), 0, itm) |
854 self.beginRemoveRows(index, 0, itm.childCount() - 1) |
860 self.beginRemoveRows(index, 0, itm.childCount() - 1) |
855 itm.removeChildren() |
861 itm.removeChildren() |
856 self.endRemoveRows() |
862 self.endRemoveRows() |
857 Utilities.ModuleParser.resetParsedModule( |
863 Utilities.ModuleParser.resetParsedModule( |
858 os.path.join(self.project.ppath, name)) |
864 os.path.join(self.project.ppath, name) |
859 |
865 ) |
|
866 |
860 self.populateItem(itm, True) |
867 self.populateItem(itm, True) |
861 |
868 |
862 def projectPropertiesChanged(self): |
869 def projectPropertiesChanged(self): |
863 """ |
870 """ |
864 Public method to react on a change of the project properties. |
871 Public method to react on a change of the project properties. |
865 """ |
872 """ |
866 # nothing to do for now |
873 # nothing to do for now |
867 return |
874 return |
868 |
875 |
869 def changeVCSStates(self, statesList): |
876 def changeVCSStates(self, statesList): |
870 """ |
877 """ |
871 Public slot to record the (non normal) VCS states. |
878 Public slot to record the (non normal) VCS states. |
872 |
879 |
873 @param statesList list of VCS state entries (list of strings) giving |
880 @param statesList list of VCS state entries (list of strings) giving |
874 the states in the first column and the path relative to the project |
881 the states in the first column and the path relative to the project |
875 directory starting with the third column. The allowed status flags |
882 directory starting with the third column. The allowed status flags |
876 are: |
883 are: |
877 <ul> |
884 <ul> |