|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a browser with class browsing capabilities. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import os |
|
12 import mimetypes |
|
13 |
|
14 from PyQt4.QtCore import * |
|
15 from PyQt4.QtGui import * |
|
16 |
|
17 from E4Gui.E4Application import e4App |
|
18 |
|
19 from BrowserModel import BrowserModel, \ |
|
20 BrowserDirectoryItem, BrowserFileItem, BrowserClassItem, BrowserMethodItem, \ |
|
21 BrowserClassAttributeItem |
|
22 from BrowserSortFilterProxyModel import BrowserSortFilterProxyModel |
|
23 |
|
24 import UI.PixmapCache |
|
25 import Preferences |
|
26 import Utilities |
|
27 |
|
28 class Browser(QTreeView): |
|
29 """ |
|
30 Class used to display a file system tree. |
|
31 |
|
32 Via the context menu that |
|
33 is displayed by a right click the user can select various actions on |
|
34 the selected file. |
|
35 |
|
36 @signal sourceFile(string, int, string) emitted to open a Python file at a line |
|
37 @signal designerFile(string) emitted to open a Qt-Designer file |
|
38 @signal linguistFile(string) emitted to open a Qt-Linguist (*.ts) file |
|
39 @signal trpreview(string list) emitted to preview a Qt-Linguist (*.qm) file |
|
40 @signal projectFile(string) emitted to open an eric4 project file |
|
41 @signal multiProjectFile(string) emitted to open an eric4 multi project file |
|
42 @signal pixmapFile(string) emitted to open a pixmap file |
|
43 @signal pixmapEditFile(string) emitted to edit a pixmap file |
|
44 @signal svgFile(string) emitted to open a SVG file |
|
45 @signal unittestOpen(string) emitted to open a Python file for a unittest |
|
46 """ |
|
47 def __init__(self, parent = None): |
|
48 """ |
|
49 Constructor |
|
50 |
|
51 @param parent parent widget (QWidget) |
|
52 """ |
|
53 QTreeView.__init__(self, parent) |
|
54 |
|
55 self.setWindowTitle(QApplication.translate('Browser', 'File-Browser')) |
|
56 self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) |
|
57 |
|
58 self.__embeddedBrowser = Preferences.getUI("LayoutFileBrowserEmbedded") |
|
59 |
|
60 self.__model = BrowserModel() |
|
61 self.__sortModel = BrowserSortFilterProxyModel() |
|
62 self.__sortModel.setSourceModel(self.__model) |
|
63 self.setModel(self.__sortModel) |
|
64 |
|
65 self.selectedItemsFilter = [BrowserFileItem] |
|
66 |
|
67 self.setContextMenuPolicy(Qt.CustomContextMenu) |
|
68 self.connect(self, SIGNAL("customContextMenuRequested(const QPoint &)"), |
|
69 self._contextMenuRequested) |
|
70 self.connect(self, SIGNAL("activated(const QModelIndex &)"), self._openItem) |
|
71 self.connect(self, SIGNAL("expanded(const QModelIndex &)"), self._resizeColumns) |
|
72 self.connect(self, SIGNAL("collapsed(const QModelIndex &)"), self._resizeColumns) |
|
73 |
|
74 self.setWhatsThis(QApplication.translate('Browser', |
|
75 """<b>The Browser Window</b>""" |
|
76 """<p>This allows you to easily navigate the hierachy of directories and""" |
|
77 """ files on your system, identify the Python programs and open them up in""" |
|
78 """ a Source Viewer window. The window displays several separate""" |
|
79 """ hierachies.</p>""" |
|
80 """<p>The first hierachy is only shown if you have opened a program for""" |
|
81 """ debugging and it's root is the directory containing that program.""" |
|
82 """ Usually all of the separate files that make up a Python application are""" |
|
83 """ held in the same directory, so this hierachy gives you easy access to""" |
|
84 """ most of what you will need.</p>""" |
|
85 """<p>The next hierachy is used to easily navigate the directories that are""" |
|
86 """ specified in the Python <tt>sys.path</tt> variable.</p>""" |
|
87 """<p>The remaining hierachies allow you navigate your system as a whole.""" |
|
88 """ On a UNIX system there will be a hierachy with <tt>/</tt> at its""" |
|
89 """ root and another with the user home directory.""" |
|
90 """ On a Windows system there will be a hierachy for each drive on the""" |
|
91 """ system.</p>""" |
|
92 """<p>Python programs (i.e. those with a <tt>.py</tt> file name suffix)""" |
|
93 """ are identified in the hierachies with a Python icon.""" |
|
94 """ The right mouse button will popup a menu which lets you""" |
|
95 """ open the file in a Source Viewer window,""" |
|
96 """ open the file for debugging or use it for a unittest run.</p>""" |
|
97 """<p>The context menu of a class, function or method allows you to open""" |
|
98 """ the file defining this class, function or method and will ensure, that""" |
|
99 """ the correct source line is visible.</p>""" |
|
100 """<p>Qt-Designer files (i.e. those with a <tt>.ui</tt> file name suffix)""" |
|
101 """ are shown with a Designer icon. The context menu of these files""" |
|
102 """ allows you to start Qt-Designer with that file.</p>""" |
|
103 """<p>Qt-Linguist files (i.e. those with a <tt>.ts</tt> file name suffix)""" |
|
104 """ are shown with a Linguist icon. The context menu of these files""" |
|
105 """ allows you to start Qt-Linguist with that file.</p>""" |
|
106 )) |
|
107 |
|
108 self.__createPopupMenus() |
|
109 |
|
110 self._init() # perform common initialization tasks |
|
111 |
|
112 def _init(self): |
|
113 """ |
|
114 Protected method to perform initialization tasks common to this |
|
115 base class and all derived classes. |
|
116 """ |
|
117 self.setRootIsDecorated(True) |
|
118 self.setAlternatingRowColors(True) |
|
119 |
|
120 header = self.header() |
|
121 header.setSortIndicator(0, Qt.AscendingOrder) |
|
122 header.setSortIndicatorShown(True) |
|
123 header.setClickable(True) |
|
124 |
|
125 self.setSortingEnabled(True) |
|
126 |
|
127 self.setSelectionMode(QAbstractItemView.ExtendedSelection) |
|
128 self.setSelectionBehavior(QAbstractItemView.SelectRows) |
|
129 |
|
130 self.header().setStretchLastSection(True) |
|
131 self.headerSize0 = 0 |
|
132 self.layoutDisplay() |
|
133 |
|
134 def layoutDisplay(self): |
|
135 """ |
|
136 Public slot to perform a layout operation. |
|
137 """ |
|
138 self.doItemsLayout() |
|
139 self._resizeColumns(QModelIndex()) |
|
140 self._resort() |
|
141 |
|
142 def _resizeColumns(self, index): |
|
143 """ |
|
144 Protected slot to resize the view when items get expanded or collapsed. |
|
145 |
|
146 @param index index of item (QModelIndex) |
|
147 """ |
|
148 w = max(100, self.sizeHintForColumn(0)) |
|
149 if w != self.headerSize0: |
|
150 self.header().resizeSection(0, w) |
|
151 self.headerSize0 = w |
|
152 |
|
153 def _resort(self): |
|
154 """ |
|
155 Protected slot to resort the tree. |
|
156 """ |
|
157 self.model().sort(self.header().sortIndicatorSection(), |
|
158 self.header().sortIndicatorOrder()) |
|
159 |
|
160 def __createPopupMenus(self): |
|
161 """ |
|
162 Private method to generate the various popup menus. |
|
163 """ |
|
164 # create the popup menu for source files |
|
165 self.sourceMenu = QMenu(self) |
|
166 self.sourceMenu.addAction(QApplication.translate('Browser', 'Open'), |
|
167 self._openItem) |
|
168 self.unittestAct = self.sourceMenu.addAction(\ |
|
169 QApplication.translate('Browser', 'Run unittest...'), self.handleUnittest) |
|
170 self.sourceMenu.addAction( |
|
171 QApplication.translate('Browser', 'Copy Path to Clipboard'), |
|
172 self._copyToClipboard) |
|
173 |
|
174 # create the popup menu for general use |
|
175 self.menu = QMenu(self) |
|
176 self.menu.addAction(QApplication.translate('Browser', 'Open'), self._openItem) |
|
177 self.editPixmapAct = \ |
|
178 self.menu.addAction(QApplication.translate('Browser', 'Open in Icon Editor'), |
|
179 self._editPixmap) |
|
180 self.menu.addAction( |
|
181 QApplication.translate('Browser', 'Copy Path to Clipboard'), |
|
182 self._copyToClipboard) |
|
183 if self.__embeddedBrowser in [1, 2]: |
|
184 self.menu.addSeparator() |
|
185 self.menu.addAction(QApplication.translate('Browser', 'Configure...'), |
|
186 self.__configure) |
|
187 |
|
188 # create the menu for multiple selected files |
|
189 self.multiMenu = QMenu(self) |
|
190 self.multiMenu.addAction(QApplication.translate('Browser', 'Open'), |
|
191 self._openItem) |
|
192 if self.__embeddedBrowser in [1, 2]: |
|
193 self.multiMenu.addSeparator() |
|
194 self.multiMenu.addAction(QApplication.translate('Browser', 'Configure...'), |
|
195 self.__configure) |
|
196 |
|
197 # create the directory menu |
|
198 self.dirMenu = QMenu(self) |
|
199 self.dirMenu.addAction(QApplication.translate('Browser', |
|
200 'New toplevel directory...'), |
|
201 self.__newToplevelDir) |
|
202 self.addAsTopLevelAct = self.dirMenu.addAction(\ |
|
203 QApplication.translate('Browser', 'Add as toplevel directory'), |
|
204 self.__addAsToplevelDir) |
|
205 self.removeFromToplevelAct = self.dirMenu.addAction(\ |
|
206 QApplication.translate('Browser', 'Remove from toplevel'), |
|
207 self.__removeToplevel) |
|
208 self.dirMenu.addSeparator() |
|
209 self.dirMenu.addAction(QApplication.translate('Browser', |
|
210 'Find in this directory'), |
|
211 self.__findInDirectory) |
|
212 self.dirMenu.addAction(QApplication.translate('Browser', |
|
213 'Find&&Replace in this directory'), |
|
214 self.__replaceInDirectory) |
|
215 self.dirMenu.addAction( |
|
216 QApplication.translate('Browser', 'Copy Path to Clipboard'), |
|
217 self._copyToClipboard) |
|
218 if self.__embeddedBrowser in [1, 2]: |
|
219 self.dirMenu.addSeparator() |
|
220 self.dirMenu.addAction(QApplication.translate('Browser', 'Configure...'), |
|
221 self.__configure) |
|
222 |
|
223 # create the background menu |
|
224 self.backMenu = QMenu(self) |
|
225 self.backMenu.addAction(QApplication.translate('Browser', |
|
226 'New toplevel directory...'), |
|
227 self.__newToplevelDir) |
|
228 if self.__embeddedBrowser in [1, 2]: |
|
229 self.backMenu.addSeparator() |
|
230 self.backMenu.addAction(QApplication.translate('Browser', 'Configure...'), |
|
231 self.__configure) |
|
232 |
|
233 def mouseDoubleClickEvent(self, mouseEvent): |
|
234 """ |
|
235 Protected method of QAbstractItemView. |
|
236 |
|
237 Reimplemented to disable expanding/collapsing |
|
238 of items when double-clicking. Instead the double-clicked entry is opened. |
|
239 |
|
240 @param mouseEvent the mouse event (QMouseEvent) |
|
241 """ |
|
242 index = self.indexAt(mouseEvent.pos()) |
|
243 if index.isValid(): |
|
244 self._openItem() |
|
245 |
|
246 def _contextMenuRequested(self, coord): |
|
247 """ |
|
248 Protected slot to show the context menu of the listview. |
|
249 |
|
250 @param coord the position of the mouse pointer (QPoint) |
|
251 """ |
|
252 categories = self.getSelectedItemsCountCategorized(\ |
|
253 [BrowserDirectoryItem, BrowserFileItem, |
|
254 BrowserClassItem, BrowserMethodItem]) |
|
255 cnt = categories["sum"] |
|
256 bfcnt = categories[unicode(BrowserFileItem)] |
|
257 if cnt > 1 and cnt == bfcnt: |
|
258 self.multiMenu.popup(self.mapToGlobal(coord)) |
|
259 else: |
|
260 index = self.indexAt(coord) |
|
261 |
|
262 if index.isValid(): |
|
263 self.setCurrentIndex(index) |
|
264 flags = QItemSelectionModel.SelectionFlags(\ |
|
265 QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) |
|
266 self.selectionModel().select(index, flags) |
|
267 |
|
268 itm = self.model().item(index) |
|
269 coord = self.mapToGlobal(coord) |
|
270 if isinstance(itm, BrowserFileItem): |
|
271 if itm.isPythonFile(): |
|
272 if itm.fileName().endswith('.py'): |
|
273 self.unittestAct.setEnabled(True) |
|
274 else: |
|
275 self.unittestAct.setEnabled(False) |
|
276 self.sourceMenu.popup(coord) |
|
277 else: |
|
278 self.editPixmapAct.setVisible(itm.isPixmapFile()) |
|
279 self.menu.popup(coord) |
|
280 elif isinstance(itm, BrowserClassItem) or \ |
|
281 isinstance(itm, BrowserMethodItem): |
|
282 self.menu.popup(coord) |
|
283 elif isinstance(itm, BrowserDirectoryItem): |
|
284 if not index.parent().isValid(): |
|
285 self.removeFromToplevelAct.setEnabled(True) |
|
286 self.addAsTopLevelAct.setEnabled(False) |
|
287 else: |
|
288 self.removeFromToplevelAct.setEnabled(False) |
|
289 self.addAsTopLevelAct.setEnabled(True) |
|
290 self.dirMenu.popup(coord) |
|
291 else: |
|
292 self.backMenu.popup(coord) |
|
293 else: |
|
294 self.backMenu.popup(self.mapToGlobal(coord)) |
|
295 |
|
296 def handlePreferencesChanged(self): |
|
297 """ |
|
298 Public slot used to handle the preferencesChanged signal. |
|
299 """ |
|
300 self.model().preferencesChanged() |
|
301 self._resort() |
|
302 |
|
303 def _openItem(self): |
|
304 """ |
|
305 Protected slot to handle the open popup menu entry. |
|
306 """ |
|
307 itmList = self.getSelectedItems(\ |
|
308 [BrowserFileItem, BrowserClassItem, |
|
309 BrowserMethodItem, BrowserClassAttributeItem]) |
|
310 |
|
311 for itm in itmList: |
|
312 if isinstance(itm, BrowserFileItem): |
|
313 if itm.isPythonFile(): |
|
314 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Python") |
|
315 elif itm.isRubyFile(): |
|
316 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "Ruby") |
|
317 elif itm.isDFile(): |
|
318 self.emit(SIGNAL('sourceFile'), itm.fileName(), 1, "D") |
|
319 elif itm.isDesignerFile(): |
|
320 self.emit(SIGNAL('designerFile'), itm.fileName()) |
|
321 elif itm.isLinguistFile(): |
|
322 if itm.fileExt() == '.ts': |
|
323 self.emit(SIGNAL('linguistFile'), itm.fileName()) |
|
324 else: |
|
325 self.emit(SIGNAL('trpreview'), [itm.fileName()]) |
|
326 elif itm.isProjectFile(): |
|
327 self.emit(SIGNAL('projectFile'), itm.fileName()) |
|
328 elif itm.isMultiProjectFile(): |
|
329 self.emit(SIGNAL('multiProjectFile'), itm.fileName()) |
|
330 elif itm.isIdlFile(): |
|
331 self.emit(SIGNAL('sourceFile'), itm.fileName()) |
|
332 elif itm.isResourcesFile(): |
|
333 self.emit(SIGNAL('sourceFile'), itm.fileName()) |
|
334 elif itm.isPixmapFile(): |
|
335 self.emit(SIGNAL('pixmapFile'), itm.fileName()) |
|
336 elif itm.isSvgFile(): |
|
337 self.emit(SIGNAL('svgFile'), itm.fileName()) |
|
338 else: |
|
339 type_ = mimetypes.guess_type(itm.fileName())[0] |
|
340 if type_ is None or type_.split("/")[0] == "text": |
|
341 self.emit(SIGNAL('sourceFile'), itm.fileName()) |
|
342 else: |
|
343 QDesktopServices.openUrl(QUrl(itm.fileName())) |
|
344 elif isinstance(itm, BrowserClassItem): |
|
345 self.emit(SIGNAL('sourceFile'), itm.fileName(), |
|
346 itm.classObject().lineno) |
|
347 elif isinstance(itm, BrowserMethodItem): |
|
348 self.emit(SIGNAL('sourceFile'), itm.fileName(), |
|
349 itm.functionObject().lineno) |
|
350 elif isinstance(itm, BrowserClassAttributeItem): |
|
351 self.emit(SIGNAL('sourceFile'), itm.fileName(), |
|
352 itm.attributeObject().lineno) |
|
353 |
|
354 def _editPixmap(self): |
|
355 """ |
|
356 Protected slot to handle the open in icon editor popup menu entry. |
|
357 """ |
|
358 itmList = self.getSelectedItems([BrowserFileItem]) |
|
359 |
|
360 for itm in itmList: |
|
361 if isinstance(itm, BrowserFileItem): |
|
362 if itm.isPixmapFile(): |
|
363 self.emit(SIGNAL('pixmapEditFile'), itm.fileName()) |
|
364 |
|
365 def _copyToClipboard(self): |
|
366 """ |
|
367 Protected method to copy the text shown for an entry to the clipboard. |
|
368 """ |
|
369 itm = self.model().item(self.currentIndex()) |
|
370 try: |
|
371 fn = itm.fileName() |
|
372 except AttributeError: |
|
373 try: |
|
374 fn = itm.dirName() |
|
375 except AttributeError: |
|
376 fn = "" |
|
377 |
|
378 if fn: |
|
379 cb = QApplication.clipboard() |
|
380 cb.setText(fn) |
|
381 |
|
382 def handleUnittest(self): |
|
383 """ |
|
384 Public slot to handle the unittest popup menu entry. |
|
385 """ |
|
386 try: |
|
387 index = self.currentIndex() |
|
388 itm = self.model().item(index) |
|
389 pyfn = itm.fileName() |
|
390 except AttributeError: |
|
391 pyfn = None |
|
392 |
|
393 if pyfn is not None: |
|
394 self.emit(SIGNAL('unittestOpen'), pyfn) |
|
395 |
|
396 def __newToplevelDir(self): |
|
397 """ |
|
398 Private slot to handle the New toplevel directory popup menu entry. |
|
399 """ |
|
400 dname = QFileDialog.getExistingDirectory(\ |
|
401 None, |
|
402 QApplication.translate('Browser', "New toplevel directory"), |
|
403 "", |
|
404 QFileDialog.Options(QFileDialog.ShowDirsOnly)) |
|
405 if dname: |
|
406 dname = os.path.abspath(Utilities.toNativeSeparators(dname)) |
|
407 self.__model.addTopLevelDir(dname) |
|
408 |
|
409 def __removeToplevel(self): |
|
410 """ |
|
411 Private slot to handle the Remove from toplevel popup menu entry. |
|
412 """ |
|
413 index = self.currentIndex() |
|
414 sindex = self.model().mapToSource(index) |
|
415 self.__model.removeToplevelDir(sindex) |
|
416 |
|
417 def __addAsToplevelDir(self): |
|
418 """ |
|
419 Private slot to handle the Add as toplevel directory popup menu entry. |
|
420 """ |
|
421 index = self.currentIndex() |
|
422 dname = self.model().item(index).dirName() |
|
423 self.__model.addTopLevelDir(dname) |
|
424 |
|
425 def __findInDirectory(self): |
|
426 """ |
|
427 Private slot to handle the Find in directory popup menu entry. |
|
428 """ |
|
429 index = self.currentIndex() |
|
430 searchDir = self.model().item(index).dirName() |
|
431 |
|
432 findFilesDialog = e4App().getObject("FindFilesDialog") |
|
433 findFilesDialog.setSearchDirectory(searchDir) |
|
434 findFilesDialog.show() |
|
435 findFilesDialog.raise_() |
|
436 findFilesDialog.activateWindow() |
|
437 |
|
438 def __replaceInDirectory(self): |
|
439 """ |
|
440 Private slot to handle the Find&Replace in directory popup menu entry. |
|
441 """ |
|
442 index = self.currentIndex() |
|
443 searchDir = self.model().item(index).dirName() |
|
444 |
|
445 replaceFilesDialog = e4App().getObject("ReplaceFilesDialog") |
|
446 replaceFilesDialog.setSearchDirectory(searchDir) |
|
447 replaceFilesDialog.show() |
|
448 replaceFilesDialog.raise_() |
|
449 replaceFilesDialog.activateWindow() |
|
450 |
|
451 def handleProgramChange(self,fn): |
|
452 """ |
|
453 Public slot to handle the programChange signal. |
|
454 """ |
|
455 self.__model.programChange(os.path.dirname(fn)) |
|
456 |
|
457 def wantedItem(self, itm, filter=None): |
|
458 """ |
|
459 Public method to check type of an item. |
|
460 |
|
461 @param itm the item to check (BrowserItem) |
|
462 @param filter list of classes to check against |
|
463 @return flag indicating item is a valid type (boolean) |
|
464 """ |
|
465 if filter is None: |
|
466 filter = self.selectedItemsFilter |
|
467 for typ in filter: |
|
468 if isinstance(itm, typ): |
|
469 return True |
|
470 return False |
|
471 |
|
472 def getSelectedItems(self, filter=None): |
|
473 """ |
|
474 Public method to get the selected items. |
|
475 |
|
476 @param filter list of classes to check against |
|
477 @return list of selected items (list of BroweserItem) |
|
478 """ |
|
479 selectedItems = [] |
|
480 indexes = self.selectedIndexes() |
|
481 for index in indexes: |
|
482 if index.column() == 0: |
|
483 itm = self.model().item(index) |
|
484 if self.wantedItem(itm, filter): |
|
485 selectedItems.append(itm) |
|
486 return selectedItems |
|
487 |
|
488 def getSelectedItemsCount(self, filter=None): |
|
489 """ |
|
490 Public method to get the count of items selected. |
|
491 |
|
492 @param filter list of classes to check against |
|
493 @return count of items selected (integer) |
|
494 """ |
|
495 count = 0 |
|
496 indexes = self.selectedIndexes() |
|
497 for index in indexes: |
|
498 if index.column() == 0: |
|
499 itm = self.model().item(index) |
|
500 if self.wantedItem(itm, filter): |
|
501 count += 1 |
|
502 return count |
|
503 |
|
504 def getSelectedItemsCountCategorized(self, filter=None): |
|
505 """ |
|
506 Public method to get a categorized count of selected items. |
|
507 |
|
508 @param filter list of classes to check against |
|
509 @return a dictionary containing the counts of items belonging |
|
510 to the individual filter classes. The keys of the dictionary |
|
511 are the unicode representation of the classes given in the |
|
512 filter (i.e. unicode(filterClass)). The dictionary contains |
|
513 an additional entry with key "sum", that stores the sum of |
|
514 all selected entries fulfilling the filter criteria. |
|
515 """ |
|
516 if filter is None: |
|
517 filter = self.selectedItemsFilter |
|
518 categories = {} |
|
519 categories["sum"] = 0 |
|
520 for typ in filter: |
|
521 categories[unicode(typ)] = 0 |
|
522 |
|
523 indexes = self.selectedIndexes() |
|
524 for index in indexes: |
|
525 if index.column() == 0: |
|
526 itm = self.model().item(index) |
|
527 for typ in filter: |
|
528 if isinstance(itm, typ): |
|
529 categories["sum"] += 1 |
|
530 categories[unicode(typ)] += 1 |
|
531 |
|
532 return categories |
|
533 |
|
534 def saveToplevelDirs(self): |
|
535 """ |
|
536 Public slot to save the toplevel directories. |
|
537 """ |
|
538 self.__model.saveToplevelDirs() |
|
539 |
|
540 def __configure(self): |
|
541 """ |
|
542 Private method to open the configuration dialog. |
|
543 """ |
|
544 if self.__embeddedBrowser == 1: |
|
545 e4App().getObject("UserInterface").showPreferences("debuggerGeneralPage") |
|
546 elif self.__embeddedBrowser == 2: |
|
547 e4App().getObject("UserInterface").showPreferences("projectBrowserPage") |