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""" 21Session dialog for named session stuff. 22""" 23 24 25import os 26import json 27 28from PyQt5.QtCore import Qt, QSettings, QUrl 29from PyQt5.QtWidgets import ( 30 QAbstractItemView, QCheckBox, QDialog, QDialogButtonBox, QFileDialog, 31 QGridLayout, QGroupBox, QLabel, QListWidgetItem, QLineEdit, QMessageBox, 32 QPushButton, QVBoxLayout) 33 34import app 35import widgets.listedit 36import widgets.urlrequester 37import sessions.manager 38import qsettings 39import userguide 40 41 42class SessionManagerDialog(QDialog): 43 def __init__(self, mainwindow): 44 super(SessionManagerDialog, self).__init__(mainwindow) 45 self.setWindowModality(Qt.WindowModal) 46 layout = QVBoxLayout() 47 self.setLayout(layout) 48 49 self.sessions = SessionList(self) 50 layout.addWidget(self.sessions) 51 52 self.imp = QPushButton(self) 53 self.exp = QPushButton(self) 54 self.act = QPushButton(self) 55 self.imp.clicked.connect(self.importSession) 56 self.exp.clicked.connect(self.exportSession) 57 self.act.clicked.connect(self.activateSession) 58 59 self.sessions.layout().addWidget(self.imp, 5, 1) 60 self.sessions.layout().addWidget(self.exp, 6, 1) 61 self.sessions.layout().addWidget(self.act, 7, 1) 62 63 layout.addWidget(widgets.Separator()) 64 65 self.buttons = b = QDialogButtonBox(self) 66 layout.addWidget(b) 67 b.setStandardButtons(QDialogButtonBox.Close) 68 b.rejected.connect(self.accept) 69 userguide.addButton(b, "sessions") 70 self.sessions.load() 71 app.translateUI(self) 72 self.sessions.changed.connect(self.enableButtons) 73 self.sessions.listBox.itemSelectionChanged.connect(self.enableButtons) 74 self.enableButtons() 75 76 def translateUI(self): 77 self.setWindowTitle(app.caption(_("Manage Sessions"))) 78 self.imp.setText(_("&Import...")) 79 self.imp.setToolTip(_("Opens a dialog to import a session from a file.")) 80 self.exp.setText(_("E&xport...")) 81 self.exp.setToolTip(_("Opens a dialog to export a session to a file.")) 82 self.act.setText(_("&Activate")) 83 self.act.setToolTip(_("Switches to the selected session.")) 84 85 def enableButtons(self): 86 """Called when the selection in the listedit changes.""" 87 enabled = bool(self.sessions.listBox.currentItem()) 88 self.act.setEnabled(enabled) 89 self.exp.setEnabled(enabled) 90 91 def importSession(self): 92 """Called when the user clicks Import.""" 93 filetypes = '{0} (*.json);;{1} (*)'.format(_("JSON Files"), _("All Files")) 94 caption = app.caption(_("dialog title", "Import session")) 95 mainwindow = self.parent() 96 directory = os.path.dirname(mainwindow.currentDocument().url().toLocalFile()) or app.basedir() 97 importfile = QFileDialog.getOpenFileName(mainwindow, caption, directory, filetypes)[0] 98 if not importfile: 99 return # cancelled by user 100 try: 101 with open(importfile, 'r') as f: 102 self.sessions.importItem(json.load(f)) 103 except IOError as e: 104 msg = _("{message}\n\n{strerror} ({errno})").format( 105 message = _("Could not read from: {url}").format(url=importfile), 106 strerror = e.strerror, 107 errno = e.errno) 108 QMessageBox.critical(self, app.caption(_("Error")), msg) 109 110 def exportSession(self): 111 """Called when the user clicks Export.""" 112 itemname, jsondict = self.sessions.exportItem() 113 caption = app.caption(_("dialog title", "Export session")) 114 filetypes = '{0} (*.json);;{1} (*)'.format(_("JSON Files"), _("All Files")) 115 mainwindow = self.parent() 116 directory = os.path.dirname(mainwindow.currentDocument().url().toLocalFile()) or app.basedir() 117 filename = os.path.join(directory, itemname + ".json") 118 filename = QFileDialog.getSaveFileName(mainwindow, caption, filename, filetypes)[0] 119 if not filename: 120 return False # cancelled 121 try: 122 with open(filename, 'w') as f: 123 json.dump(jsondict, f, indent=4) 124 except IOError as e: 125 msg = _("{message}\n\n{strerror} ({errno})").format( 126 message = _("Could not write to: {url}").format(url=filename), 127 strerror = e.strerror, 128 errno = e.errno) 129 QMessageBox.critical(self, app.caption(_("Error")), msg) 130 131 def activateSession(self): 132 """Called when the user clicks Activate.""" 133 item = self.sessions.listBox.currentItem() 134 if item: 135 name = item.text() 136 mainwindow = self.parent() 137 man = sessions.manager.get(mainwindow) 138 man.saveCurrentSessionIfDesired() 139 self.accept() 140 man.startSession(name) 141 142 143class SessionList(widgets.listedit.ListEdit): 144 """Manage the list of sessions.""" 145 def load(self): 146 """Loads the list of session names in the list edit.""" 147 names = sessions.sessionNames() 148 current = sessions.currentSession() 149 self.setValue(names) 150 if current in names: 151 self.setCurrentRow(names.index(current)) 152 153 def removeItem(self, item): 154 """Reimplemented to delete the specified session.""" 155 sessions.deleteSession(item.text()) 156 super(SessionList, self).removeItem(item) 157 158 def openEditor(self, item): 159 """Reimplemented to allow editing the specified session.""" 160 name = SessionEditor(self).edit(item.text()) 161 if name: 162 item.setText(name) 163 return True 164 165 def importItem(self, data): 166 """Implement importing a new session from a json data dict.""" 167 name = data['name'] 168 session = sessions.sessionGroup(name) 169 for key in data: 170 if key == 'urls': 171 urls = [] 172 for u in data[key]: 173 urls.append(QUrl(u)) 174 session.setValue("urls", urls) 175 elif key != 'name': 176 session.setValue(key, data[key]) 177 self.load() 178 names = sessions.sessionNames() 179 if name in names: 180 self.setCurrentRow(names.index(name)) 181 182 def exportItem(self): 183 """Implement exporting the currently selected session item to a dict. 184 185 Returns the dict, which can be dumped as a json data dictionary. 186 187 """ 188 jsondict = {} 189 item = self.listBox.currentItem() 190 s = sessions.sessionGroup(item.text()) 191 for key in s.allKeys(): 192 if key == 'urls': 193 urls = [] 194 for u in s.value(key): 195 urls.append(u.toString()) 196 jsondict[key] = urls 197 else: 198 jsondict[key] = s.value(key) 199 return (item.text(), jsondict) 200 201 202class SessionEditor(QDialog): 203 def __init__(self, parent=None): 204 super(SessionEditor, self).__init__(parent) 205 self.setWindowModality(Qt.WindowModal) 206 207 layout = QVBoxLayout() 208 self.setLayout(layout) 209 210 grid = QGridLayout() 211 layout.addLayout(grid) 212 213 self.name = QLineEdit() 214 self.nameLabel = l = QLabel() 215 l.setBuddy(self.name) 216 grid.addWidget(l, 0, 0) 217 grid.addWidget(self.name, 0, 1) 218 219 self.autosave = QCheckBox() 220 grid.addWidget(self.autosave, 1, 1) 221 222 self.basedir = widgets.urlrequester.UrlRequester() 223 self.basedirLabel = l = QLabel() 224 l.setBuddy(self.basedir) 225 grid.addWidget(l, 2, 0) 226 grid.addWidget(self.basedir, 2, 1) 227 228 self.inclPaths = ip = QGroupBox(self, checkable=True, checked=False) 229 ipLayout = QVBoxLayout() 230 ip.setLayout(ipLayout) 231 232 self.replPaths = QCheckBox() 233 ipLayout.addWidget(self.replPaths) 234 self.replPaths.toggled.connect(self.toggleReplace) 235 236 self.include = widgets.listedit.FilePathEdit() 237 self.include.listBox.setDragDropMode(QAbstractItemView.InternalMove) 238 ipLayout.addWidget(self.include) 239 240 grid.addWidget(ip, 3, 1) 241 242 self.revt = QPushButton(self) 243 self.clear = QPushButton(self) 244 self.revt.clicked.connect(self.revertPaths) 245 self.clear.clicked.connect(self.clearPaths) 246 247 self.include.layout().addWidget(self.revt, 5, 1) 248 self.include.layout().addWidget(self.clear, 6, 1) 249 250 layout.addWidget(widgets.Separator()) 251 self.buttons = b = QDialogButtonBox(self) 252 layout.addWidget(b) 253 b.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 254 b.accepted.connect(self.accept) 255 b.rejected.connect(self.reject) 256 userguide.addButton(b, "sessions") 257 app.translateUI(self) 258 259 def translateUI(self): 260 self.nameLabel.setText(_("Name:")) 261 self.autosave.setText(_("Always save the list of documents in this session")) 262 self.basedirLabel.setText(_("Base directory:")) 263 self.inclPaths.setTitle(_("Use session specific include path")) 264 self.replPaths.setText(_("Replace global path")) 265 self.replPaths.setToolTip(_("When checked, paths in LilyPond preferences are not included.")) 266 self.revt.setText(_("Copy global path")) 267 self.revt.setToolTip(_("Add and edit the path from LilyPond preferences.")) 268 self.clear.setText(_("Clear")) 269 self.clear.setToolTip(_("Remove all paths.")) 270 271 def load(self, name): 272 settings = sessions.sessionGroup(name) 273 self.autosave.setChecked(settings.value("autosave", True, bool)) 274 self.basedir.setPath(settings.value("basedir", "", str)) 275 self.include.setValue(qsettings.get_string_list(settings, "include-path")) 276 self.inclPaths.setChecked(settings.value("set-paths", False, bool)) 277 self.replPaths.setChecked(settings.value("repl-paths", False, bool)) 278 if not self.replPaths.isChecked(): 279 self.addDisabledGenPaths() 280 self.revt.setEnabled(False) 281 # more settings here 282 283 def fetchGenPaths(self): 284 """Fetch paths from general preferences.""" 285 return qsettings.get_string_list(QSettings(), 286 "lilypond_settings/include_path") 287 288 def addDisabledGenPaths(self): 289 """Add global paths, but set as disabled.""" 290 genPaths = self.fetchGenPaths() 291 for p in genPaths: 292 i = QListWidgetItem(p, self.include.listBox) 293 i.setFlags(Qt.NoItemFlags) 294 295 def toggleReplace(self): 296 """Called when user changes setting for replace of global paths.""" 297 if self.replPaths.isChecked(): 298 items = self.include.items() 299 for i in items: 300 if not (i.flags() & Qt.ItemIsEnabled): #is not enabled 301 self.include.listBox.takeItem(self.include.listBox.row(i)) 302 self.revt.setEnabled(True) 303 else: 304 self.addDisabledGenPaths() 305 self.revt.setEnabled(False) 306 307 def revertPaths(self): 308 """Add global paths (for edit).""" 309 genPaths = self.fetchGenPaths() 310 for p in genPaths: 311 i = QListWidgetItem(p, self.include.listBox) 312 313 def clearPaths(self): 314 """Remove all active paths.""" 315 items = self.include.items() 316 for i in items: 317 if i.flags() & Qt.ItemIsEnabled: 318 self.include.listBox.takeItem(self.include.listBox.row(i)) 319 320 def save(self, name): 321 settings = sessions.sessionGroup(name) 322 settings.setValue("autosave", self.autosave.isChecked()) 323 settings.setValue("basedir", self.basedir.path()) 324 settings.setValue("set-paths", self.inclPaths.isChecked()) 325 settings.setValue("repl-paths", self.replPaths.isChecked()) 326 path = [i.text() for i in self.include.items() if i.flags() & Qt.ItemIsEnabled] 327 settings.setValue("include-path", path) 328 # more settings here 329 330 def defaults(self): 331 self.autosave.setChecked(True) 332 self.basedir.setPath('') 333 self.inclPaths.setChecked(False) 334 self.replPaths.setChecked(False) 335 self.addDisabledGenPaths() 336 self.revt.setEnabled(False) 337 # more defaults here 338 339 def edit(self, name=None): 340 self._originalName = name 341 if name: 342 caption = _("Edit session: {name}").format(name=name) 343 self.name.setText(name) 344 self.load(name) 345 else: 346 caption = _("Edit new session") 347 self.name.clear() 348 self.name.setFocus() 349 self.defaults() 350 self.setWindowTitle(app.caption(caption)) 351 if self.exec_(): 352 # name changed? 353 name = self.name.text() 354 if self._originalName and name != self._originalName: 355 sessions.renameSession(self._originalName, name) 356 self.save(name) 357 return name 358 359 def done(self, result): 360 if not result or self.validate(): 361 super(SessionEditor, self).done(result) 362 363 def validate(self): 364 """Checks if the input is acceptable. 365 366 If this method returns True, the dialog is accepted when OK is clicked. 367 Otherwise a messagebox could be displayed, and the dialog will remain 368 visible. 369 """ 370 name = self.name.text().strip() 371 self.name.setText(name) 372 if not name: 373 self.name.setFocus() 374 QMessageBox.warning(self, app.caption(_("Warning")), 375 _("Please enter a session name.")) 376 if self._originalName: 377 self.name.setText(self._originalName) 378 return False 379 380 elif name == '-': 381 self.name.setFocus() 382 QMessageBox.warning(self, app.caption(_("Warning")), 383 _("Please do not use the name '{name}'.").format(name="-")) 384 return False 385 386 elif self._originalName != name and name in sessions.sessionNames(): 387 self.name.setFocus() 388 box = QMessageBox(QMessageBox.Warning, app.caption(_("Warning")), 389 _("Another session with the name {name} already exists.\n\n" 390 "Do you want to overwrite it?").format(name=name), 391 QMessageBox.Discard | QMessageBox.Cancel, self) 392 box.button(QMessageBox.Discard).setText(_("Overwrite")) 393 result = box.exec_() 394 if result != QMessageBox.Discard: 395 return False 396 397 return True 398 399