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.
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
)
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
:
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(',')
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
)
77 selected
= item
.isSelected()
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
])
86 model
.set_param(param
+'_item', '')
87 elif isinstance(widget
, QtGui
.QTreeWidget
):
88 item
= widget
.currentItem()
90 selected
= item
.isSelected()
91 row
= widget
.indexOfTopLevelItem(item
)
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
])
101 model
.set_param(param
+'_item', '')
102 elif isinstance(widget
, QtGui
.QComboBox
):
103 idx
= widget
.currentIndex()
104 txt
= unicode(widget
.currentText())
105 model
.set_param(param
+'_index', idx
)
106 model
.set_param(param
+'_item', txt
)
108 print("SLOT(): Unknown widget:", param
, widget
)
110 self
._in
_callback
= True
111 if param
in self
._callbacks
:
112 self
._callbacks
[param
](*args
)
113 self
._in
_callback
= False
114 self
._in
_textfield
= False
116 def connect(self
, obj
, signal_str
, *args
):
117 """Convenience function so that subclasses do not have
118 to import QtCore.SIGNAL."""
120 if type(signal
) is str:
121 signal
= QtCore
.SIGNAL(signal
)
122 return QtCore
.QObject
.connect(obj
, signal
, *args
)
124 def add_signals(self
, signal_str
, *objects
):
125 """Connects object's signal to the QObserver."""
127 self
.connect(obj
, signal_str
, self
.SLOT
)
129 def add_callbacks(self
, **callbacks
):
130 """Registers callbacks that are called in response to GUI events."""
131 for sender
, callback
in callbacks
.iteritems():
132 self
._callbacks
[sender
] = callback
133 widget
= getattr(self
.view
, sender
)
134 self
._widget
_names
[widget
] = sender
135 self
._autoconnect
(widget
)
137 def add_observables(self
, *params
):
138 """This method assumes that widgets and model params
139 share the same name."""
141 widget
= getattr(self
.view
, param
)
142 self
._model
_to
_view
[param
] = widget
143 self
._widget
_names
[widget
] = param
144 self
._mapped
_params
.add(param
)
145 self
._autoconnect
(widget
)
147 def _autoconnect(self
, widget
):
148 """Automagically connects Qt widgets to QObserver.SLOT"""
150 if widget
in self
._connected
:
152 self
._connected
.add(widget
)
154 if isinstance(widget
, QtGui
.QTextEdit
):
155 self
.add_signals('textChanged()', widget
)
156 elif isinstance(widget
, QtGui
.QLineEdit
):
157 self
.add_signals('textChanged(const QString&)', widget
)
158 elif isinstance(widget
, QtGui
.QListWidget
):
159 self
.add_signals('itemSelectionChanged()', widget
)
160 self
.add_signals('itemClicked(QListWidgetItem *)', widget
)
161 doubleclick
= str(widget
.objectName())+'_doubleclick'
162 if hasattr(self
, doubleclick
):
163 self
.connect(widget
, 'itemDoubleClicked(QListWidgetItem *)',
164 getattr(self
, doubleclick
))
165 elif isinstance(widget
, QtGui
.QTreeWidget
):
166 self
.add_signals('itemSelectionChanged()', widget
)
167 self
.add_signals('itemClicked(QTreeWidgetItem *, int)', widget
)
168 doubleclick
= str(widget
.objectName())+'_doubleclick'
169 if hasattr(self
, doubleclick
):
170 self
.connect(widget
, 'itemDoubleClicked(QTreeWidgetItem *, int)',
171 getattr(self
, doubleclick
))
173 elif isinstance(widget
, QtGui
.QAbstractButton
):
174 self
.add_signals('released()', widget
)
175 elif isinstance(widget
, QtGui
.QAction
):
176 self
.add_signals('triggered()', widget
)
177 elif isinstance(widget
, QtGui
.QCheckBox
):
178 self
.add_signals('stateChanged(int)', widget
)
179 elif isinstance(widget
, QtGui
.QSpinBox
):
180 self
.add_signals('valueChanged(int)', widget
)
181 elif isinstance(widget
, QtGui
.QFontComboBox
):
182 self
.add_signals('currentFontChanged(const QFont&)', widget
)
183 elif isinstance(widget
, QtGui
.QSplitter
):
184 self
.add_signals('splitterMoved(int,int)', widget
)
185 elif isinstance(widget
, QtGui
.QDateEdit
):
186 self
.add_signals('dateChanged(const QDate&)', widget
)
187 elif isinstance(widget
, QtGui
.QComboBox
):
188 self
.add_signals('currentIndexChanged(int)', widget
)
189 self
.add_signals('editTextChanged(QString)', widget
)
191 raise Exception('Asked to connect unknown widget:\n\t%s => %s'
192 % (type(widget
), str(widget
.objectName())))
194 def add_actions(self
, **kwargs
):
196 Register view actions.
198 Action are called in response to view changes.(view->model).
201 for param
, callback
in kwargs
.iteritems():
202 if type(callback
) is list:
203 self
._actions
[param
] = callback
205 self
._actions
[param
] = [callback
]
207 def subject_changed(self
, param
, value
):
208 """Sends a model param to the view(model->view)"""
210 if self
._in
_textfield
and not self
._in
_callback
:
211 # A slot has changed the model and we're not in
212 # a user callback. In this case the event is causing
213 # a feedback loop so skip redundant work and return.
216 if param
in self
._model
_to
_view
:
217 # Temporarily disable notification to avoid loop-backs
218 notify
= self
.model
.notification_enabled
219 self
.model
.notification_enabled
= False
221 widget
= self
._model
_to
_view
[param
]
222 if isinstance(widget
, QtGui
.QSpinBox
):
223 widget
.setValue(value
)
224 elif isinstance(widget
, QtGui
.QPixmap
):
226 elif isinstance(widget
, QtGui
.QTextEdit
):
227 widget
.setText(value
)
228 elif isinstance(widget
, QtGui
.QLineEdit
):
229 widget
.setText(value
)
230 elif isinstance(widget
, QtGui
.QListWidget
):
231 self
.model
.set_param(param
+'_item', '')
235 if self
.model
.has_param(param
+'_index'):
236 idx
= self
.model
.param(param
+'_index')
237 if idx
!= -1 and idx
< len(value
):
238 item
= widget
.item(idx
)
239 widget
.setCurrentItem(item
)
240 widget
.setItemSelected(item
, True)
241 self
.model
.set_param(param
+'_item', value
[idx
])
242 if param
in self
._callbacks
:
243 self
.model
.notification_enabled
= True
244 self
._callbacks
[param
]()
245 self
.model
.notification_enabled
= False
247 self
.model
.set_param(param
+'_item', '')
248 elif isinstance(widget
, QtGui
.QTreeWidget
):
249 self
.model
.set_param(param
+'_item', '')
252 item
= QtGui
.QTreeWidgetItem([i
])
253 count
= widget
.topLevelItemCount()
254 item
.setData(0, QtCore
.Qt
.UserRole
,
255 QtCore
.QVariant(count
))
256 widget
.addTopLevelItem(item
)
257 if self
.model
.has_param(param
+'_index'):
258 idx
= self
.model
.param(param
+'_index')
259 if idx
!= -1 and idx
< len(value
):
260 item
= widget
.topLevelItem(idx
)
261 widget
.setCurrentItem(item
)
262 widget
.setItemSelected(item
, True)
264 self
.model
.set_param(param
+'_item', val
)
265 if param
in self
._callbacks
:
266 self
.model
.notification_enabled
= True
267 self
._callbacks
[param
]()
268 self
.model
.notification_enabled
= False
270 self
.model
.set_param(param
+'_item', '')
272 elif isinstance(widget
, QtGui
.QCheckBox
):
273 widget
.setChecked(value
)
274 elif isinstance(widget
, QtGui
.QFontComboBox
):
276 font
.fromString(value
)
277 widget
.setCurrentFont(font
)
278 elif isinstance(widget
, QtGui
.QDateEdit
):
281 fmt
= QtCore
.Qt
.ISODate
282 date
= QtCore
.QDate
.fromString(value
, fmt
)
285 elif isinstance(widget
, QtGui
.QComboBox
):
286 self
.model
.set_param(param
+'_item', '')
290 if self
.model
.has_param(param
+'_index'):
291 idx
= self
.model
.param(param
+'_index')
292 if idx
!= -1 and idx
< len(value
):
293 widget
.setCurrentIndex(idx
)
294 self
.model
.set_param(param
+'_item', value
[idx
])
296 print('subject_changed(): Unknown widget:',
297 str(widget
.objectName()), widget
, value
)
298 self
.model
.notification_enabled
= notify
300 if param
not in self
._actions
:
303 if param
in self
._model
_to
_view
:
304 widget
= self
._model
_to
_view
[param
]
305 widgets
.append(widget
)
306 # Call the model callback w/ the view's widgets as the args
307 for action
in self
._actions
[param
]:
310 def refresh_view(self
, *params
):
311 """Sends a notification message for each known model parameter."""
313 params
= tuple(self
._model
_to
_view
.keys() +
314 self
._actions
.keys())
317 if param
not in notified
:
318 notified
.append(param
)
319 self
.model
.notify_observers(*notified
)