views.main: automatically expand top-level items
[git-cola.git] / cola / qobserver.py
blob729e87e7274008c4753fc18c796a5833707e3f55
1 # Copyright (c) 2008 David Aguilar
2 """This module provides the QObserver class which allows for simple
3 correspondancies between model parameters and Qt widgets.
5 The QObserver class handles receiving notifications from
6 model classes and updating Qt widgets accordingly.
8 Qt signals are also relayed back to the model so that changes
9 are always available in the model without having to worry about the
10 different ways to query Qt widgets.
12 """
13 import types
15 from PyQt4 import QtCore
16 from PyQt4 import QtGui
18 from cola import observer
20 class QObserver(observer.Observer, QtCore.QObject):
22 def __init__(self, model, view, *args, **kwargs):
23 """Binds a model and Qt view"""
24 observer.Observer.__init__(self, model)
25 QtCore.QObject.__init__(self)
27 self.view = view
29 self._actions = {}
30 self._callbacks = {}
31 self._widget_names = {}
32 self._model_to_view = {}
33 self._mapped_params = set()
34 self._connected = set()
35 self._in_textfield = False
36 self._in_callback = False
38 def SLOT(self, *args):
39 """Default slot to handle all Qt callbacks.
40 This method delegates to callbacks from add_signals."""
42 self._in_textfield = False
44 widget = self.sender()
45 param = self._widget_names[widget]
47 if param in self._mapped_params:
48 model = self.model
49 if isinstance(widget, QtGui.QTextEdit):
50 self._in_textfield = True
51 value = unicode(widget.toPlainText())
52 model.set_param(param, value, notify=False)
53 elif isinstance(widget, QtGui.QLineEdit):
54 self._in_textfield = True
55 value = unicode(widget.text())
56 model.set_param(param, value)
57 elif isinstance(widget, QtGui.QCheckBox):
58 model.set_param(param, widget.isChecked())
59 elif isinstance(widget, QtGui.QSpinBox):
60 model.set_param(param, widget.value())
61 elif isinstance(widget, QtGui.QFontComboBox):
62 value = unicode(widget.currentFont().toString())
63 if model.has_param(param+'_size'):
64 size = model.param(param+'_size')
65 props = value.split(',')
66 props[1] = str(size)
67 value = ','.join(props)
68 model.set_param(param, value)
69 elif isinstance(widget, QtGui.QDateEdit):
70 fmt = QtCore.Qt.ISODate
71 value = str(widget.date().toString(fmt))
72 model.set_param(param, value)
73 elif isinstance(widget, QtGui.QListWidget):
74 row = widget.currentRow()
75 item = widget.item(row)
76 if item:
77 selected = item.isSelected()
78 else:
79 selected = False
80 model.set_param(param+'_selected', selected)
81 model.set_param(param+'_index', row)
82 if selected and row != -1:
83 model.set_param(param+'_item',
84 model.param(param)[row])
85 else:
86 model.set_param(param+'_item', '')
87 elif isinstance(widget, QtGui.QTreeWidget):
88 item = widget.currentItem()
89 if item:
90 selected = item.isSelected()
91 row = widget.indexOfTopLevelItem(item)
92 else:
93 selected = False
94 row = -1
95 model.set_param(param+'_selected', selected)
96 model.set_param(param+'_index', row)
97 items = model.param(param)
98 if selected and row != -1 and row < len(items):
99 model.set_param(param+'_item', items[row])
100 else:
101 model.set_param(param+'_item', '')
102 elif isinstance(widget, QtGui.QComboBox):
103 idx = widget.currentIndex()
104 model.set_param(param+'_index', idx)
105 if idx != -1 and model.param(param):
106 model.set_param(param+'_item',
107 model.param(param)[idx])
108 else:
109 model.set_param(param+'_item', '')
111 else:
112 print("SLOT(): Unknown widget:", param, widget)
114 self._in_callback = True
115 if param in self._callbacks:
116 self._callbacks[param](*args)
117 self._in_callback = False
118 self._in_textfield = False
120 def connect(self, obj, signal_str, *args):
121 """Convenience function so that subclasses do not have
122 to import QtCore.SIGNAL."""
123 signal = signal_str
124 if type(signal) is str:
125 signal = QtCore.SIGNAL(signal)
126 return QtCore.QObject.connect(obj, signal, *args)
128 def add_signals(self, signal_str, *objects):
129 """Connects object's signal to the QObserver."""
130 for obj in objects:
131 self.connect(obj, signal_str, self.SLOT)
133 def add_callbacks(self, **callbacks):
134 """Registers callbacks that are called in response to GUI events."""
135 for sender, callback in callbacks.iteritems():
136 self._callbacks[sender] = callback
137 widget = getattr(self.view, sender)
138 self._widget_names[widget] = sender
139 self._autoconnect(widget)
141 def add_observables(self, *params):
142 """This method assumes that widgets and model params
143 share the same name."""
144 for param in params:
145 widget = getattr(self.view, param)
146 self._model_to_view[param] = widget
147 self._widget_names[widget] = param
148 self._mapped_params.add(param)
149 self._autoconnect(widget)
151 def _autoconnect(self, widget):
152 """Automagically connects Qt widgets to QObserver.SLOT"""
154 if widget in self._connected:
155 return
156 self._connected.add(widget)
158 if isinstance(widget, QtGui.QTextEdit):
159 self.add_signals('textChanged()', widget)
160 elif isinstance(widget, QtGui.QLineEdit):
161 self.add_signals('textChanged(const QString&)', widget)
162 elif isinstance(widget, QtGui.QListWidget):
163 self.add_signals('itemSelectionChanged()', widget)
164 self.add_signals('itemClicked(QListWidgetItem *)', widget)
165 doubleclick = str(widget.objectName())+'_doubleclick'
166 if hasattr(self, doubleclick):
167 self.connect(widget, 'itemDoubleClicked(QListWidgetItem *)',
168 getattr(self, doubleclick))
169 elif isinstance(widget, QtGui.QTreeWidget):
170 self.add_signals('itemSelectionChanged()', widget)
171 self.add_signals('itemClicked(QTreeWidgetItem *, int)', widget)
172 doubleclick = str(widget.objectName())+'_doubleclick'
173 if hasattr(self, doubleclick):
174 self.connect(widget, 'itemDoubleClicked(QTreeWidgetItem *, int)',
175 getattr(self, doubleclick))
177 elif isinstance(widget, QtGui.QAbstractButton):
178 self.add_signals('released()', widget)
179 elif isinstance(widget, QtGui.QAction):
180 self.add_signals('triggered()', widget)
181 elif isinstance(widget, QtGui.QCheckBox):
182 self.add_signals('stateChanged(int)', widget)
183 elif isinstance(widget, QtGui.QSpinBox):
184 self.add_signals('valueChanged(int)', widget)
185 elif isinstance(widget, QtGui.QFontComboBox):
186 self.add_signals('currentFontChanged(const QFont&)', widget)
187 elif isinstance(widget, QtGui.QSplitter):
188 self.add_signals('splitterMoved(int,int)', widget)
189 elif isinstance(widget, QtGui.QDateEdit):
190 self.add_signals('dateChanged(const QDate&)', widget)
191 elif isinstance(widget, QtGui.QComboBox):
192 self.add_signals('currentIndexChanged(int)', widget)
193 else:
194 raise Exception('Asked to connect unknown widget:\n\t%s => %s'
195 % (type(widget), str(widget.objectName())))
197 def add_actions(self, **kwargs):
199 Register view actions.
201 Action are called in response to view changes.(view->model).
204 for param, callback in kwargs.iteritems():
205 if type(callback) is list:
206 self._actions[param] = callback
207 else:
208 self._actions[param] = [callback]
210 def subject_changed(self, param, value):
211 """Sends a model param to the view(model->view)"""
213 if self._in_textfield and not self._in_callback:
214 # A slot has changed the model and we're not in
215 # a user callback. In this case the event is causing
216 # a feedback loop so skip redundant work and return.
217 return
219 if param in self._model_to_view:
220 # Temporarily disable notification to avoid loop-backs
221 notify = self.model.notification_enabled
222 self.model.notification_enabled = False
224 widget = self._model_to_view[param]
225 if isinstance(widget, QtGui.QSpinBox):
226 widget.setValue(value)
227 elif isinstance(widget, QtGui.QPixmap):
228 widget.load(value)
229 elif isinstance(widget, QtGui.QTextEdit):
230 widget.setText(value)
231 elif isinstance(widget, QtGui.QLineEdit):
232 widget.setText(value)
233 elif isinstance(widget, QtGui.QListWidget):
234 self.model.set_param(param+'_item', '')
235 widget.clear()
236 for i in value:
237 widget.addItem(i)
238 if self.model.has_param(param+'_index'):
239 idx = self.model.param(param+'_index')
240 if idx != -1 and idx < len(value):
241 item = widget.item(idx)
242 widget.setCurrentItem(item)
243 widget.setItemSelected(item, True)
244 self.model.set_param(param+'_item', value[idx])
245 if param in self._callbacks:
246 self.model.notification_enabled = True
247 self._callbacks[param]()
248 self.model.notification_enabled = False
249 else:
250 self.model.set_param(param+'_item', '')
251 elif isinstance(widget, QtGui.QTreeWidget):
252 self.model.set_param(param+'_item', '')
253 widget.clear()
254 for i in value:
255 item = QtGui.QTreeWidgetItem([i])
256 count = widget.topLevelItemCount()
257 item.setData(0, QtCore.Qt.UserRole,
258 QtCore.QVariant(count))
259 widget.addTopLevelItem(item)
260 if self.model.has_param(param+'_index'):
261 idx = self.model.param(param+'_index')
262 if idx != -1 and idx < len(value):
263 item = widget.topLevelItem(idx)
264 widget.setCurrentItem(item)
265 widget.setItemSelected(item, True)
266 val = value[idx]
267 self.model.set_param(param+'_item', val)
268 if param in self._callbacks:
269 self.model.notification_enabled = True
270 self._callbacks[param]()
271 self.model.notification_enabled = False
272 else:
273 self.model.set_param(param+'_item', '')
275 elif isinstance(widget, QtGui.QCheckBox):
276 widget.setChecked(value)
277 elif isinstance(widget, QtGui.QFontComboBox):
278 font = QtGui.QFont()
279 font.fromString(value)
280 widget.setCurrentFont(font)
281 elif isinstance(widget, QtGui.QDateEdit):
282 if not value:
283 return
284 fmt = QtCore.Qt.ISODate
285 date = QtCore.QDate.fromString(value, fmt)
286 if date:
287 widget.setDate(date)
288 elif isinstance(widget, QtGui.QComboBox):
289 self.model.set_param(param+'_item', '')
290 widget.clear()
291 for item in value:
292 widget.addItem(item)
293 if self.model.has_param(param+'_index'):
294 idx = self.model.param(param+'_index')
295 if idx != -1 and idx < len(value):
296 widget.setCurrentIndex(idx)
297 self.model.set_param(param+'_item', value[idx])
298 else:
299 print('subject_changed(): Unknown widget:',
300 str(widget.objectName()), widget, value)
301 self.model.notification_enabled = notify
303 if param not in self._actions:
304 return
305 widgets = []
306 if param in self._model_to_view:
307 widget = self._model_to_view[param]
308 widgets.append(widget)
309 # Call the model callback w/ the view's widgets as the args
310 for action in self._actions[param]:
311 action(*widgets)
313 def refresh_view(self, *params):
314 """Sends a notification message for each known model parameter."""
315 if not params:
316 params= tuple(self._model_to_view.keys() +
317 self._actions.keys())
318 notified = []
319 for param in params:
320 if param not in notified:
321 notified.append(param)
322 self.model.notify_observers(*notified)