PageRenderTime 38ms CodeModel.GetById 14ms app.highlight 19ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/webconf.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 336 lines | 318 code | 10 blank | 8 comment | 6 complexity | 758a9b615e7c02d7fc241cd2ad95a6b8 MD5 | raw file
  1# webconf.py - Widget to show/edit hgweb config
  2#
  3# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
  4#
  5# This software may be used and distributed according to the terms of the
  6# GNU General Public License version 2, incorporated herein by reference.
  7
  8import os
  9from PyQt4.QtCore import *
 10from PyQt4.QtGui import *
 11from tortoisehg.util import hglib, wconfig
 12from tortoisehg.hgqt import qtlib
 13from tortoisehg.hgqt.i18n import _
 14from tortoisehg.hgqt.webconf_ui import Ui_WebconfForm
 15
 16_FILE_FILTER = _('Config files (*.conf *.config *.ini);;Any files (*)')
 17
 18class WebconfForm(QWidget):
 19    """Widget to show/edit webconf"""
 20    def __init__(self, parent=None, webconf=None):
 21        super(WebconfForm, self).__init__(parent, acceptDrops=True)
 22        self._qui = Ui_WebconfForm()
 23        self._qui.setupUi(self)
 24        self._initicons()
 25        self._qui.path_edit.currentIndexChanged.connect(self._updateview)
 26        self._qui.path_edit.currentIndexChanged.connect(self._updateform)
 27        self._qui.add_button.clicked.connect(self._addpathmap)
 28
 29        self.setwebconf(webconf or wconfig.config())
 30        self._updateform()
 31
 32    def _initicons(self):
 33        def setstdicon(w, name):
 34            w.setIcon(self.style().standardIcon(name))
 35
 36        setstdicon(self._qui.open_button, QStyle.SP_DialogOpenButton)
 37        setstdicon(self._qui.save_button, QStyle.SP_DialogSaveButton)
 38        self._qui.add_button.setIcon(qtlib.geticon('fileadd'))
 39        self._qui.edit_button.setIcon(qtlib.geticon('fallback'))  # TODO
 40        self._qui.remove_button.setIcon(qtlib.geticon('filedelete'))
 41
 42    def dragEnterEvent(self, event):
 43        if self._getlocalpath_from_dropevent(event):
 44            event.setDropAction(Qt.LinkAction)
 45            event.accept()
 46
 47    def dropEvent(self, event):
 48        localpath = self._getlocalpath_from_dropevent(event)
 49        if localpath:
 50            event.setDropAction(Qt.LinkAction)
 51            event.accept()
 52            self._addpathmap(localpath=localpath)
 53
 54    @staticmethod
 55    def _getlocalpath_from_dropevent(event):
 56        m = event.mimeData()
 57        if m.hasFormat('text/uri-list') and len(m.urls()) == 1:
 58            return unicode(m.urls()[0].toLocalFile())
 59
 60    def setwebconf(self, webconf):
 61        """set current webconf object"""
 62        path = hglib.tounicode(getattr(webconf, 'path', None) or '')
 63        i = self._qui.path_edit.findText(path)
 64        if i < 0:
 65            i = 0
 66            self._qui.path_edit.insertItem(i, path, webconf)
 67        self._qui.path_edit.setCurrentIndex(i)
 68
 69    @property
 70    def webconf(self):
 71        """current webconf object"""
 72        def curconf(w):
 73            i = w.currentIndex()
 74            _path, conf = unicode(w.itemText(i)), w.itemData(i).toPyObject()
 75            return conf
 76
 77        return curconf(self._qui.path_edit)
 78
 79    @property
 80    def _webconfmodel(self):
 81        """current model object of webconf"""
 82        return self._qui.repos_view.model()
 83
 84    @pyqtSlot()
 85    def _updateview(self):
 86        m = WebconfModel(config=self.webconf, parent=self)
 87        self._qui.repos_view.setModel(m)
 88        self._qui.repos_view.selectionModel().currentChanged.connect(
 89            self._updateform)
 90
 91    def _updateform(self):
 92        """Update availability of each widget"""
 93        self._qui.repos_view.setEnabled(hasattr(self.webconf, 'write'))
 94        self._qui.add_button.setEnabled(hasattr(self.webconf, 'write'))
 95        self._qui.edit_button.setEnabled(
 96            hasattr(self.webconf, 'write')
 97            and self._qui.repos_view.currentIndex().isValid())
 98        self._qui.remove_button.setEnabled(
 99            hasattr(self.webconf, 'write')
100            and self._qui.repos_view.currentIndex().isValid())
101
102    @pyqtSlot()
103    def on_open_button_clicked(self):
104        path = QFileDialog.getOpenFileName(
105            self, _('Open hgweb config'),
106            getattr(self.webconf, 'path', None) or '', _FILE_FILTER)
107        if path:
108            self.openwebconf(path)
109
110    def openwebconf(self, path):
111        """load the specified webconf file"""
112        path = hglib.fromunicode(path)
113        c = wconfig.readfile(path)
114        c.path = os.path.abspath(path)
115        self.setwebconf(c)
116
117    @pyqtSlot()
118    def on_save_button_clicked(self):
119        path = QFileDialog.getSaveFileName(
120            self, _('Save hgweb config'),
121            getattr(self.webconf, 'path', None) or '', _FILE_FILTER)
122        if path:
123            self.savewebconf(path)
124
125    def savewebconf(self, path):
126        """save current webconf to the specified file"""
127        path = hglib.fromunicode(path)
128        wconfig.writefile(self.webconf, path)
129        self.openwebconf(path)  # reopen in case file path changed
130
131    @pyqtSlot()
132    def _addpathmap(self, path=None, localpath=None):
133        path, localpath = _PathDialog.getaddpathmap(
134            self, path=path, localpath=localpath,
135            invalidpaths=self._webconfmodel.paths)
136        if path:
137            self._webconfmodel.addpathmap(path, localpath)
138
139    @pyqtSlot()
140    def on_edit_button_clicked(self):
141        self.on_repos_view_doubleClicked(self._qui.repos_view.currentIndex())
142
143    @pyqtSlot(QModelIndex)
144    def on_repos_view_doubleClicked(self, index):
145        assert index.isValid()
146        origpath, origlocalpath = self._webconfmodel.getpathmapat(index.row())
147        path, localpath = _PathDialog.geteditpathmap(
148            self, path=origpath, localpath=origlocalpath,
149            invalidpaths=set(self._webconfmodel.paths) - set([origpath]))
150        if not path:
151            return
152        if path != origpath:
153            # we cannot change config key without reordering
154            self._webconfmodel.removepathmap(origpath)
155            self._webconfmodel.addpathmap(path, localpath)
156        else:
157            self._webconfmodel.setpathmap(path, localpath)
158
159    @pyqtSlot()
160    def on_remove_button_clicked(self):
161        index = self._qui.repos_view.currentIndex()
162        assert index.isValid()
163        path, _localpath = self._webconfmodel.getpathmapat(index.row())
164        self._webconfmodel.removepathmap(path)
165
166class _PathDialog(QDialog):
167    """Dialog to add/edit path mapping"""
168    def __init__(self, title, acceptlabel, path=None, localpath=None,
169                 invalidpaths=None, parent=None):
170        super(_PathDialog, self).__init__(parent)
171        self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint)
172                            & ~Qt.WindowContextHelpButtonHint)
173        self.resize(QFontMetrics(self.font()).width('M') * 50, self.height())
174        self.setWindowTitle(title)
175        self._invalidpaths = set(invalidpaths or [])
176        self.setLayout(QFormLayout(fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow))
177        self._initfields()
178        self._initbuttons(acceptlabel)
179        self._path_edit.setText(path or os.path.basename(localpath or ''))
180        self._localpath_edit.setText(localpath or '')
181        self._updateform()
182
183    def _initfields(self):
184        """initialize input fields"""
185        def addfield(key, label, *extras):
186            edit = QLineEdit(self)
187            edit.textChanged.connect(self._updateform)
188            if extras:
189                field = QHBoxLayout()
190                field.addWidget(edit)
191                for e in extras:
192                    field.addWidget(e)
193            else:
194                field = edit
195            self.layout().addRow(label, field)
196            setattr(self, '_%s_edit' % key, edit)
197
198        addfield('path', _('Path:'))
199        self._localpath_browse_button = QToolButton(
200            icon=self.style().standardIcon(QStyle.SP_DialogOpenButton))
201        addfield('localpath', _('Local Path:'), self._localpath_browse_button)
202        self._localpath_browse_button.clicked.connect(self._browse_localpath)
203
204    def _initbuttons(self, acceptlabel):
205        """initialize dialog buttons"""
206        self._buttons = QDialogButtonBox(self)
207        self._accept_button = self._buttons.addButton(QDialogButtonBox.Ok)
208        self._reject_button = self._buttons.addButton(QDialogButtonBox.Cancel)
209        self._accept_button.setText(acceptlabel)
210        self._buttons.accepted.connect(self.accept)
211        self._buttons.rejected.connect(self.reject)
212        self.layout().addRow(self._buttons)
213
214    @property
215    def path(self):
216        """value of path field"""
217        return unicode(self._path_edit.text())
218
219    @property
220    def localpath(self):
221        """value of localpath field"""
222        return unicode(self._localpath_edit.text())
223
224    @pyqtSlot()
225    def _browse_localpath(self):
226        path = QFileDialog.getExistingDirectory(self, _('Select Repository'),
227                                                self.localpath)
228        if not path:
229            return
230
231        self._localpath_edit.setText(path)
232        if not self.path:
233            self._path_edit.setText(os.path.basename(unicode(path)))
234
235    @pyqtSlot()
236    def _updateform(self):
237        """update availability of form elements"""
238        self._accept_button.setEnabled(self._isacceptable())
239
240    def _isacceptable(self):
241        return bool(self.path and self.localpath
242                    and self.path not in self._invalidpaths)
243
244    @classmethod
245    def getaddpathmap(cls, parent, path=None, localpath=None, invalidpaths=None):
246        d = cls(title=_('Add Path to Serve'), acceptlabel=_('Add'),
247                path=path, localpath=localpath,
248                invalidpaths=invalidpaths, parent=parent)
249        if d.exec_():
250            return d.path, d.localpath
251        else:
252            return None, None
253
254    @classmethod
255    def geteditpathmap(cls, parent, path=None, localpath=None, invalidpaths=None):
256        d = cls(title=_('Edit Path to Serve'), acceptlabel=_('Edit'),
257                path=path, localpath=localpath,
258                invalidpaths=invalidpaths, parent=parent)
259        if d.exec_():
260            return d.path, d.localpath
261        else:
262            return None, None
263
264class WebconfModel(QAbstractTableModel):
265    """Wrapper for webconf object to be a Qt's model object"""
266    _COLUMNS = [(_('Path'),),
267                (_('Local Path'),)]
268
269    def __init__(self, config, parent=None):
270        super(WebconfModel, self).__init__(parent)
271        self._config = config
272
273    def data(self, index, role):
274        if not index.isValid():
275            return None
276        if role == Qt.DisplayRole:
277            v = self._config.items('paths')[index.row()][index.column()]
278            return hglib.tounicode(v)
279        return None
280
281    def rowCount(self, parent=QModelIndex()):
282        if parent.isValid():
283            return 0  # no child
284        return len(self._config['paths'])
285
286    def columnCount(self, parent=QModelIndex()):
287        if parent.isValid():
288            return 0  # no child
289        return len(self._COLUMNS)
290
291    def headerData(self, section, orientation, role):
292        if role != Qt.DisplayRole or orientation != Qt.Horizontal:
293            return None
294        return self._COLUMNS[section][0]
295
296    @property
297    def paths(self):
298        """return list of known paths"""
299        return [hglib.tounicode(e) for e in self._config['paths']]
300
301    def getpathmapat(self, row):
302        """return pair of (path, localpath) at the specified index"""
303        assert 0 <= row and row < self.rowCount()
304        return tuple(hglib.tounicode(e) for e in self._config.items('paths')[row])
305
306    def addpathmap(self, path, localpath):
307        """add path mapping to serve"""
308        assert path not in self.paths
309        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
310        try:
311            self._config.set('paths', hglib.fromunicode(path),
312                             hglib.fromunicode(localpath))
313        finally:
314            self.endInsertRows()
315
316    def setpathmap(self, path, localpath):
317        """change path mapping at the specified index"""
318        self._config.set('paths', hglib.fromunicode(path),
319                         hglib.fromunicode(localpath))
320        row = self._indexofpath(path)
321        self.dataChanged.emit(self.index(row, 0),
322                              self.index(row, self.columnCount()))
323
324    def removepathmap(self, path):
325        """remove path from mapping"""
326        row = self._indexofpath(path)
327        self.beginRemoveRows(QModelIndex(), row, row)
328        try:
329            del self._config['paths'][hglib.fromunicode(path)]
330        finally:
331            self.endRemoveRows()
332
333    def _indexofpath(self, path):
334        path = hglib.fromunicode(path)
335        assert path in self._config['paths']
336        return list(self._config['paths']).index(path)