1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
3
# Copyright (c) 2008, 2009, 2010 by Wilbert Berendsen
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.
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.
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.
20
from __future__ import unicode_literals
22
import itertools, os, re, sip, weakref
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
39
from signals import Signal
42
from kateshell.app import cacheresult, naturalsort
44
# Easily get our global config
45
def config(group="kateshell"):
46
return KGlobal.config().group(group)
49
Top = KMultiTabBar.Top
50
Right = KMultiTabBar.Right
51
Bottom = KMultiTabBar.Bottom
52
Left = KMultiTabBar.Left
55
def addAccelerators(actions):
56
"""Adds accelerators to the list of actions.
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.
66
m = re.search(r'&(\w)', a.text())
67
used.append(m.group(1).lower()) if m else todo.append(a)
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():])
78
class MainWindow(KParts.MainWindow):
79
"""An editor main window.
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.
86
currentDocumentChanged = Signal()
87
aboutToClose = Signal()
89
def __init__(self, app):
90
KParts.MainWindow.__init__(self)
92
self._currentDoc = None
98
self.sb_linecol = QLabel(sb)
99
sb.addWidget(self.sb_linecol, 0)
101
self.sb_modified = QLabel(sb)
102
self.sb_modified.setFixedSize(16, 16)
103
sb.addWidget(self.sb_modified, 0)
105
self.sb_insmode = QLabel(sb)
106
sb.addWidget(self.sb_insmode, 0)
108
self.sb_selmode = QLabel(sb)
109
sb.addWidget(self.sb_selmode, 0)
111
tab_bottom = TabBar(Bottom, sb)
112
sb.addWidget(tab_bottom, 0)
116
self.setCentralWidget(h)
117
tab_left = TabBar(Left, h)
118
s = QSplitter(Qt.Horizontal, h)
119
tab_right = TabBar(Right, h)
121
self.docks[Left] = Dock(s, tab_left, "go-previous", i18n("Left Sidebar"))
122
s.addWidget(self.docks[Left])
125
self.docks[Right] = Dock(s, tab_right, "go-next", i18n("Right Sidebar"))
126
s.addWidget(self.docks[Right])
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))
134
tab_top = TabBar(Top, v)
135
s1 = QSplitter(Qt.Vertical, v)
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
141
layout = QVBoxLayout()
143
layout.setContentsMargins(0, 0, 0, 0)
145
self.viewTabs = self.createViewTabBar()
146
layout.addWidget(self.viewTabs)
147
self.viewStack = QStackedWidget()
148
layout.addWidget(self.viewStack)
150
self.docks[Bottom] = Dock(s1, tab_bottom, "go-down", i18n("Bottom Sidebar"))
151
s1.addWidget(self.docks[Bottom])
153
s1.setStretchFactor(0, 0)
154
s1.setStretchFactor(1, 1)
155
s1.setStretchFactor(2, 0)
156
s1.setChildrenCollapsible(False)
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))
162
self.viewStack.setMinimumSize(200, 100)
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
170
if not self.initialGeometrySet():
171
self.resize(700, 480)
173
self.setupGeneratedMenus()
174
self.setAutoSaveSettings()
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)
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"),
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)
205
a = self.actionCollection().addAction(KStandardAction.FullScreen, 'fullscreen')
206
a.toggled.connect(lambda t: KToggleFullScreenAction.setFullScreen(self, t))
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,
214
self.act('options_configure_keys', KStandardAction.KeyBindings,
218
a = KActionMenu(i18n("&Tool Views"), self)
219
self.actionCollection().addAction('options_toolviews', a)
220
def makefunc(action):
224
for tool in self.tools.itervalues():
226
menu.addAction(tool.action())
227
tool.addMenuActions(menu)
229
a.menu().aboutToShow.connect(makefunc(a))
232
@self.onAction(i18n("New..."), "document-new")
234
self.sessionManager().new()
236
@self.onAction(KStandardGuiItem.save().text(), "document-save")
238
self.sessionManager().save()
240
@self.onAction(i18n("Manage Sessions..."), "view-choose")
241
def sessions_manage():
242
self.sessionManager().manage()
245
def setupTools(self):
246
"""Implement this to create the Tool instances.
248
This is called before the ui.rc file is loaded, so the user can
249
configure the keyboard shortcuts for the tools.
253
def xmlGuiContainer(self, name):
254
"""Returns the XMLGUI container with name.
256
If not present, the local ui.rc file is probably erroneous,
257
inform the user via a message box.
260
obj = self.factory().container(name, self)
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",
271
KGlobal.dirs().saveLocation('appdata'),
274
def setupGeneratedMenus(self):
275
"""This should setup menus that are generated on show.
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
283
# Set up the documents menu so that it shows all open documents.
284
docMenu = self.xmlGuiContainer("documents")
286
docGroup = QActionGroup(docMenu)
287
docGroup.setExclusive(True)
288
docGroup.triggered.connect(lambda a: a.doc().setActive())
290
def populateDocMenu():
291
for a in docGroup.actions():
293
for d in self.app.documents:
294
a = KAction(d.documentName(), docGroup)
296
a.doc = weakref.ref(d)
297
icon = d.documentIcon()
299
a.setIcon(KIcon(icon))
300
if d is self._currentDoc:
302
docGroup.addAction(a)
304
addAccelerators(docMenu.actions())
305
docMenu.setParent(docMenu.parent()) # BUG: SIP otherwise looses outer scope
306
docMenu.aboutToShow.connect(populateDocMenu)
309
sessMenu = self.xmlGuiContainer("sessions")
311
sessGroup = QActionGroup(sessMenu)
312
sessGroup.setExclusive(True)
314
def populateSessMenu():
315
for a in sessGroup.actions():
318
sm = self.sessionManager()
319
sessions = sm.names()
320
current = sm.current()
324
# "No Session" action
325
a = KAction(i18n("No Session"), sessGroup)
330
a.triggered.connect(lambda: sm.switch(None))
331
sessGroup.addAction(a)
332
sessMenu.addAction(a)
333
sessGroup.addAction(sessMenu.addSeparator())
335
for name in sessions:
336
a = KAction(name, sessGroup)
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)
348
def sessionManager(self):
349
return self.createSessionManager()
351
def createSessionManager(self):
352
"""Override this to return a different session manager."""
353
return SessionManager(self)
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)
361
a = self.actionCollection().addAction(name)
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))
370
def selAct(self, *args, **kwargs):
371
a = self.act(*args, **kwargs)
372
self._selectionActions.append(a)
375
def onAction(self, texttype, icon=None, tooltip=None, whatsthis=None, key=None):
376
"""Decorator to add a function to an action.
378
The name of the function becomes the name of the action.
382
self.act(func.func_name, texttype, func, icon, tooltip, whatsthis, key)
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.
390
The name of the function becomes the name of the action.
391
If there is no selection, the action is automatically disabled.
396
doc = self.currentDocument()
398
text = doc.selectionText()
401
if result is not None:
402
doc.replaceSelectionWith(result, keepSelection)
404
KMessageBox.sorry(self, i18n("Please select some text first."))
405
self.selAct(func.func_name, texttype, selfunc, icon, tooltip, whatsthis, key)
409
def setCurrentDocument(self, doc):
410
"""Called when the application makes a different Document active."""
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)
425
self.updateStatusBar()
426
self.updateSelection()
427
self.currentDocumentChanged(doc) # emit our signal
429
def addDocument(self, doc):
430
"""Internal. Add Document to our viewStack."""
431
self.viewStack.addWidget(doc.view)
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
441
"""Returns the current view or None if none."""
443
return self._currentDoc.view
445
def currentDocument(self):
446
"""Returns the current Document or None if none."""
447
return self._currentDoc
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)
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()
460
name = (self.showPath.isChecked() and doc.prettyUrl() or
463
name = '...' + name[-69:]
466
caption += " [{0}]".format(i18n("modified"))
467
self.sb_modified.setPixmap(KIcon("document-save").pixmap(16))
469
self.sb_modified.setPixmap(QPixmap())
470
self.setCaption(caption)
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())
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:
486
if doc.view.blockSelection():
487
text, tip = i18n("BLOCK"), i18n("Block selection mode")
489
text, tip = i18n("LINE"), i18n("Line selection mode")
490
self.sb_selmode.setText(" {0} ".format(text))
491
self.sb_selmode.setToolTip(tip)
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)
502
def allActionCollections(self):
503
"""Iterator over KActionCollections.
505
Yields all KActionCollections that need to be checked if the user
506
wants to alter a keyboard shortcut.
508
Each item is a two-tuple (name, KActionCollection).
511
yield KGlobal.mainComponent().aboutData().programName(), self.actionCollection()
513
yield None, self.view().actionCollection()
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)
527
def newDocument(self):
528
"""Create a new empty document."""
529
self.app.createDocument().setActive()
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()]
544
def addToRecentFiles(self, doc=None):
545
"""Add url of document to recently opened files."""
546
doc = doc or self.currentDocument()
549
if not url.isEmpty() and url not in self.openRecent.urls():
550
self.openRecent.addUrl(url)
552
@pyqtSignature("KUrl")
553
def slotOpenRecent(self, url):
554
"""Called by the open recent files action."""
555
self.app.openUrl(url).setActive()
557
def queryClose(self):
558
"""Called when the user wants to close the MainWindow.
560
Returns True if the application may quit.
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
571
if not d.queryClose():
572
return False # cancelled
573
# Then close the documents
574
self.currentDocumentChanged.clear() # disconnect all tools etc.
576
for d in self.app.history[:]: # iterate over a copy
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]
588
if not d.queryClose():
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"))
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)
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)
612
docs = [self.app.openUrl(KUrl(url)) for url in urls]
614
if active < 0 or active >= len(docs):
615
active = len(docs) - 1
616
docs[active].setActive()
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())
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():
633
# also the main editor object:
634
self.app.editor.writeConfig()
638
def createViewTabBar(self):
639
return ViewTabBar(self)
641
def dragEnterEvent(self, event):
642
event.setAccepted(KUrl.List.canDecode(event.mimeData()))
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)
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
659
# get the documents to create their tabs.
660
for doc in mainwin.app.documents:
661
self.addDocument(doc)
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)
671
self.tabCloseRequested.connect(self.slotTabCloseRequested)
672
except AttributeError:
676
self.tabMoved.connect(self.slotTabMoved)
677
except AttributeError:
681
def readSettings(self):
682
# closeable? only in Qt >= 4.6
684
self.setTabsClosable(config("tab bar").readEntry("close button", True))
685
except AttributeError:
688
# expanding? only in Qt >= 4.5
690
self.setExpanding(config("tab bar").readEntry("expanding", False))
691
except AttributeError:
694
# movable? only in Qt >= 4.5
696
self.setMovable(config("tab bar").readEntry("movable", True))
697
except AttributeError:
700
def addDocument(self, doc):
701
if doc not in self.docs:
702
self.docs.append(doc)
703
self.blockSignals(True)
705
self.blockSignals(False)
706
self.setDocumentStatus(doc)
707
doc.urlChanged.connect(self.setDocumentStatus)
708
doc.captionChanged.connect(self.setDocumentStatus)
710
def removeDocument(self, doc):
712
index = self.docs.index(doc)
713
self.docs.remove(doc)
714
self.blockSignals(True)
715
self.removeTab(index)
716
self.blockSignals(False)
718
def setDocumentStatus(self, doc):
720
index = self.docs.index(doc)
721
self.setTabIcon(index, KIcon(doc.documentIcon() or "text-plain"))
722
self.setTabText(index, doc.documentName())
724
def setCurrentDocument(self, doc):
725
""" Raise the tab belonging to this document."""
727
index = self.docs.index(doc)
728
self.blockSignals(True)
729
self.setCurrentIndex(index)
730
self.blockSignals(False)
732
def slotCurrentChanged(self, index):
733
""" Called when the user clicks a tab. """
734
self.docs[index].setActive()
736
def slotTabCloseRequested(self, index):
737
""" Called when the user clicks the close button. """
738
self.docs[index].close()
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)
745
def contextMenuEvent(self, ev):
746
""" Called when the right mouse button is clicked on the tab bar. """
747
tab = self.tabAt(ev.pos())
750
self.addMenuActions(menu, self.docs[tab])
751
menu.exec_(ev.globalPos())
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())
762
g = KStandardGuiItem.close()
763
a = menu.addAction(g.icon(), g.text())
764
a.triggered.connect(lambda: doc.close())
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)
773
if orientation in (KMultiTabBar.Bottom, KMultiTabBar.Top):
774
self.setMaximumHeight(maxSize)
776
self.setMaximumWidth(maxSize)
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)))
792
def removeTool(self, tool):
793
self._tools.remove(tool)
794
self.removeTab(tool._id)
796
def showTool(self, tool):
797
self.tab(tool._id).setState(True)
799
def hideTool(self, tool):
800
self.tab(tool._id).setState(False)
802
def updateState(self, tool):
803
tab = self.tab(tool._id)
804
tab.setIcon(tool.icon())
805
tab.setText(tool.title())
808
class Dock(QStackedWidget):
809
"""A dock where tools can be added to.
811
Hides itself when there are no tools visible.
812
When it receives a tool, a button is created in the associated tabbar.
815
def __init__(self, parent, tabbar, icon, title):
816
QStackedWidget.__init__(self, parent)
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
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)
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
839
def showTool(self, tool):
840
"""Internal: only to be called by tool.show().
842
Use tool.show() to make a tool active.
845
if tool not in self._tools or tool is self._currentTool:
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
858
def hideTool(self, tool):
859
"""Internal: only to be called by tool.hide().
861
Use tool.hide() to make a tool inactive.
864
self.tabbar.hideTool(tool)
865
if tool is self._currentTool:
866
self._currentTool = None
869
def currentTool(self):
870
return self._currentTool
872
def updateState(self, tool):
873
self.tabbar.updateState(tool)
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)
882
self.setAttribute(Qt.WA_DeleteOnClose, False)
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)
891
if self.tool.dialogPos:
892
self.move(self.tool.dialogPos)
895
self.tool.dialogSize = self.size()
896
self.tool.dialogPos = self.pos()
898
QDialog.done(self, r)
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())
906
def keyPressEvent(self, e):
907
if e.key() != Qt.Key_Escape:
908
QDialog.keyPressEvent(self, e)
912
"""A Tool, that can be docked or undocked in/from the MainWindow.
914
Intended to be subclassed.
917
allowedPlaces = Top, Right, Bottom, Left
921
helpAnchor, helpAppName = "", ""
923
__instance_counter = 0
925
def __init__(self, mainwin, name,
926
title="", icon="", key="", dock=Right,
928
self._id = Tool.__instance_counter
933
self.dialogSize = None
934
self.dialogPos = None
935
self.mainwin = mainwin
937
mainwin.tools[name] = self
938
action = KAction(mainwin, triggered=self.slotAction) # action to toggle our view
939
mainwin.actionCollection().addAction("tool_" + name, action)
941
action.setShortcut(KShortcut(key))
946
Tool.__instance_counter += 1
950
return self.mainwin.actionCollection().action("tool_" + self.name)
952
def slotAction(self):
953
"""Called when our action is triggered.
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).
962
def materialize(self):
963
"""If not yet done, calls self.factory() to get the widget of our tool.
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.
970
if self.widget is None:
971
with self.mainwin.app.busyCursor():
972
self.widget = self.factory()
975
"""Should return this Tool's widget when it must become visible.
977
I you didn't supply a widget on init, you must override this method.
983
"""Completely remove the tool.
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.
991
self._dock.removeTool(self)
993
sip.delete(self.widget)
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]
1001
""" Bring our tool into view. """
1005
self._dock.showTool(self)
1007
self._dialog.raise_()
1010
""" Hide our tool. """
1012
self._active = False
1013
self._dock.hideTool(self)
1014
view = self.mainwin.view()
1019
""" Toggle visibility if docked. """
1027
""" Returns True if the tool is currently the active one in its dock."""
1031
""" Returns True if the tool is docked. """
1034
def setDock(self, place):
1035
""" Puts the tool in one of the four places.
1037
place is one of (KMultiTabBar).Top, Right, Bottom, Left
1040
dock = self.mainwin.docks.get(place, self._dock)
1041
if dock is self._dock:
1045
self._dock.removeTool(self)
1050
""" Undock our widget """
1051
if not self._docked:
1053
self._dock.removeTool(self)
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)
1068
""" Dock and hide the dialog window """
1073
self._dock.addTool(self)
1078
def setIcon(self, icon):
1079
self._icon = icon and KIcon(icon) or KIcon()
1080
self.action().setIcon(self._icon)
1086
def setTitle(self, title):
1088
self.action().setText(self._title)
1091
def updateState(self):
1094
self._dock.updateState(self)
1096
self._dialog.updateState()
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]
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))
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:
1116
a = m.addAction(KIcon("help-contextual"), KStandardGuiItem.help().text())
1117
a.triggered.connect(self.help)
1118
m.aboutToHide.connect(m.deleteLater)
1121
def addMenuActions(self, menu):
1122
"""Use this to add your own actions to a tool menu."""
1126
""" Return a suitable configgroup for our settings. """
1127
return config("tool_" + self.name)
1129
def loadSettings(self):
1130
""" Do not override this method, use readConfig instead. """
1131
conf = self.config()
1132
self.readConfig(conf)
1134
def saveSettings(self):
1135
""" Do not override this method, use writeConfig instead. """
1136
conf = self.config()
1137
self.writeConfig(conf)
1139
def readConfig(self, conf):
1140
"""Implement this in your subclass to read additional config data."""
1143
def writeConfig(self, conf):
1144
"""Implement this in your subclass to write additional config data."""
1148
"""Invokes Help on our tool.
1150
See the helpAnchor and helpAppName attributes.
1153
KToolInvocation.invokeHelp(self.helpAnchor, self.helpAppName)
1156
class KPartTool(Tool):
1157
"""A Tool where the widget is loaded via the KParts system."""
1159
# set this to the library name you want to load
1161
# set this to the name of the app containing this part
1164
def __init__(self, mainwin, name, title="", icon="", key="", dock=Right):
1167
Tool.__init__(self, mainwin, name, title, icon, key, dock)
1172
factory = KPluginLoader(self._partlibrary).factory()
1174
part = factory.create(self.mainwin)
1177
part.destroyed.connect(self.slotDestroyed, Qt.DirectConnection)
1178
QTimer.singleShot(0, self.partLoaded)
1179
return part.widget()
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)))
1185
def partLoaded(self):
1186
""" Called when part is loaded. Use this to apply settings, etc."""
1191
self.part.destroyed.disconnect(self.slotDestroyed)
1192
super(KPartTool, self).delete()
1194
def slotDestroyed(self):
1198
if not sip.isdeleted(self.mainwin):
1202
self._active = False
1203
self._dialog.done(0)
1205
def openUrl(self, url):
1206
""" Expects KUrl."""
1208
self.part.openUrl(url)
1211
class UserShortcutManager(object):
1212
"""Manages user-defined keyboard shortcuts.
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
1218
You should subclass this base class and implement the widget() and client()
1223
# which config group to store our shortcuts
1224
configGroup = "user shortcuts"
1226
# the shortcut type to use
1227
shortcutContext = Qt.WidgetWithChildrenShortcut
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, ""):
1239
self._collection.readSettings()
1242
"""Should return the widget where the actions should be added to."""
1246
"""Should return the object that can further process our actions.
1248
Most times this will be a kateshell.shortcut.ShortcutClientBase instance.
1250
It should have the following methods:
1251
- actionTriggered(name)
1252
- populateAction(name, action)
1257
def addAction(self, name):
1258
"""(Internal) Create a new action with name name.
1260
If existing, return the existing action.
1263
action = self._collection.action(name)
1265
action = self._collection.addAction(name)
1266
action.setShortcutContext(self.shortcutContext)
1267
action.triggered.connect(lambda: self.client().actionTriggered(name))
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():
1276
self.client().populateAction(action.objectName(), action)
1277
return self._collection
1280
class SessionManager(object):
1281
"""Manages sessions (basically lists of open documents).
1283
Sessions are stored in the appdata/sessions configfile, with each session
1287
sessionChanged = Signal()
1288
sessionAdded = Signal()
1290
def __init__(self, mainwin):
1291
self.mainwin = mainwin
1292
mainwin.aboutToClose.connect(self.shutdown)
1293
self._current = None
1294
self.sessionConfig = None
1298
"""Destroys and recreate the sessions KConfig object.
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.
1304
if self.sessionConfig:
1305
self.sessionConfig.sync()
1306
sip.delete(self.sessionConfig)
1307
self.sessionConfig = KConfig("sessions", KConfig.NoGlobals, "appdata")
1309
def config(self, session=None):
1310
"""Returns the config group for the named or current session.
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.
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)
1324
"""Opens the Manage Sessions dialog."""
1325
self.managerDialog().show()
1328
def managerDialog(self):
1329
return self.createManagerDialog()
1331
def createManagerDialog(self):
1332
"""Override this to return a subclass of ManagerDialog."""
1333
import kateshell.sessions
1334
return kateshell.sessions.ManagerDialog(self)
1337
def editorDialog(self):
1338
"""Returns a dialog to edit properties of the session."""
1339
return self.createEditorDialog()
1341
def createEditorDialog(self):
1342
"""Override this to return a subclass of EditorDialog."""
1343
import kateshell.sessions
1344
return kateshell.sessions.EditorDialog(self)
1346
def switch(self, name):
1347
"""Switches to the given session.
1349
Use None or "none" for the no-session state.
1350
If the given session does not exist, it is created from the current
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():
1367
self.mainwin.loadDocumentList(self.config(name))
1369
# this group did not exist, create it
1370
self.addSession(name)
1371
self._current = name
1372
self.sessionChanged()
1376
"""Returns a list of names of all sessions."""
1377
names = self.sessionConfig.groupList()
1378
names.sort(key=naturalsort)
1382
"""Returns the name of the current session, or None if none."""
1383
return self._current
1386
"""Saves the current session."""
1387
if self._current is None:
1390
self.mainwin.saveDocumentList(self.config())
1393
"""Saves the current session if the session wants to be autosaved."""
1394
if self._current and self.config().readEntry("autosave", True):
1396
self.config().sync()
1399
"""Called on application exit."""
1400
self.config(False).writeEntry("lastused", self.current() or "none")
1403
def restoreLastSession(self):
1404
"""Restores the last saved session."""
1405
name = self.config(False).readEntry("lastused", "none")
1409
def saveProperties(self, conf):
1410
"""Save our state in a session group of QSessionManager."""
1411
conf.writeEntry("name", self._current or "none")
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
1419
self.sessionChanged()
1422
"""Prompts for a name for a new session.
1424
If the user enters a name and accepts the dialog, the session is
1425
created and switched to.
1428
name = self.editorDialog().edit()
1430
# switch there with the current document list
1431
self.mainwin.saveDocumentList(self.config(name))
1432
self._current = name
1433
self.sessionChanged()
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()
1444
def renameSession(self, old, new):
1445
"""Renames a session.
1447
The document list is taken over but not the other settings.
1448
Both names must be valid session names, and old must exist.
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))
1456
if old == self._current:
1458
self.sessionChanged()
1459
self.config(old).deleteGroup()
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))
1468
"""Returns the configured base directory for this session, if any."""
1470
return self.config().readPathEntry("basedir", "")