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 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
])
109 model
.set_param(param
+'_item', '')
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."""
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."""
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."""
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
:
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
)
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
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.
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
):
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', '')
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
250 self
.model
.set_param(param
+'_item', '')
251 elif isinstance(widget
, QtGui
.QTreeWidget
):
252 self
.model
.set_param(param
+'_item', '')
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)
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
273 self
.model
.set_param(param
+'_item', '')
275 elif isinstance(widget
, QtGui
.QCheckBox
):
276 widget
.setChecked(value
)
277 elif isinstance(widget
, QtGui
.QFontComboBox
):
279 font
.fromString(value
)
280 widget
.setCurrentFont(font
)
281 elif isinstance(widget
, QtGui
.QDateEdit
):
284 fmt
= QtCore
.Qt
.ISODate
285 date
= QtCore
.QDate
.fromString(value
, fmt
)
288 elif isinstance(widget
, QtGui
.QComboBox
):
289 self
.model
.set_param(param
+'_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
])
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
:
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
]:
313 def refresh_view(self
, *params
):
314 """Sends a notification message for each known model parameter."""
316 params
= tuple(self
._model
_to
_view
.keys() +
317 self
._actions
.keys())
320 if param
not in notified
:
321 notified
.append(param
)
322 self
.model
.notify_observers(*notified
)