1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/ 2# 3# Copyright (c) 2008 - 2014 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""" 21The snippets widget. 22""" 23 24 25from PyQt5.QtCore import QEvent, QItemSelectionModel, QModelIndex, Qt 26from PyQt5.QtGui import QKeySequence 27from PyQt5.QtWidgets import ( 28 QAction, QApplication, QCompleter, QFileDialog, QHBoxLayout, QMenu, 29 QMessageBox, QPushButton, QSplitter, QTextBrowser, QToolButton, 30 QTreeView, QVBoxLayout, QWidget) 31 32import app 33import userguide 34import icons 35import widgets.lineedit 36import textformats 37import actioncollectionmanager 38 39from . import actions 40from . import model 41from . import snippets 42from . import edit 43from . import insert 44from . import highlight 45 46 47class Widget(QWidget): 48 def __init__(self, panel): 49 super(Widget, self).__init__(panel) 50 51 layout = QVBoxLayout() 52 self.setLayout(layout) 53 layout.setSpacing(0) 54 55 self.searchEntry = SearchLineEdit() 56 self.treeView = QTreeView(contextMenuPolicy=Qt.CustomContextMenu) 57 self.textView = QTextBrowser() 58 59 applyButton = QToolButton(autoRaise=True) 60 editButton = QToolButton(autoRaise=True) 61 addButton = QToolButton(autoRaise=True) 62 self.menuButton = QPushButton(flat=True) 63 menu = QMenu(self.menuButton) 64 self.menuButton.setMenu(menu) 65 66 splitter = QSplitter(Qt.Vertical) 67 top = QHBoxLayout() 68 layout.addLayout(top) 69 splitter.addWidget(self.treeView) 70 splitter.addWidget(self.textView) 71 layout.addWidget(splitter) 72 splitter.setSizes([200, 100]) 73 splitter.setCollapsible(0, False) 74 75 top.addWidget(self.searchEntry) 76 top.addWidget(applyButton) 77 top.addSpacing(10) 78 top.addWidget(addButton) 79 top.addWidget(editButton) 80 top.addWidget(self.menuButton) 81 82 # action generator for actions added to search entry 83 def act(slot, icon=None): 84 a = QAction(self, triggered=slot) 85 self.addAction(a) 86 a.setShortcutContext(Qt.WidgetWithChildrenShortcut) 87 icon and a.setIcon(icons.get(icon)) 88 return a 89 90 # hide if ESC pressed in lineedit 91 a = act(self.slotEscapePressed) 92 a.setShortcut(QKeySequence(Qt.Key_Escape)) 93 94 # import action 95 a = self.importAction = act(self.slotImport, 'document-open') 96 menu.addAction(a) 97 98 # export action 99 a = self.exportAction = act(self.slotExport, 'document-save-as') 100 menu.addAction(a) 101 102 # apply button 103 a = self.applyAction = act(self.slotApply, 'edit-paste') 104 applyButton.setDefaultAction(a) 105 menu.addSeparator() 106 menu.addAction(a) 107 108 # add button 109 a = self.addAction_ = act(self.slotAdd, 'list-add') 110 a.setShortcut(QKeySequence(Qt.Key_Insert)) 111 addButton.setDefaultAction(a) 112 menu.addSeparator() 113 menu.addAction(a) 114 115 # edit button 116 a = self.editAction = act(self.slotEdit, 'document-edit') 117 a.setShortcut(QKeySequence(Qt.Key_F2)) 118 editButton.setDefaultAction(a) 119 menu.addAction(a) 120 121 # set shortcut action 122 a = self.shortcutAction = act(self.slotShortcut, 'preferences-desktop-keyboard-shortcuts') 123 menu.addAction(a) 124 125 # delete action 126 a = self.deleteAction = act(self.slotDelete, 'list-remove') 127 a.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Delete)) 128 menu.addAction(a) 129 130 # restore action 131 a = self.restoreAction = act(self.slotRestore) 132 menu.addSeparator() 133 menu.addAction(a) 134 135 # help button 136 a = self.helpAction = act(self.slotHelp, 'help-contents') 137 menu.addSeparator() 138 menu.addAction(a) 139 140 self.treeView.setSelectionBehavior(QTreeView.SelectRows) 141 self.treeView.setSelectionMode(QTreeView.ExtendedSelection) 142 self.treeView.setRootIsDecorated(False) 143 self.treeView.setAllColumnsShowFocus(True) 144 self.treeView.setModel(model.model()) 145 self.treeView.setCurrentIndex(QModelIndex()) 146 147 # signals 148 self.searchEntry.returnPressed.connect(self.slotReturnPressed) 149 self.searchEntry.textChanged.connect(self.updateFilter) 150 self.treeView.doubleClicked.connect(self.slotDoubleClicked) 151 self.treeView.customContextMenuRequested.connect(self.showContextMenu) 152 self.treeView.selectionModel().currentChanged.connect(self.updateText) 153 self.treeView.model().dataChanged.connect(self.updateFilter) 154 155 # highlight text 156 self.highlighter = highlight.Highlighter(self.textView.document()) 157 158 # complete on snippet variables 159 self.searchEntry.setCompleter(QCompleter([ 160 ':icon', ':indent', ':menu', ':name', ':python', ':selection', 161 ':set', ':symbol', ':template', ':template-run'], self.searchEntry)) 162 self.readSettings() 163 app.settingsChanged.connect(self.readSettings) 164 app.translateUI(self) 165 self.updateColumnSizes() 166 self.setAcceptDrops(True) 167 168 def dropEvent(self, ev): 169 if not ev.source() and ev.mimeData().hasUrls(): 170 filename = ev.mimeData().urls()[0].toLocalFile() 171 if filename: 172 ev.accept() 173 from . import import_export 174 import_export.load(filename, self) 175 176 def dragEnterEvent(self, ev): 177 if not ev.source() and ev.mimeData().hasUrls(): 178 ev.accept() 179 180 def translateUI(self): 181 try: 182 self.searchEntry.setPlaceholderText(_("Search...")) 183 except AttributeError: 184 pass # not in Qt 4.6 185 shortcut = lambda a: a.shortcut().toString(QKeySequence.NativeText) 186 self.menuButton.setText(_("&Menu")) 187 self.addAction_.setText(_("&Add...")) 188 self.addAction_.setToolTip( 189 _("Add a new snippet. ({key})").format(key=shortcut(self.addAction_))) 190 self.editAction.setText(_("&Edit...")) 191 self.editAction.setToolTip( 192 _("Edit the current snippet. ({key})").format(key=shortcut(self.editAction))) 193 self.shortcutAction.setText(_("Configure Keyboard &Shortcut...")) 194 self.deleteAction.setText(_("&Remove")) 195 self.deleteAction.setToolTip(_("Remove the selected snippets.")) 196 self.applyAction.setText(_("A&pply")) 197 self.applyAction.setToolTip(_("Apply the current snippet.")) 198 self.importAction.setText(_("&Import...")) 199 self.importAction.setToolTip(_("Import snippets from a file.")) 200 self.exportAction.setText(_("E&xport...")) 201 self.exportAction.setToolTip(_("Export snippets to a file.")) 202 self.restoreAction.setText(_("Restore &Built-in Snippets...")) 203 self.restoreAction.setToolTip( 204 _("Restore deleted or changed built-in snippets.")) 205 self.helpAction.setText(_("&Help")) 206 self.searchEntry.setToolTip(_( 207 "Enter text to search in the snippets list.\n" 208 "See \"What's This\" for more information.")) 209 self.searchEntry.setWhatsThis(''.join(map("<p>{0}</p>\n".format, ( 210 _("Enter text to search in the snippets list, and " 211 "press Enter to apply the currently selected snippet."), 212 _("If the search text fully matches the value of the '{name}' variable " 213 "of a snippet, that snippet is selected.").format(name="name"), 214 _("If the search text starts with a colon ':', the rest of the " 215 "search text filters snippets that define the given variable. " 216 "After a space a value can also be entered, snippets will then " 217 "match if the value of the given variable contains the text after " 218 "the space."), 219 _("E.g. entering {menu} will show all snippets that are displayed " 220 "in the insert menu.").format(menu="<code>:menu</code>"), 221 )))) 222 223 def sizeHint(self): 224 return self.parent().mainwindow().size() / 4 225 226 def readSettings(self): 227 data = textformats.formatData('editor') 228 self.textView.setFont(data.font) 229 self.textView.setPalette(data.palette()) 230 231 def showContextMenu(self, pos): 232 """Called when the user right-clicks the tree view.""" 233 self.menuButton.menu().popup(self.treeView.viewport().mapToGlobal(pos)) 234 235 def slotReturnPressed(self): 236 """Called when the user presses Return in the search entry. Applies current snippet.""" 237 name = self.currentSnippet() 238 if name: 239 view = self.parent().mainwindow().currentView() 240 insert.insert(name, view) 241 self.parent().hide() # make configurable? 242 view.setFocus() 243 244 def slotEscapePressed(self): 245 """Called when the user presses ESC in the search entry. Hides the panel.""" 246 self.parent().hide() 247 self.parent().mainwindow().currentView().setFocus() 248 249 def slotDoubleClicked(self, index): 250 name = self.treeView.model().name(index) 251 view = self.parent().mainwindow().currentView() 252 insert.insert(name, view) 253 254 def slotAdd(self): 255 """Called when the user wants to add a new snippet.""" 256 edit.Edit(self, None) 257 258 def slotEdit(self): 259 """Called when the user wants to edit a snippet.""" 260 name = self.currentSnippet() 261 if name: 262 edit.Edit(self, name) 263 264 def slotShortcut(self): 265 """Called when the user selects the Configure Shortcut action.""" 266 from widgets import shortcuteditdialog 267 name = self.currentSnippet() 268 if name: 269 collection = self.parent().snippetActions 270 action = actions.action(name, None, collection) 271 default = collection.defaults().get(name) 272 mgr = actioncollectionmanager.manager(self.parent().mainwindow()) 273 cb = mgr.findShortcutConflict 274 dlg = shortcuteditdialog.ShortcutEditDialog(self, cb, (collection, name)) 275 276 if dlg.editAction(action, default): 277 mgr.removeShortcuts(action.shortcuts()) 278 collection.setShortcuts(name, action.shortcuts()) 279 self.treeView.update() 280 281 def slotDelete(self): 282 """Called when the user wants to delete the selected rows.""" 283 rows = sorted(set(i.row() for i in self.treeView.selectedIndexes()), reverse=True) 284 if rows: 285 for row in rows: 286 name = self.treeView.model().names()[row] 287 self.parent().snippetActions.setShortcuts(name, []) 288 self.treeView.model().removeRow(row) 289 self.updateFilter() 290 291 def slotApply(self): 292 """Called when the user clicks the apply button. Applies current snippet.""" 293 name = self.currentSnippet() 294 if name: 295 view = self.parent().mainwindow().currentView() 296 insert.insert(name, view) 297 298 def slotImport(self): 299 """Called when the user activates the import action.""" 300 filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) 301 caption = app.caption(_("dialog title", "Import Snippets")) 302 filename = None 303 filename = QFileDialog.getOpenFileName(self, caption, filename, filetypes)[0] 304 if filename: 305 from . import import_export 306 import_export.load(filename, self) 307 308 def slotExport(self): 309 """Called when the user activates the export action.""" 310 allrows = [row for row in range(model.model().rowCount()) 311 if not self.treeView.isRowHidden(row, QModelIndex())] 312 selectedrows = [i.row() for i in self.treeView.selectedIndexes() 313 if i.column() == 0 and i.row() in allrows] 314 names = self.treeView.model().names() 315 names = [names[row] for row in selectedrows or allrows] 316 317 filetypes = "{0} (*.xml);;{1} (*)".format(_("XML Files"), _("All Files")) 318 n = len(names) 319 caption = app.caption(_("dialog title", 320 "Export {num} Snippet", "Export {num} Snippets", n).format(num=n)) 321 filename = QFileDialog.getSaveFileName(self, caption, None, filetypes)[0] 322 if filename: 323 from . import import_export 324 try: 325 import_export.save(names, filename) 326 except (IOError, OSError) as e: 327 QMessageBox.critical(self, _("Error"), _( 328 "Can't write to destination:\n\n{url}\n\n{error}").format( 329 url=filename, error=e.strerror)) 330 331 def slotRestore(self): 332 """Called when the user activates the Restore action.""" 333 from . import restore 334 dlg = restore.RestoreDialog(self) 335 dlg.setWindowModality(Qt.WindowModal) 336 dlg.populate() 337 dlg.show() 338 dlg.finished.connect(dlg.deleteLater) 339 340 def slotHelp(self): 341 """Called when the user clicks the small help button.""" 342 userguide.show("snippets") 343 344 def currentSnippet(self): 345 """Returns the name of the current snippet if it is visible.""" 346 row = self.treeView.currentIndex().row() 347 if row != -1 and not self.treeView.isRowHidden(row, QModelIndex()): 348 return self.treeView.model().names()[row] 349 350 def updateFilter(self): 351 """Called when the text in the entry changes, updates search results.""" 352 text = self.searchEntry.text() 353 ltext = text.lower() 354 filterVars = text.startswith(':') 355 if filterVars: 356 try: 357 fvar, fval = text[1:].split(None, 1) 358 fhide = lambda v: v.get(fvar) in (True, None) or fval not in v.get(fvar) 359 except ValueError: 360 fvar = text[1:].strip() 361 fhide = lambda v: not v.get(fvar) 362 for row in range(self.treeView.model().rowCount()): 363 name = self.treeView.model().names()[row] 364 nameid = snippets.get(name).variables.get('name', '') 365 if filterVars: 366 hide = fhide(snippets.get(name).variables) 367 elif nameid == text: 368 i = self.treeView.model().createIndex(row, 0) 369 self.treeView.selectionModel().setCurrentIndex(i, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) 370 hide = False 371 elif nameid.lower().startswith(ltext): 372 hide = False 373 elif ltext in snippets.title(name).lower(): 374 hide = False 375 else: 376 hide = True 377 self.treeView.setRowHidden(row, QModelIndex(), hide) 378 self.updateText() 379 380 def updateText(self): 381 """Called when the current snippet changes.""" 382 name = self.currentSnippet() 383 self.textView.clear() 384 if name: 385 s = snippets.get(name) 386 self.highlighter.setPython('python' in s.variables) 387 self.textView.setPlainText(s.text) 388 389 def updateColumnSizes(self): 390 self.treeView.resizeColumnToContents(0) 391 self.treeView.resizeColumnToContents(1) 392 393 394class SearchLineEdit(widgets.lineedit.LineEdit): 395 def __init__(self, *args): 396 super(SearchLineEdit, self).__init__(*args) 397 398 def event(self, ev): 399 if ev.type() == QEvent.KeyPress and any(ev.matches(key) for key in ( 400 QKeySequence.MoveToNextLine, QKeySequence.SelectNextLine, 401 QKeySequence.MoveToPreviousLine, QKeySequence.SelectPreviousLine, 402 QKeySequence.MoveToNextPage, QKeySequence.SelectNextPage, 403 QKeySequence.MoveToPreviousPage, QKeySequence.SelectPreviousPage)): 404 QApplication.sendEvent(self.parent().treeView, ev) 405 return True 406 return super(SearchLineEdit, self).event(ev) 407 408 409