~ubuntu-branches/ubuntu/vivid/frescobaldi/vivid

« back to all changes in this revision

Viewing changes to python/kateshell/mainwindow.py

  • Committer: Package Import Robot
  • Author(s): Ryan Kavanagh
  • Date: 2012-01-03 16:20:11 UTC
  • mfrom: (1.4.1)
  • Revision ID: package-import@ubuntu.com-20120103162011-tsjkwl4sntwmprea
Tags: 2.0.0-1
* New upstream release 
* Drop the following uneeded patches:
  + 01_checkmodules_no_python-kde4_build-dep.diff
  + 02_no_pyc.diff
  + 04_no_binary_lilypond_upgrades.diff
* Needs new dependency python-poppler-qt4
* Update debian/watch for new download path
* Update copyright file with new holders and years

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
2
 
#
3
 
# Copyright (c) 2008, 2009, 2010 by Wilbert Berendsen
4
 
#
5
 
# This program is free software; you can redistribute it and/or
6
 
# modify it under the terms of the GNU General Public License
7
 
# as published by the Free Software Foundation; either version 2
8
 
# of the License, or (at your option) any later version.
9
 
#
10
 
# This program is distributed in the hope that it will be useful,
11
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 
# GNU General Public License for more details.
14
 
#
15
 
# You should have received a copy of the GNU General Public License
16
 
# along with this program; if not, write to the Free Software
17
 
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
 
# See http://www.gnu.org/licenses/ for more information.
19
 
 
20
 
from __future__ import unicode_literals
21
 
 
22
 
import itertools, os, re, sip, weakref
23
 
 
24
 
from PyQt4.QtCore import QEvent, QTimer, Qt, SLOT, pyqtSignature
25
 
from PyQt4.QtGui import (
26
 
    QAction, QActionGroup, QDialog, QKeySequence, QLabel, QPixmap, QSplitter,
27
 
    QStackedWidget, QTabBar, QVBoxLayout, QWidget)
28
 
from PyKDE4.kdecore import (
29
 
    KConfig, KGlobal, KPluginLoader, KToolInvocation, KUrl, i18n)
30
 
from PyKDE4.kdeui import (
31
 
    KAcceleratorManager, KAction, KActionCollection, KActionMenu, KDialog,
32
 
    KEditToolBar, KHBox, KIcon, KKeySequenceWidget, KMenu, KMessageBox,
33
 
    KMultiTabBar, KShortcut, KShortcutsDialog, KShortcutsEditor,
34
 
    KStandardAction, KStandardGuiItem, KToggleFullScreenAction, KVBox)
35
 
from PyKDE4.kparts import KParts
36
 
from PyKDE4.ktexteditor import KTextEditor
37
 
from PyKDE4.kio import KEncodingFileDialog
38
 
 
39
 
from signals import Signal
40
 
 
41
 
import kateshell.app
42
 
from kateshell.app import cacheresult, naturalsort
43
 
 
44
 
# Easily get our global config
45
 
def config(group="kateshell"):
46
 
    return KGlobal.config().group(group)
47
 
 
48
 
 
49
 
Top = KMultiTabBar.Top
50
 
Right = KMultiTabBar.Right
51
 
Bottom = KMultiTabBar.Bottom
52
 
Left = KMultiTabBar.Left
53
 
 
54
 
 
55
 
def addAccelerators(actions):
56
 
    """Adds accelerators to the list of actions.
57
 
    
58
 
    Actions that have accelerators are skipped, but the accelerators they use
59
 
    are recorded. This can be used for e.g. menus that are created on the fly,
60
 
    and not picked up by KAcceleratorManager.
61
 
    
62
 
    """
63
 
    todo, used = [], []
64
 
    for a in actions:
65
 
        if a.text():
66
 
            m = re.search(r'&(\w)', a.text())
67
 
            used.append(m.group(1).lower()) if m else todo.append(a)
68
 
    for a in todo:
69
 
        text = a.text()
70
 
        for m in itertools.chain(re.finditer(r'\b\w', text),
71
 
                                 re.finditer(r'\B\w', text)):
72
 
            if m.group().lower() not in used:
73
 
                used.append(m.group().lower())
74
 
                a.setText(text[:m.start()] + '&' + text[m.start():])
75
 
                break
76
 
 
77
 
 
78
 
class MainWindow(KParts.MainWindow):
79
 
    """An editor main window.
80
 
    
81
 
    Emits the following (Python) signals:
82
 
    - currentDocumentChanged(Document) if the active document changes or its url.
83
 
    - aboutToClose() when the window will be closed.
84
 
    
85
 
    """
86
 
    currentDocumentChanged = Signal()
87
 
    aboutToClose = Signal()
88
 
    
89
 
    def __init__(self, app):
90
 
        KParts.MainWindow.__init__(self)
91
 
        self.app = app
92
 
        self._currentDoc = None
93
 
        self.docks = {}
94
 
        self.tools = {}
95
 
        
96
 
        # status bar
97
 
        sb = self.statusBar()
98
 
        self.sb_linecol = QLabel(sb)
99
 
        sb.addWidget(self.sb_linecol, 0)
100
 
        
101
 
        self.sb_modified = QLabel(sb)
102
 
        self.sb_modified.setFixedSize(16, 16)
103
 
        sb.addWidget(self.sb_modified, 0)
104
 
        
105
 
        self.sb_insmode = QLabel(sb)
106
 
        sb.addWidget(self.sb_insmode, 0)
107
 
        
108
 
        self.sb_selmode = QLabel(sb)
109
 
        sb.addWidget(self.sb_selmode, 0)
110
 
        
111
 
        tab_bottom = TabBar(Bottom, sb)
112
 
        sb.addWidget(tab_bottom, 0)
113
 
 
114
 
        # window layout
115
 
        h = KHBox(self)
116
 
        self.setCentralWidget(h)
117
 
        tab_left = TabBar(Left, h)
118
 
        s = QSplitter(Qt.Horizontal, h)
119
 
        tab_right = TabBar(Right, h)
120
 
 
121
 
        self.docks[Left] = Dock(s, tab_left, "go-previous", i18n("Left Sidebar"))
122
 
        s.addWidget(self.docks[Left])
123
 
        v = KVBox(s)
124
 
        s.addWidget(v)
125
 
        self.docks[Right] = Dock(s, tab_right, "go-next", i18n("Right Sidebar"))
126
 
        s.addWidget(self.docks[Right])
127
 
        
128
 
        s.setStretchFactor(0, 0)
129
 
        s.setStretchFactor(1, 1)
130
 
        s.setStretchFactor(2, 1)
131
 
        s.setChildrenCollapsible(False)
132
 
        s.setSizes((100, 400, 600))
133
 
        
134
 
        tab_top = TabBar(Top, v)
135
 
        s1 = QSplitter(Qt.Vertical, v)
136
 
        
137
 
        self.docks[Top] = Dock(s1, tab_top, "go-up", i18n("Top Sidebar"))
138
 
        s1.addWidget(self.docks[Top])
139
 
        # tabbar and editor view widget stack together in one widget
140
 
        w = QWidget()
141
 
        layout = QVBoxLayout()
142
 
        w.setLayout(layout)
143
 
        layout.setContentsMargins(0, 0, 0, 0)
144
 
        layout.setSpacing(0)
145
 
        self.viewTabs = self.createViewTabBar()
146
 
        layout.addWidget(self.viewTabs)
147
 
        self.viewStack = QStackedWidget()
148
 
        layout.addWidget(self.viewStack)
149
 
        s1.addWidget(w)
150
 
        self.docks[Bottom] = Dock(s1, tab_bottom, "go-down", i18n("Bottom Sidebar"))
151
 
        s1.addWidget(self.docks[Bottom])
152
 
 
153
 
        s1.setStretchFactor(0, 0)
154
 
        s1.setStretchFactor(1, 1)
155
 
        s1.setStretchFactor(2, 0)
156
 
        s1.setChildrenCollapsible(False)
157
 
        
158
 
        # Set some reasonable default sizes for top and bottom dock, to
159
 
        # prevent the embedded terminal taking up a too large default space.
160
 
        s1.setSizes((140, 200, 140))
161
 
        
162
 
        self.viewStack.setMinimumSize(200, 100)
163
 
 
164
 
        self._selectionActions = []
165
 
        self.setupActions() # Let subclasses add more actions
166
 
        self.setupTools()   # Initialize the tools before loading ui.rc
167
 
        self.setStandardToolBarMenuEnabled(True)
168
 
        self.createShellGUI(True) # ui.rc is loaded automagically
169
 
        
170
 
        if not self.initialGeometrySet():
171
 
            self.resize(700, 480)
172
 
        
173
 
        self.setupGeneratedMenus()
174
 
        self.setAutoSaveSettings()
175
 
        self.loadSettings()
176
 
        self.setAcceptDrops(True)
177
 
        app.documentCreated.connect(self.addToRecentFiles)
178
 
        app.documentMaterialized.connect(self.addDocument)
179
 
        app.activeChanged.connect(self.setCurrentDocument)
180
 
        app.documentClosed.connect(self.removeDocument)
181
 
        self.sessionManager().sessionChanged.connect(self.updateCaption)
182
 
        
183
 
    def setupActions(self):
184
 
        self.act('file_new', KStandardAction.New, self.newDocument)
185
 
        self.act('file_open', KStandardAction.Open, self.openDocument)
186
 
        self.act('file_close', KStandardAction.Close,
187
 
            lambda: self.app.activeDocument().close())
188
 
        self.act('file_save', KStandardAction.Save,
189
 
            lambda: self.app.activeDocument().save())
190
 
        self.act('file_save_as', KStandardAction.SaveAs,
191
 
            lambda: self.app.activeDocument().saveAs())
192
 
        self.act('file_close_other', i18n("Close Other Documents"),
193
 
            self.closeOtherDocuments)
194
 
        self.act('file_quit', KStandardAction.Quit, self.app.quit)
195
 
        self.act('doc_back', KStandardAction.Back, self.app.back)
196
 
        self.act('doc_forward', KStandardAction.Forward, self.app.forward)
197
 
        self.showPath = self.act('options_show_full_path', i18n("Show Path"),
198
 
            self.updateCaption)
199
 
        self.showPath.setCheckable(True)
200
 
        self.showTabs = self.act('options_show_tabs', i18n("Show Document Tabs"),
201
 
            lambda: self.viewTabs.setVisible(self.showTabs.isChecked()))
202
 
        self.showTabs.setCheckable(True)
203
 
        
204
 
        # full screen
205
 
        a = self.actionCollection().addAction(KStandardAction.FullScreen, 'fullscreen')
206
 
        a.toggled.connect(lambda t: KToggleFullScreenAction.setFullScreen(self, t))
207
 
        # recent files.
208
 
        self.openRecent = KStandardAction.openRecent(
209
 
            self, SLOT("slotOpenRecent(KUrl)"), self)
210
 
        self.actionCollection().addAction(
211
 
            self.openRecent.objectName(), self.openRecent)
212
 
        self.act('options_configure_toolbars', KStandardAction.ConfigureToolbars,
213
 
            self.editToolbars)
214
 
        self.act('options_configure_keys', KStandardAction.KeyBindings,
215
 
            self.editKeys)
216
 
        
217
 
        # tool views submenu
218
 
        a = KActionMenu(i18n("&Tool Views"), self)
219
 
        self.actionCollection().addAction('options_toolviews', a)
220
 
        def makefunc(action):
221
 
            def populate():
222
 
                menu = action.menu()
223
 
                menu.clear()
224
 
                for tool in self.tools.itervalues():
225
 
                    menu.addSeparator()
226
 
                    menu.addAction(tool.action())
227
 
                    tool.addMenuActions(menu)
228
 
            return populate
229
 
        a.menu().aboutToShow.connect(makefunc(a))
230
 
        
231
 
        # sessions menu
232
 
        @self.onAction(i18n("New..."), "document-new")
233
 
        def sessions_new():
234
 
            self.sessionManager().new()
235
 
            
236
 
        @self.onAction(KStandardGuiItem.save().text(), "document-save")
237
 
        def sessions_save():
238
 
            self.sessionManager().save()
239
 
            
240
 
        @self.onAction(i18n("Manage Sessions..."), "view-choose")
241
 
        def sessions_manage():
242
 
            self.sessionManager().manage()
243
 
            
244
 
        
245
 
    def setupTools(self):
246
 
        """Implement this to create the Tool instances.
247
 
        
248
 
        This is called before the ui.rc file is loaded, so the user can
249
 
        configure the keyboard shortcuts for the tools.
250
 
        """
251
 
        pass
252
 
    
253
 
    def xmlGuiContainer(self, name):
254
 
        """Returns the XMLGUI container with name.
255
 
        
256
 
        If not present, the local ui.rc file is probably erroneous,
257
 
        inform the user via a message box.
258
 
        
259
 
        """
260
 
        obj = self.factory().container(name, self)
261
 
        if obj:
262
 
            return obj
263
 
        else:
264
 
            KMessageBox.error(self, i18n(
265
 
                "Could not find the XMLGUI container \"%1\".\n\n"
266
 
                "Probably the local ui.rc file contains errors. "
267
 
                "It is recommended to delete this file because elements in the "
268
 
                "user interface will be missing. "
269
 
                "This is the full path of the file:\n\n%2\n",
270
 
                name, os.path.join(
271
 
                    KGlobal.dirs().saveLocation('appdata'),
272
 
                    self.xmlFile())))
273
 
                
274
 
    def setupGeneratedMenus(self):
275
 
        """This should setup menus that are generated on show.
276
 
        
277
 
        The generated menus that are setup here must be rebound to the XMLGUI if
278
 
        the toolbars are reconfigured by the user, that's why they must be setup
279
 
        in this method. This method is called again if the user changes the
280
 
        toolbars.
281
 
        
282
 
        """
283
 
        # Set up the documents menu so that it shows all open documents.
284
 
        docMenu = self.xmlGuiContainer("documents")
285
 
        if docMenu:
286
 
            docGroup = QActionGroup(docMenu)
287
 
            docGroup.setExclusive(True)
288
 
            docGroup.triggered.connect(lambda a: a.doc().setActive())
289
 
                
290
 
            def populateDocMenu():
291
 
                for a in docGroup.actions():
292
 
                    sip.delete(a)
293
 
                for d in self.app.documents:
294
 
                    a = KAction(d.documentName(), docGroup)
295
 
                    a.setCheckable(True)
296
 
                    a.doc = weakref.ref(d)
297
 
                    icon = d.documentIcon()
298
 
                    if icon:
299
 
                        a.setIcon(KIcon(icon))
300
 
                    if d is self._currentDoc:
301
 
                        a.setChecked(True)
302
 
                    docGroup.addAction(a)
303
 
                    docMenu.addAction(a)
304
 
                addAccelerators(docMenu.actions())
305
 
            docMenu.setParent(docMenu.parent()) # BUG: SIP otherwise looses outer scope
306
 
            docMenu.aboutToShow.connect(populateDocMenu)
307
 
        
308
 
        # sessions menu
309
 
        sessMenu = self.xmlGuiContainer("sessions")
310
 
        if sessMenu:
311
 
            sessGroup = QActionGroup(sessMenu)
312
 
            sessGroup.setExclusive(True)
313
 
                
314
 
            def populateSessMenu():
315
 
                for a in sessGroup.actions():
316
 
                    sip.delete(a)
317
 
                
318
 
                sm = self.sessionManager()
319
 
                sessions = sm.names()
320
 
                current = sm.current()
321
 
                
322
 
                if not sessions:
323
 
                    return
324
 
                # "No Session" action
325
 
                a = KAction(i18n("No Session"), sessGroup)
326
 
                a.setCheckable(True)
327
 
                if not current:
328
 
                    a.setChecked(True)
329
 
                else:
330
 
                    a.triggered.connect(lambda: sm.switch(None))
331
 
                sessGroup.addAction(a)
332
 
                sessMenu.addAction(a)
333
 
                sessGroup.addAction(sessMenu.addSeparator())
334
 
                # other sessions:
335
 
                for name in sessions:
336
 
                    a = KAction(name, sessGroup)
337
 
                    a.setCheckable(True)
338
 
                    if name == current:
339
 
                        a.setChecked(True)
340
 
                    a.triggered.connect((lambda name: lambda: sm.switch(name))(name))
341
 
                    sessGroup.addAction(a)
342
 
                    sessMenu.addAction(a)
343
 
                addAccelerators(sessMenu.actions())
344
 
            sessMenu.setParent(sessMenu.parent()) # BUG: SIP otherwise looses outer scope
345
 
            sessMenu.aboutToShow.connect(populateSessMenu)
346
 
        
347
 
    @cacheresult
348
 
    def sessionManager(self):
349
 
        return self.createSessionManager()
350
 
        
351
 
    def createSessionManager(self):
352
 
        """Override this to return a different session manager."""
353
 
        return SessionManager(self)
354
 
    
355
 
    def act(self, name, texttype, func,
356
 
            icon=None, tooltip=None, whatsthis=None, key=None):
357
 
        """Create an action and add it to own actionCollection."""
358
 
        if isinstance(texttype, KStandardAction.StandardAction):
359
 
            a = self.actionCollection().addAction(texttype, name)
360
 
        else:
361
 
            a = self.actionCollection().addAction(name)
362
 
            a.setText(texttype)
363
 
        a.triggered.connect(func)
364
 
        if icon: a.setIcon(KIcon(icon))
365
 
        if tooltip: a.setToolTip(tooltip)
366
 
        if whatsthis: a.setWhatsThis(whatsthis)
367
 
        if key: a.setShortcut(KShortcut(key))
368
 
        return a
369
 
        
370
 
    def selAct(self, *args, **kwargs):
371
 
        a = self.act(*args, **kwargs)
372
 
        self._selectionActions.append(a)
373
 
        return a
374
 
 
375
 
    def onAction(self, texttype, icon=None, tooltip=None, whatsthis=None, key=None):
376
 
        """Decorator to add a function to an action.
377
 
        
378
 
        The name of the function becomes the name of the action.
379
 
        
380
 
        """
381
 
        def decorator(func):
382
 
            self.act(func.func_name, texttype, func, icon, tooltip, whatsthis, key)
383
 
            return func
384
 
        return decorator
385
 
        
386
 
    def onSelAction(self, texttype, icon=None, tooltip=None, whatsthis=None, key=None,
387
 
                    warn=True, keepSelection=True):
388
 
        """Decorator to add a function that is run on the selection to an action.
389
 
        
390
 
        The name of the function becomes the name of the action.
391
 
        If there is no selection, the action is automatically disabled.
392
 
        
393
 
        """
394
 
        def decorator(func):
395
 
            def selfunc():
396
 
                doc = self.currentDocument()
397
 
                if doc:
398
 
                    text = doc.selectionText()
399
 
                    if text:
400
 
                        result = func(text)
401
 
                        if result is not None:
402
 
                            doc.replaceSelectionWith(result, keepSelection)
403
 
                    elif warn:
404
 
                        KMessageBox.sorry(self, i18n("Please select some text first."))
405
 
            self.selAct(func.func_name, texttype, selfunc, icon, tooltip, whatsthis, key)
406
 
            return func
407
 
        return decorator
408
 
        
409
 
    def setCurrentDocument(self, doc):
410
 
        """Called when the application makes a different Document active."""
411
 
        if self._currentDoc:
412
 
            self._currentDoc.urlChanged.disconnect(self.slotUrlChanged)
413
 
            self._currentDoc.captionChanged.disconnect(self.updateCaption)
414
 
            self._currentDoc.statusChanged.disconnect(self.updateStatusBar)
415
 
            self._currentDoc.selectionChanged.disconnect(self.updateSelection)
416
 
            self.guiFactory().removeClient(self._currentDoc.view)
417
 
        self._currentDoc = doc
418
 
        self.guiFactory().addClient(doc.view)
419
 
        self.viewStack.setCurrentWidget(doc.view)
420
 
        doc.urlChanged.connect(self.slotUrlChanged)
421
 
        doc.captionChanged.connect(self.updateCaption)
422
 
        doc.statusChanged.connect(self.updateStatusBar)
423
 
        doc.selectionChanged.connect(self.updateSelection)
424
 
        self.updateCaption()
425
 
        self.updateStatusBar()
426
 
        self.updateSelection()
427
 
        self.currentDocumentChanged(doc) # emit our signal
428
 
 
429
 
    def addDocument(self, doc):
430
 
        """Internal. Add Document to our viewStack."""
431
 
        self.viewStack.addWidget(doc.view)
432
 
        
433
 
    def removeDocument(self, doc):
434
 
        """Internal. Remove Document from our viewStack."""
435
 
        self.viewStack.removeWidget(doc.view)
436
 
        if doc is self._currentDoc:
437
 
            self.guiFactory().removeClient(doc.view)
438
 
            self._currentDoc = None
439
 
    
440
 
    def view(self):
441
 
        """Returns the current view or None if none."""
442
 
        if self._currentDoc:
443
 
            return self._currentDoc.view
444
 
    
445
 
    def currentDocument(self):
446
 
        """Returns the current Document or None if none."""
447
 
        return self._currentDoc
448
 
    
449
 
    def slotUrlChanged(self, doc=None):
450
 
        """Called when the url of the current Document changes."""
451
 
        self.addToRecentFiles(doc)
452
 
        self.currentDocumentChanged(doc or self._currentDoc)
453
 
 
454
 
    def updateCaption(self):
455
 
        """Called when the window title needs to be redisplayed."""
456
 
        session = self.sessionManager().current()
457
 
        caption = "{0}: ".format(session) if session else ""
458
 
        doc = self.currentDocument()
459
 
        if doc:
460
 
            name = (self.showPath.isChecked() and doc.prettyUrl() or
461
 
                    doc.documentName())
462
 
            if len(name) > 72:
463
 
                name = '...' + name[-69:]
464
 
            caption += name
465
 
            if doc.isModified():
466
 
                caption += " [{0}]".format(i18n("modified"))
467
 
                self.sb_modified.setPixmap(KIcon("document-save").pixmap(16))
468
 
            else:
469
 
                self.sb_modified.setPixmap(QPixmap())
470
 
        self.setCaption(caption)
471
 
        
472
 
    def updateStatusBar(self):
473
 
        """Called when the status bar needs to be redisplayed."""
474
 
        doc = self.currentDocument()
475
 
        pos = doc.view.cursorPositionVirtual()
476
 
        line, col = pos.line()+1, pos.column()
477
 
        self.sb_linecol.setText(i18n("Line: %1 Col: %2", line, col))
478
 
        self.sb_insmode.setText(doc.view.viewMode())
479
 
 
480
 
    def updateSelection(self):
481
 
        """Called when the selection changes."""
482
 
        doc = self.currentDocument()
483
 
        enable = doc.view.selection() and not doc.view.selectionRange().isEmpty()
484
 
        for a in self._selectionActions:
485
 
            a.setEnabled(enable)
486
 
        if doc.view.blockSelection():
487
 
            text, tip = i18n("BLOCK"), i18n("Block selection mode")
488
 
        else:
489
 
            text, tip = i18n("LINE"), i18n("Line selection mode")
490
 
        self.sb_selmode.setText(" {0} ".format(text))
491
 
        self.sb_selmode.setToolTip(tip)
492
 
 
493
 
    def editKeys(self):
494
 
        """Opens a window to edit the keyboard shortcuts."""
495
 
        with self.app.busyCursor():
496
 
            dlg = KShortcutsDialog(KShortcutsEditor.AllActions,
497
 
                KShortcutsEditor.LetterShortcutsDisallowed, self)
498
 
            for name, collection in self.allActionCollections():
499
 
                dlg.addCollection(collection, name)
500
 
        dlg.configure()
501
 
    
502
 
    def allActionCollections(self):
503
 
        """Iterator over KActionCollections.
504
 
        
505
 
        Yields all KActionCollections that need to be checked if the user
506
 
        wants to alter a keyboard shortcut.
507
 
        
508
 
        Each item is a two-tuple (name, KActionCollection).
509
 
        
510
 
        """
511
 
        yield KGlobal.mainComponent().aboutData().programName(), self.actionCollection()
512
 
        if self.view():
513
 
            yield None, self.view().actionCollection()
514
 
            
515
 
    def editToolbars(self):
516
 
        """Opens a window to edit the toolbar(s)."""
517
 
        conf = config("MainWindow")
518
 
        self.saveMainWindowSettings(conf)
519
 
        dlg = KEditToolBar(self.guiFactory(), self)
520
 
        def newToolbarConfig():
521
 
            self.applyMainWindowSettings(conf)
522
 
            self.setupGeneratedMenus()
523
 
        dlg.newToolbarConfig.connect(newToolbarConfig)
524
 
        dlg.setModal(True)
525
 
        dlg.show()
526
 
 
527
 
    def newDocument(self):
528
 
        """Create a new empty document."""
529
 
        self.app.createDocument().setActive()
530
 
        
531
 
    def openDocument(self):
532
 
        """Open an existing document."""
533
 
        res = KEncodingFileDialog.getOpenUrlsAndEncoding(
534
 
            self.app.defaultEncoding,
535
 
            self.currentDocument().url().url()
536
 
            or self.sessionManager().basedir() or self.app.defaultDirectory(),
537
 
            '\n'.join(self.app.fileTypes + ["*|" + i18n("All Files")]),
538
 
            self, i18n("Open File"))
539
 
        docs = [self.app.openUrl(url, res.encoding)
540
 
                for url in res.URLs if not url.isEmpty()]
541
 
        if docs:
542
 
            docs[-1].setActive()
543
 
    
544
 
    def addToRecentFiles(self, doc=None):
545
 
        """Add url of document to recently opened files."""
546
 
        doc = doc or self.currentDocument()
547
 
        if doc:
548
 
            url = doc.url()
549
 
            if not url.isEmpty() and url not in self.openRecent.urls():
550
 
                self.openRecent.addUrl(url)
551
 
    
552
 
    @pyqtSignature("KUrl")
553
 
    def slotOpenRecent(self, url):
554
 
        """Called by the open recent files action."""
555
 
        self.app.openUrl(url).setActive()
556
 
 
557
 
    def queryClose(self):
558
 
        """Called when the user wants to close the MainWindow.
559
 
        
560
 
        Returns True if the application may quit.
561
 
        
562
 
        """
563
 
        if self.app.kapp.sessionSaving():
564
 
            sc = self.app.kapp.sessionConfig()
565
 
            self.saveDocumentList(sc.group("documents"))
566
 
            self.sessionManager().saveProperties(sc.group("session"))
567
 
        # just ask, cancel at any time will keep all documents.
568
 
        for d in self.app.history[::-1]: # iterate over a copy, current first
569
 
            if d.isModified():
570
 
                d.setActive()
571
 
                if not d.queryClose():
572
 
                    return False # cancelled
573
 
        # Then close the documents
574
 
        self.currentDocumentChanged.clear() # disconnect all tools etc.
575
 
        self.aboutToClose()
576
 
        for d in self.app.history[:]: # iterate over a copy
577
 
            d.close(False)
578
 
        # save some settings
579
 
        self.saveSettings()
580
 
        return True
581
 
    
582
 
    def closeOtherDocuments(self):
583
 
        """Close all documents except the current document."""
584
 
        # iterate over a copy, current first, except current document
585
 
        docs = self.app.history[-2::-1]
586
 
        for d in docs:
587
 
            if d.isModified():
588
 
                if not d.queryClose():
589
 
                    return # cancelled
590
 
        for d in docs:
591
 
            d.close(False)
592
 
    
593
 
    def readGlobalProperties(self, conf):
594
 
        """Called on session restore, loads the list of open documents."""
595
 
        self.loadDocumentList(conf.group("documents"))
596
 
        self.sessionManager().readProperties(conf.group("session"))
597
 
        
598
 
    def saveDocumentList(self, cg):
599
 
        """Stores the list of documents to the given KConfigGroup."""
600
 
        urls = [d.url().url() for d in self.viewTabs.docs] # order of tabs
601
 
        d = self.currentDocument()
602
 
        current = self.viewTabs.docs.index(d) if d else -1
603
 
        cg.writePathEntry("urls", urls)
604
 
        cg.writeEntry("active", current)
605
 
        cg.sync()
606
 
        
607
 
    def loadDocumentList(self, cg):
608
 
        """Loads the documents mentioned in the given KConfigGroup."""
609
 
        urls = cg.readPathEntry("urls", [])
610
 
        active = cg.readEntry("active", 0)
611
 
        if any(urls):
612
 
            docs = [self.app.openUrl(KUrl(url)) for url in urls]
613
 
            if docs:
614
 
                if active < 0 or active >= len(docs):
615
 
                    active = len(docs) - 1
616
 
                docs[active].setActive()
617
 
    
618
 
    def loadSettings(self):
619
 
        """Load some settings from our configfile."""
620
 
        self.openRecent.loadEntries(config("recent files"))
621
 
        self.showPath.setChecked(config().readEntry("show full path", False))
622
 
        self.showTabs.setChecked(config().readEntry("show tabs", True))
623
 
        self.viewTabs.setVisible(self.showTabs.isChecked())
624
 
 
625
 
    def saveSettings(self):
626
 
        """Store settings in our configfile."""
627
 
        self.openRecent.saveEntries(config("recent files"))
628
 
        config().writeEntry("show full path", self.showPath.isChecked())
629
 
        config().writeEntry("show tabs", self.showTabs.isChecked())
630
 
        # also all the tools:
631
 
        for tool in self.tools.itervalues():
632
 
            tool.saveSettings()
633
 
        # also the main editor object:
634
 
        self.app.editor.writeConfig()
635
 
        # write them back
636
 
        config().sync()
637
 
 
638
 
    def createViewTabBar(self):
639
 
        return ViewTabBar(self)
640
 
 
641
 
    def dragEnterEvent(self, event):
642
 
        event.setAccepted(KUrl.List.canDecode(event.mimeData()))
643
 
        
644
 
    def dropEvent(self, event):
645
 
        if KUrl.List.canDecode(event.mimeData()):
646
 
            urls = KUrl.List.fromMimeData(event.mimeData())
647
 
            docs = map(self.app.openUrl, urls)
648
 
            if docs:
649
 
                docs[-1].setActive()
650
 
 
651
 
 
652
 
class ViewTabBar(QTabBar):
653
 
    """The tab bar above the document editor view."""
654
 
    def __init__(self, mainwin):
655
 
        QTabBar.__init__(self)
656
 
        KAcceleratorManager.setNoAccel(self)
657
 
        self.mainwin = mainwin
658
 
        self.docs = []
659
 
        # get the documents to create their tabs.
660
 
        for doc in mainwin.app.documents:
661
 
            self.addDocument(doc)
662
 
            if doc.isActive():
663
 
                self.setCurrentDocument(doc)
664
 
        mainwin.app.documentCreated.connect(self.addDocument)
665
 
        mainwin.app.documentClosed.connect(self.removeDocument)
666
 
        mainwin.app.documentMaterialized.connect(self.setDocumentStatus)
667
 
        self.currentChanged.connect(self.slotCurrentChanged)
668
 
        mainwin.currentDocumentChanged.connect(self.setCurrentDocument)
669
 
        try:
670
 
            self.setTabsClosable
671
 
            self.tabCloseRequested.connect(self.slotTabCloseRequested)
672
 
        except AttributeError:
673
 
            pass
674
 
        try:
675
 
            self.setMovable
676
 
            self.tabMoved.connect(self.slotTabMoved)
677
 
        except AttributeError:
678
 
            pass
679
 
        self.readSettings()
680
 
        
681
 
    def readSettings(self):
682
 
        # closeable? only in Qt >= 4.6
683
 
        try:
684
 
            self.setTabsClosable(config("tab bar").readEntry("close button", True))
685
 
        except AttributeError:
686
 
            pass
687
 
        
688
 
        # expanding? only in Qt >= 4.5
689
 
        try:
690
 
            self.setExpanding(config("tab bar").readEntry("expanding", False))
691
 
        except AttributeError:
692
 
            pass
693
 
        
694
 
        # movable? only in Qt >= 4.5
695
 
        try:
696
 
            self.setMovable(config("tab bar").readEntry("movable", True))
697
 
        except AttributeError:
698
 
            pass
699
 
        
700
 
    def addDocument(self, doc):
701
 
        if doc not in self.docs:
702
 
            self.docs.append(doc)
703
 
            self.blockSignals(True)
704
 
            self.addTab('')
705
 
            self.blockSignals(False)
706
 
            self.setDocumentStatus(doc)
707
 
            doc.urlChanged.connect(self.setDocumentStatus)
708
 
            doc.captionChanged.connect(self.setDocumentStatus)
709
 
 
710
 
    def removeDocument(self, doc):
711
 
        if doc in self.docs:
712
 
            index = self.docs.index(doc)
713
 
            self.docs.remove(doc)
714
 
            self.blockSignals(True)
715
 
            self.removeTab(index)
716
 
            self.blockSignals(False)
717
 
 
718
 
    def setDocumentStatus(self, doc):
719
 
        if doc in self.docs:
720
 
            index = self.docs.index(doc)
721
 
            self.setTabIcon(index, KIcon(doc.documentIcon() or "text-plain"))
722
 
            self.setTabText(index, doc.documentName())
723
 
    
724
 
    def setCurrentDocument(self, doc):
725
 
        """ Raise the tab belonging to this document."""
726
 
        if doc in self.docs:
727
 
            index = self.docs.index(doc)
728
 
            self.blockSignals(True)
729
 
            self.setCurrentIndex(index)
730
 
            self.blockSignals(False)
731
 
 
732
 
    def slotCurrentChanged(self, index):
733
 
        """ Called when the user clicks a tab. """
734
 
        self.docs[index].setActive()
735
 
    
736
 
    def slotTabCloseRequested(self, index):
737
 
        """ Called when the user clicks the close button. """
738
 
        self.docs[index].close()
739
 
    
740
 
    def slotTabMoved(self, index_from, index_to):
741
 
        """ Called when the user moved a tab. """
742
 
        doc = self.docs.pop(index_from)
743
 
        self.docs.insert(index_to, doc)
744
 
        
745
 
    def contextMenuEvent(self, ev):
746
 
        """ Called when the right mouse button is clicked on the tab bar. """
747
 
        tab = self.tabAt(ev.pos())
748
 
        if tab >= 0:
749
 
            menu = KMenu()
750
 
            self.addMenuActions(menu, self.docs[tab])
751
 
            menu.exec_(ev.globalPos())
752
 
 
753
 
    def addMenuActions(self, menu, doc):
754
 
        """ Populate the menu with actions relevant for the document. """
755
 
        g = KStandardGuiItem.save()
756
 
        a = menu.addAction(g.icon(), g.text())
757
 
        a.triggered.connect(lambda: doc.save())
758
 
        g = KStandardGuiItem.saveAs()
759
 
        a = menu.addAction(g.icon(), g.text())
760
 
        a.triggered.connect(lambda: doc.saveAs())
761
 
        menu.addSeparator()
762
 
        g = KStandardGuiItem.close()
763
 
        a = menu.addAction(g.icon(), g.text())
764
 
        a.triggered.connect(lambda: doc.close())
765
 
 
766
 
 
767
 
class TabBar(KMultiTabBar):
768
 
    """Our own tabbar with some nice defaults."""
769
 
    def __init__(self, orientation, parent, maxSize=18):
770
 
        KMultiTabBar.__init__(self, orientation, parent)
771
 
        self.setStyle(KMultiTabBar.KDEV3ICON)
772
 
        if maxSize:
773
 
            if orientation in (KMultiTabBar.Bottom, KMultiTabBar.Top):
774
 
                self.setMaximumHeight(maxSize)
775
 
            else:
776
 
                self.setMaximumWidth(maxSize)
777
 
        self._tools = []
778
 
        
779
 
    def addTool(self, tool):
780
 
        self._tools.append(tool)
781
 
        self.appendTab(tool.icon().pixmap(16), tool._id, tool.title())
782
 
        tab = self.tab(tool._id)
783
 
        tab.setFocusPolicy(Qt.NoFocus)
784
 
        tab.setToolTip(u"<b>{0}</b><br/>{1}".format(tool.title(),
785
 
            i18n("Right-click for tab options")))
786
 
        tab.setContextMenuPolicy(Qt.CustomContextMenu)
787
 
        tab.clicked.connect(tool.toggle)
788
 
        tab.setParent(tab.parent()) # BUG: otherwise SIP looses outer scope
789
 
        tab.customContextMenuRequested.connect(
790
 
            lambda pos: tool.showContextMenu(tab.mapToGlobal(pos)))
791
 
 
792
 
    def removeTool(self, tool):
793
 
        self._tools.remove(tool)
794
 
        self.removeTab(tool._id)
795
 
        
796
 
    def showTool(self, tool):
797
 
        self.tab(tool._id).setState(True)
798
 
        
799
 
    def hideTool(self, tool):
800
 
        self.tab(tool._id).setState(False)
801
 
        
802
 
    def updateState(self, tool):
803
 
        tab = self.tab(tool._id)
804
 
        tab.setIcon(tool.icon())
805
 
        tab.setText(tool.title())
806
 
 
807
 
 
808
 
class Dock(QStackedWidget):
809
 
    """A dock where tools can be added to.
810
 
    
811
 
    Hides itself when there are no tools visible.
812
 
    When it receives a tool, a button is created in the associated tabbar.
813
 
    
814
 
    """
815
 
    def __init__(self, parent, tabbar, icon, title):
816
 
        QStackedWidget.__init__(self, parent)
817
 
        self.tabbar = tabbar
818
 
        self.title = title
819
 
        self.icon = icon and KIcon(icon) or KIcon()
820
 
        self._tools = []          # a list of the tools we host
821
 
        self._currentTool = None # the currently active tool, if any
822
 
        self.hide() # by default
823
 
 
824
 
    def addTool(self, tool):
825
 
        """ Add a tool to our tabbar, save dock and tabid in the tool """
826
 
        self.tabbar.addTool(tool)
827
 
        self._tools.append(tool)
828
 
        if tool.isActive():
829
 
            self.showTool(tool)
830
 
 
831
 
    def removeTool(self, tool):
832
 
        """ Remove a tool from our dock. """
833
 
        self.tabbar.removeTool(tool)
834
 
        self._tools.remove(tool)
835
 
        if tool is self._currentTool:
836
 
            self._currentTool = None
837
 
            self.hide()
838
 
        
839
 
    def showTool(self, tool):
840
 
        """Internal: only to be called by tool.show().
841
 
        
842
 
        Use tool.show() to make a tool active.
843
 
        
844
 
        """
845
 
        if tool not in self._tools or tool is self._currentTool:
846
 
            return
847
 
        if self.indexOf(tool.widget) == -1:
848
 
            self.addWidget(tool.widget)
849
 
        self.setCurrentWidget(tool.widget)
850
 
        self.tabbar.showTool(tool)
851
 
        cur = self._currentTool
852
 
        self._currentTool = tool
853
 
        if cur:
854
 
            cur.hide()
855
 
        else:
856
 
            self.show()
857
 
            
858
 
    def hideTool(self, tool):
859
 
        """Internal: only to be called by tool.hide().
860
 
        
861
 
        Use tool.hide() to make a tool inactive.
862
 
        
863
 
        """
864
 
        self.tabbar.hideTool(tool)
865
 
        if tool is self._currentTool:
866
 
            self._currentTool = None
867
 
            self.hide()
868
 
        
869
 
    def currentTool(self):
870
 
        return self._currentTool
871
 
        
872
 
    def updateState(self, tool):
873
 
        self.tabbar.updateState(tool)
874
 
 
875
 
 
876
 
class DockDialog(QDialog):
877
 
    """A QDialog that (re)docks itself when closed."""
878
 
    def __init__(self, tool):
879
 
        QDialog.__init__(self, tool.mainwin)
880
 
        QVBoxLayout(self).setContentsMargins(0, 0, 0, 0)
881
 
        self.tool = tool
882
 
        self.setAttribute(Qt.WA_DeleteOnClose, False)
883
 
        self.updateState()
884
 
    
885
 
    def show(self):
886
 
        # Take the widget by adding it to our layout
887
 
        self.layout().addWidget(self.tool.widget)
888
 
        if self.tool.dialogSize:
889
 
            self.resize(self.tool.dialogSize)
890
 
        QDialog.show(self)
891
 
        if self.tool.dialogPos:
892
 
            self.move(self.tool.dialogPos)
893
 
        
894
 
    def done(self, r):
895
 
        self.tool.dialogSize = self.size()
896
 
        self.tool.dialogPos = self.pos()
897
 
        self.tool.dock()
898
 
        QDialog.done(self, r)
899
 
 
900
 
    def updateState(self):
901
 
        title = KDialog.makeStandardCaption(self.tool.title(), self,
902
 
            KDialog.HIGCompliantCaption)
903
 
        self.setWindowTitle(title)
904
 
        self.setWindowIcon(self.tool.icon())
905
 
 
906
 
    def keyPressEvent(self, e):
907
 
        if e.key() != Qt.Key_Escape:
908
 
            QDialog.keyPressEvent(self, e)
909
 
 
910
 
 
911
 
class Tool(object):
912
 
    """A Tool, that can be docked or undocked in/from the MainWindow.
913
 
    
914
 
    Intended to be subclassed.
915
 
    
916
 
    """
917
 
    allowedPlaces = Top, Right, Bottom, Left
918
 
    defaultHeight = 300
919
 
    defaultWidth = 500
920
 
    
921
 
    helpAnchor, helpAppName = "", ""
922
 
 
923
 
    __instance_counter = 0
924
 
    
925
 
    def __init__(self, mainwin, name,
926
 
            title="", icon="", key="", dock=Right,
927
 
            widget=None):
928
 
        self._id = Tool.__instance_counter
929
 
        self._active = False
930
 
        self._docked = True
931
 
        self._dock = None
932
 
        self._dialog = None
933
 
        self.dialogSize = None
934
 
        self.dialogPos = None
935
 
        self.mainwin = mainwin
936
 
        self.name = name
937
 
        mainwin.tools[name] = self
938
 
        action = KAction(mainwin, triggered=self.slotAction) # action to toggle our view
939
 
        mainwin.actionCollection().addAction("tool_" + name, action)
940
 
        if key:
941
 
            action.setShortcut(KShortcut(key))
942
 
        self.widget = widget
943
 
        self.setTitle(title)
944
 
        self.setIcon(icon)
945
 
        self.setDock(dock)
946
 
        Tool.__instance_counter += 1
947
 
        self.loadSettings()
948
 
    
949
 
    def action(self):
950
 
        return self.mainwin.actionCollection().action("tool_" + self.name)
951
 
    
952
 
    def slotAction(self):
953
 
        """Called when our action is triggered.
954
 
        
955
 
        Default behaviour is to toggle the visibility of our tool.
956
 
        Override this to implement other behaviour when our action is called
957
 
        (e.g. focus instead of hide).
958
 
        
959
 
        """
960
 
        self.toggle()
961
 
        
962
 
    def materialize(self):
963
 
        """If not yet done, calls self.factory() to get the widget of our tool.
964
 
        
965
 
        The widget is stored in the widget instance attribute.
966
 
        Use this to make tools 'lazy': only instantiate the widget and load
967
 
        other modules if needed as soon as the user wants to show the tool.
968
 
        
969
 
        """
970
 
        if self.widget is None:
971
 
            with self.mainwin.app.busyCursor():
972
 
                self.widget = self.factory()
973
 
    
974
 
    def factory(self):
975
 
        """Should return this Tool's widget when it must become visible.
976
 
        
977
 
        I you didn't supply a widget on init, you must override this method.
978
 
        
979
 
        """
980
 
        return QWidget()
981
 
        
982
 
    def delete(self):
983
 
        """Completely remove the tool.
984
 
        
985
 
        Its association with the mainwindow is removed, and it will be
986
 
        garbage collected as soon as the last reference to it is lost.
987
 
        
988
 
        """
989
 
        if not self._docked:
990
 
            self.dock()
991
 
        self._dock.removeTool(self)
992
 
        if self.widget:
993
 
            sip.delete(self.widget)
994
 
        if self._dialog:
995
 
            sip.delete(self._dialog)
996
 
        sip.delete(self.action())
997
 
        del self._dock, self.widget, self._dialog
998
 
        del self.mainwin.tools[self.name]
999
 
 
1000
 
    def show(self):
1001
 
        """ Bring our tool into view. """
1002
 
        self.materialize()
1003
 
        if self._docked:
1004
 
            self._active = True
1005
 
            self._dock.showTool(self)
1006
 
        else:
1007
 
            self._dialog.raise_()
1008
 
            
1009
 
    def hide(self):
1010
 
        """ Hide our tool. """
1011
 
        if self._docked:
1012
 
            self._active = False
1013
 
            self._dock.hideTool(self)
1014
 
            view = self.mainwin.view()
1015
 
            if view:
1016
 
                view.setFocus()
1017
 
 
1018
 
    def toggle(self):
1019
 
        """ Toggle visibility if docked. """
1020
 
        if self._docked:
1021
 
            if self._active:
1022
 
                self.hide()
1023
 
            else:
1024
 
                self.show()
1025
 
 
1026
 
    def isActive(self):
1027
 
        """ Returns True if the tool is currently the active one in its dock."""
1028
 
        return self._active
1029
 
    
1030
 
    def isDocked(self):
1031
 
        """ Returns True if the tool is docked. """
1032
 
        return self._docked
1033
 
        
1034
 
    def setDock(self, place):
1035
 
        """ Puts the tool in one of the four places.
1036
 
        
1037
 
        place is one of (KMultiTabBar).Top, Right, Bottom, Left
1038
 
        
1039
 
        """
1040
 
        dock = self.mainwin.docks.get(place, self._dock)
1041
 
        if dock is self._dock:
1042
 
            return
1043
 
        if self._docked:
1044
 
            if self._dock:
1045
 
                self._dock.removeTool(self)
1046
 
            dock.addTool(self)
1047
 
        self._dock = dock
1048
 
            
1049
 
    def undock(self):
1050
 
        """ Undock our widget """
1051
 
        if not self._docked:
1052
 
            return
1053
 
        self._dock.removeTool(self)
1054
 
        self.materialize()
1055
 
        self._docked = False
1056
 
        if not self._dialog:
1057
 
            size = self._dock.size()
1058
 
            if size.height() <= 0:
1059
 
                size.setHeight(self.defaultHeight)
1060
 
            if size.width() <= 0:
1061
 
                size.setWidth(self.defaultWidth)
1062
 
            self.dialogSize = size
1063
 
            self._dialog = DockDialog(self)
1064
 
        self._dialog.show()
1065
 
        self.widget.show()
1066
 
 
1067
 
    def dock(self):
1068
 
        """ Dock and hide the dialog window """
1069
 
        if self._docked:
1070
 
            return
1071
 
        self._dialog.hide()
1072
 
        self._docked = True
1073
 
        self._dock.addTool(self)
1074
 
        
1075
 
    def icon(self):
1076
 
        return self._icon
1077
 
        
1078
 
    def setIcon(self, icon):
1079
 
        self._icon = icon and KIcon(icon) or KIcon()
1080
 
        self.action().setIcon(self._icon)
1081
 
        self.updateState()
1082
 
 
1083
 
    def title(self):
1084
 
        return self._title
1085
 
    
1086
 
    def setTitle(self, title):
1087
 
        self._title = title
1088
 
        self.action().setText(self._title)
1089
 
        self.updateState()
1090
 
            
1091
 
    def updateState(self):
1092
 
        if self._docked:
1093
 
            if self._dock:
1094
 
                self._dock.updateState(self)
1095
 
        else:
1096
 
            self._dialog.updateState()
1097
 
            
1098
 
    def showContextMenu(self, globalPos):
1099
 
        """Show a popup menu to manipulate this tool."""
1100
 
        m = KMenu(self.mainwin)
1101
 
        places = [place for place in Left, Right, Top, Bottom
1102
 
            if place in self.allowedPlaces
1103
 
            and self.mainwin.docks.get(place, self._dock) is not self._dock]
1104
 
        if places:
1105
 
            m.addTitle(KIcon("transform-move"), i18n("Move To"))
1106
 
            for place in places:
1107
 
                dock = self.mainwin.docks[place]
1108
 
                a = m.addAction(dock.icon, dock.title)
1109
 
                a.triggered.connect((lambda place: lambda: self.setDock(place))(place))
1110
 
            m.addSeparator()
1111
 
        a = m.addAction(KIcon("tab-detach"), i18n("Undock"))
1112
 
        a.triggered.connect(self.undock)
1113
 
        self.addMenuActions(m)
1114
 
        if self.helpAnchor or self.helpAppName:
1115
 
            m.addSeparator()
1116
 
            a = m.addAction(KIcon("help-contextual"), KStandardGuiItem.help().text())
1117
 
            a.triggered.connect(self.help)
1118
 
        m.aboutToHide.connect(m.deleteLater)
1119
 
        m.popup(globalPos)
1120
 
 
1121
 
    def addMenuActions(self, menu):
1122
 
        """Use this to add your own actions to a tool menu."""
1123
 
        pass
1124
 
    
1125
 
    def config(self):
1126
 
        """ Return a suitable configgroup for our settings. """
1127
 
        return config("tool_" + self.name)
1128
 
 
1129
 
    def loadSettings(self):
1130
 
        """ Do not override this method, use readConfig instead. """
1131
 
        conf = self.config()
1132
 
        self.readConfig(conf)
1133
 
 
1134
 
    def saveSettings(self):
1135
 
        """ Do not override this method, use writeConfig instead. """
1136
 
        conf = self.config()
1137
 
        self.writeConfig(conf)
1138
 
        
1139
 
    def readConfig(self, conf):
1140
 
        """Implement this in your subclass to read additional config data."""
1141
 
        pass
1142
 
    
1143
 
    def writeConfig(self, conf):
1144
 
        """Implement this in your subclass to write additional config data."""
1145
 
        pass
1146
 
    
1147
 
    def help(self):
1148
 
        """Invokes Help on our tool.
1149
 
        
1150
 
        See the helpAnchor and helpAppName attributes.
1151
 
        
1152
 
        """
1153
 
        KToolInvocation.invokeHelp(self.helpAnchor, self.helpAppName)
1154
 
 
1155
 
 
1156
 
class KPartTool(Tool):
1157
 
    """A Tool where the widget is loaded via the KParts system."""
1158
 
    
1159
 
    # set this to the library name you want to load
1160
 
    _partlibrary = ""
1161
 
    # set this to the name of the app containing this part
1162
 
    _partappname = ""
1163
 
    
1164
 
    def __init__(self, mainwin, name, title="", icon="", key="", dock=Right):
1165
 
        self.part = None
1166
 
        self.failed = False
1167
 
        Tool.__init__(self, mainwin, name, title, icon, key, dock)
1168
 
    
1169
 
    def factory(self):
1170
 
        if self.part:
1171
 
            return
1172
 
        factory = KPluginLoader(self._partlibrary).factory()
1173
 
        if factory:
1174
 
            part = factory.create(self.mainwin)
1175
 
            if part:
1176
 
                self.part = part
1177
 
                part.destroyed.connect(self.slotDestroyed, Qt.DirectConnection)
1178
 
                QTimer.singleShot(0, self.partLoaded)
1179
 
                return part.widget()
1180
 
        self.failed = True
1181
 
        return QLabel("<center><p>{0}</p><p>{1}</p></center>".format(
1182
 
            i18n("Could not load %1", self._partlibrary),
1183
 
            i18n("Please install %1", self._partappname or self._partlibrary)))
1184
 
 
1185
 
    def partLoaded(self):
1186
 
        """ Called when part is loaded. Use this to apply settings, etc."""
1187
 
        pass
1188
 
    
1189
 
    def delete(self):
1190
 
        if self.part:
1191
 
            self.part.destroyed.disconnect(self.slotDestroyed)
1192
 
        super(KPartTool, self).delete()
1193
 
        
1194
 
    def slotDestroyed(self):
1195
 
        self.part = None
1196
 
        self.failed = False
1197
 
        self.widget = None
1198
 
        if not sip.isdeleted(self.mainwin):
1199
 
            if self._docked:
1200
 
                self.hide()
1201
 
            elif self._dialog:
1202
 
                self._active = False
1203
 
                self._dialog.done(0)
1204
 
        
1205
 
    def openUrl(self, url):
1206
 
        """ Expects KUrl."""
1207
 
        if self.part:
1208
 
            self.part.openUrl(url)
1209
 
 
1210
 
 
1211
 
class UserShortcutManager(object):
1212
 
    """Manages user-defined keyboard shortcuts.
1213
 
    
1214
 
    Keyboard shortcuts can be loaded without loading the module they belong to.
1215
 
    If a shortcut is triggered, the module is loaded on demand and the action
1216
 
    triggered.
1217
 
 
1218
 
    You should subclass this base class and implement the widget() and client()
1219
 
    methods.
1220
 
    
1221
 
    """
1222
 
 
1223
 
    # which config group to store our shortcuts
1224
 
    configGroup = "user shortcuts"
1225
 
    
1226
 
    # the shortcut type to use
1227
 
    shortcutContext = Qt.WidgetWithChildrenShortcut
1228
 
    
1229
 
    def __init__(self, mainwin):
1230
 
        self.mainwin = mainwin
1231
 
        self._collection = KActionCollection(self.widget())
1232
 
        self._collection.setConfigGroup(self.configGroup)
1233
 
        self._collection.addAssociatedWidget(self.widget())
1234
 
        # load the shortcuts
1235
 
        group = KGlobal.config().group(self.configGroup)
1236
 
        for key in group.keyList():
1237
 
            if group.readEntry(key, ""):
1238
 
                self.addAction(key)
1239
 
        self._collection.readSettings()
1240
 
    
1241
 
    def widget(self):
1242
 
        """Should return the widget where the actions should be added to."""
1243
 
        pass
1244
 
        
1245
 
    def client(self):
1246
 
        """Should return the object that can further process our actions.
1247
 
        
1248
 
        Most times this will be a kateshell.shortcut.ShortcutClientBase instance.
1249
 
        
1250
 
        It should have the following methods:
1251
 
        - actionTriggered(name)
1252
 
        - populateAction(name, action)
1253
 
        
1254
 
        """
1255
 
        pass
1256
 
        
1257
 
    def addAction(self, name):
1258
 
        """(Internal) Create a new action with name name.
1259
 
        
1260
 
        If existing, return the existing action.
1261
 
        
1262
 
        """
1263
 
        action = self._collection.action(name)
1264
 
        if not action:
1265
 
            action = self._collection.addAction(name)
1266
 
            action.setShortcutContext(self.shortcutContext)
1267
 
            action.triggered.connect(lambda: self.client().actionTriggered(name))
1268
 
        return action
1269
 
    
1270
 
    def actionCollection(self):
1271
 
        """Returns the action collection, populated with texts and icons."""
1272
 
        for action in self._collection.actions()[:]:
1273
 
            if action.shortcut().isEmpty():
1274
 
                sip.delete(action)
1275
 
            else:
1276
 
                self.client().populateAction(action.objectName(), action)
1277
 
        return self._collection
1278
 
 
1279
 
 
1280
 
class SessionManager(object):
1281
 
    """Manages sessions (basically lists of open documents).
1282
 
    
1283
 
    Sessions are stored in the appdata/sessions configfile, with each session
1284
 
    in its own group.
1285
 
    
1286
 
    """
1287
 
    sessionChanged = Signal()
1288
 
    sessionAdded = Signal()
1289
 
    
1290
 
    def __init__(self, mainwin):
1291
 
        self.mainwin = mainwin
1292
 
        mainwin.aboutToClose.connect(self.shutdown)
1293
 
        self._current = None
1294
 
        self.sessionConfig = None
1295
 
        self.reConfig()
1296
 
        
1297
 
    def reConfig(self):
1298
 
        """Destroys and recreate the sessions KConfig object.
1299
 
        
1300
 
        Intended as a workaround for BUG 192266 in bugs.KDE.org.
1301
 
        Otherwise deleting sessions does not work well.
1302
 
        Call this after using deleteGroup.
1303
 
        """
1304
 
        if self.sessionConfig:
1305
 
            self.sessionConfig.sync()
1306
 
            sip.delete(self.sessionConfig)
1307
 
        self.sessionConfig = KConfig("sessions", KConfig.NoGlobals, "appdata")
1308
 
        
1309
 
    def config(self, session=None):
1310
 
        """Returns the config group for the named or current session.
1311
 
        
1312
 
        If session=False or 0, returns always the root KConfigGroup.
1313
 
        If session=None (default), returns the group for the current session,
1314
 
        if the current session is None, returns the root group.
1315
 
        
1316
 
        """
1317
 
        if session:
1318
 
            return self.sessionConfig.group(session)
1319
 
        if session is None and self._current:
1320
 
            return self.sessionConfig.group(self._current)
1321
 
        return self.sessionConfig.group(None)
1322
 
        
1323
 
    def manage(self):
1324
 
        """Opens the Manage Sessions dialog."""
1325
 
        self.managerDialog().show()
1326
 
        
1327
 
    @cacheresult
1328
 
    def managerDialog(self):
1329
 
        return self.createManagerDialog()
1330
 
        
1331
 
    def createManagerDialog(self):
1332
 
        """Override this to return a subclass of ManagerDialog."""
1333
 
        import kateshell.sessions
1334
 
        return kateshell.sessions.ManagerDialog(self)
1335
 
        
1336
 
    @cacheresult
1337
 
    def editorDialog(self):
1338
 
        """Returns a dialog to edit properties of the session."""
1339
 
        return self.createEditorDialog()
1340
 
    
1341
 
    def createEditorDialog(self):
1342
 
        """Override this to return a subclass of EditorDialog."""
1343
 
        import kateshell.sessions
1344
 
        return kateshell.sessions.EditorDialog(self)
1345
 
 
1346
 
    def switch(self, name):
1347
 
        """Switches to the given session.
1348
 
        
1349
 
        Use None or "none" for the no-session state.
1350
 
        If the given session does not exist, it is created from the current
1351
 
        setup.
1352
 
        
1353
 
        """
1354
 
        if name == "none":
1355
 
            name = None
1356
 
        self.autoSave()
1357
 
        
1358
 
        if name:
1359
 
            if self.config(False).hasGroup(name):
1360
 
                # existing group, close all the documents
1361
 
                docs = self.mainwin.app.history[:] # copy
1362
 
                for d in docs[::-1]: # in reversed order
1363
 
                    if not d.queryClose():
1364
 
                        return False
1365
 
                for d in docs:
1366
 
                    d.close(False)
1367
 
                self.mainwin.loadDocumentList(self.config(name))
1368
 
            else:
1369
 
                # this group did not exist, create it
1370
 
                self.addSession(name)
1371
 
        self._current = name
1372
 
        self.sessionChanged()
1373
 
        return True
1374
 
    
1375
 
    def names(self):
1376
 
        """Returns a list of names of all sessions."""
1377
 
        names = self.sessionConfig.groupList()
1378
 
        names.sort(key=naturalsort)
1379
 
        return names
1380
 
        
1381
 
    def current(self):
1382
 
        """Returns the name of the current session, or None if none."""
1383
 
        return self._current
1384
 
    
1385
 
    def save(self):
1386
 
        """Saves the current session."""
1387
 
        if self._current is None:
1388
 
            self.new()
1389
 
        else:
1390
 
            self.mainwin.saveDocumentList(self.config())
1391
 
    
1392
 
    def autoSave(self):
1393
 
        """Saves the current session if the session wants to be autosaved."""
1394
 
        if self._current and self.config().readEntry("autosave", True):
1395
 
            self.save()
1396
 
        self.config().sync()
1397
 
    
1398
 
    def shutdown(self):
1399
 
        """Called on application exit."""
1400
 
        self.config(False).writeEntry("lastused", self.current() or "none")
1401
 
        self.autoSave()
1402
 
        
1403
 
    def restoreLastSession(self):
1404
 
        """Restores the last saved session."""
1405
 
        name = self.config(False).readEntry("lastused", "none")
1406
 
        if name != "none":
1407
 
            self.switch(name)
1408
 
        
1409
 
    def saveProperties(self, conf):
1410
 
        """Save our state in a session group of QSessionManager."""
1411
 
        conf.writeEntry("name", self._current or "none")
1412
 
        conf.sync()
1413
 
    
1414
 
    def readProperties(self, conf):
1415
 
        """Restore our state from a QSessionManager session group."""
1416
 
        name = conf.readEntry("name", "none")
1417
 
        self._current = None if name == "none" else name
1418
 
        if self._current:
1419
 
            self.sessionChanged()
1420
 
    
1421
 
    def new(self):
1422
 
        """Prompts for a name for a new session.
1423
 
        
1424
 
        If the user enters a name and accepts the dialog, the session is
1425
 
        created and switched to.
1426
 
        
1427
 
        """
1428
 
        name = self.editorDialog().edit()
1429
 
        if name:
1430
 
            # switch there with the current document list
1431
 
            self.mainwin.saveDocumentList(self.config(name))
1432
 
            self._current = name
1433
 
            self.sessionChanged()
1434
 
            self.sessionAdded()
1435
 
 
1436
 
    def deleteSession(self, name):
1437
 
        """Deletes the named session."""
1438
 
        if name == self._current:
1439
 
            self._current = None
1440
 
            self.sessionChanged()
1441
 
        self.config(name).deleteGroup()
1442
 
        self.reConfig()
1443
 
    
1444
 
    def renameSession(self, old, new):
1445
 
        """Renames a session.
1446
 
        
1447
 
        The document list is taken over but not the other settings.
1448
 
        Both names must be valid session names, and old must exist.
1449
 
        
1450
 
        """
1451
 
        oldConfig = self.config(old)
1452
 
        newConfig = self.config(new)
1453
 
        newConfig.writePathEntry("urls", oldConfig.readPathEntry("urls", []))
1454
 
        newConfig.writeEntry("active", oldConfig.readEntry("active", 0))
1455
 
        
1456
 
        if old == self._current:
1457
 
            self._current = new
1458
 
            self.sessionChanged()
1459
 
        self.config(old).deleteGroup()
1460
 
        self.reConfig()
1461
 
            
1462
 
    def addSession(self, name):
1463
 
        """Adds the named session, with the current document list."""
1464
 
        if not self.config(False).hasGroup(name):
1465
 
            self.mainwin.saveDocumentList(self.config(name))
1466
 
        
1467
 
    def basedir(self):
1468
 
        """Returns the configured base directory for this session, if any."""
1469
 
        if self._current:
1470
 
            return self.config().readPathEntry("basedir", "")
1471