Merge lp:~thisfred/u1db/cosas-conflict-resolution into lp:u1db

Proposed by Eric Casteleijn
Status: Merged
Approved by: Eric Casteleijn
Approved revision: 375
Merged at revision: 373
Proposed branch: lp:~thisfred/u1db/cosas-conflict-resolution
Merge into: lp:u1db
Prerequisite: lp:~thisfred/u1db/its-full-of-conflicts
Diff against target: 451 lines (+253/-62)
3 files modified
cosas/conflicts.ui (+139/-0)
cosas/cosas.ui (+6/-1)
cosas/ui.py (+108/-61)
To merge this branch: bzr merge lp:~thisfred/u1db/cosas-conflict-resolution
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+119193@code.launchpad.net

Commit message

- added UI for conflict resolution

Description of the change

- added UI for conflict resolution

NOTE: right now the only way to resolve a conflicted task is to select it and hit enter. I'd like to have clicking the item do the same thing, but I couldn't figure that out, please advise.

How to test:

PYTHONPATH=. python cosas/ui.py
[ create some tasks if you don't already have any, close the program ]
mv ~/.local/share/cosas/cosas.u1db ~/.local/share/cosas/cosas_copy.u1db
PYTHONPATH=. python cosas/ui.py
[ open sync dialog, sync with ~/.local/share/cosas/cosas_copy.u1db, edit some of the tasks ]
mv ~/.local/share/cosas/cosas.u1db ~/.local/share/cosas/tmp.u1db
mv ~/.local/share/cosas/cosas_copy.u1db ~/.local/share/cosas/cosas.u1db
PYTHONPATH=. python cosas/ui.py
[ edit the same tasks but to have different text and/or done status, open sync dialog, sync with ~/.local/share/cosas/tmp.u1db ]

you should now see conflicts (text is reddish, exclamation point on the right hand side)

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) wrote :

Clicking the item should emit the currentItemChanged signal and/or the itemClicked signal.

I agree that is a better interaction, so holding until we can try to figure that out on monday.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Added the itemClicked handler, and that works beautifully, thanks!

Revision history for this message
Roberto Alsina (ralsina) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cosas/conflicts.ui'
2--- cosas/conflicts.ui 1970-01-01 00:00:00 +0000
3+++ cosas/conflicts.ui 2012-08-13 12:53:19 +0000
4@@ -0,0 +1,139 @@
5+<?xml version="1.0" encoding="UTF-8"?>
6+<ui version="4.0">
7+ <class>conflict_dialog</class>
8+ <widget class="QDialog" name="conflict_dialog">
9+ <property name="windowModality">
10+ <enum>Qt::ApplicationModal</enum>
11+ </property>
12+ <property name="geometry">
13+ <rect>
14+ <x>0</x>
15+ <y>0</y>
16+ <width>329</width>
17+ <height>205</height>
18+ </rect>
19+ </property>
20+ <property name="windowTitle">
21+ <string>conflicts</string>
22+ </property>
23+ <property name="styleSheet">
24+ <string notr="true">QToolButton {
25+ background-color: rgb(203,203,203);
26+ border: 1px solid rgb(102, 102, 102);
27+}
28+
29+QFrame#drop_frame{
30+ border: 0px;
31+ border-top: 2px dotted rgb(102,102,102);
32+ border-bottom: 2px dotted rgb(102,102,102);
33+}
34+
35+QPushButton {
36+ background-color: rgb(61, 133, 198);
37+ color: white;
38+ border: 2px solid rgb(7, 55, 99);
39+ border-radius: 6 6 px;
40+ padding: 5 15 5 15 px;
41+}
42+
43+QComboBox {
44+ background-color: rgb(204, 204, 204);
45+ color: black;
46+ border: 2px solid rgb(102,102,102);
47+ border-radius: 6 6 px;
48+ padding: 5 px;
49+}
50+
51+QFrame#buttons_frame QPushButton {
52+ background-color: rgb(204, 204, 204);
53+ color: black;
54+ border: 2px solid rgb(102,102,102);
55+ border-radius: 0px;
56+}
57+
58+QTreeView::item{
59+ border: 1px dotted rgb(102, 102, 102);
60+ border-top: 0px;
61+ background-color: white;
62+}
63+
64+QLineEdit, QTextEdit {
65+ background-color: white;
66+ border-radius: 0;
67+ border: 1px solid rgb(102,102,102);
68+}
69+
70+* {
71+ background-color: rgb(239, 239, 239);
72+}
73+</string>
74+ </property>
75+ <layout class="QVBoxLayout" name="verticalLayout">
76+ <item>
77+ <widget class="QGroupBox" name="conflicts">
78+ <property name="title">
79+ <string>Conflicting versions:</string>
80+ </property>
81+ <property name="alignment">
82+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
83+ </property>
84+ <layout class="QVBoxLayout" name="verticalLayout_3"/>
85+ </widget>
86+ </item>
87+ <item>
88+ <widget class="QDialogButtonBox" name="buttonBox">
89+ <property name="sizePolicy">
90+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
91+ <horstretch>0</horstretch>
92+ <verstretch>0</verstretch>
93+ </sizepolicy>
94+ </property>
95+ <property name="orientation">
96+ <enum>Qt::Horizontal</enum>
97+ </property>
98+ <property name="standardButtons">
99+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
100+ </property>
101+ <property name="centerButtons">
102+ <bool>false</bool>
103+ </property>
104+ </widget>
105+ </item>
106+ </layout>
107+ </widget>
108+ <resources/>
109+ <connections>
110+ <connection>
111+ <sender>buttonBox</sender>
112+ <signal>accepted()</signal>
113+ <receiver>conflict_dialog</receiver>
114+ <slot>accept()</slot>
115+ <hints>
116+ <hint type="sourcelabel">
117+ <x>248</x>
118+ <y>254</y>
119+ </hint>
120+ <hint type="destinationlabel">
121+ <x>157</x>
122+ <y>274</y>
123+ </hint>
124+ </hints>
125+ </connection>
126+ <connection>
127+ <sender>buttonBox</sender>
128+ <signal>rejected()</signal>
129+ <receiver>conflict_dialog</receiver>
130+ <slot>reject()</slot>
131+ <hints>
132+ <hint type="sourcelabel">
133+ <x>316</x>
134+ <y>260</y>
135+ </hint>
136+ <hint type="destinationlabel">
137+ <x>286</x>
138+ <y>274</y>
139+ </hint>
140+ </hints>
141+ </connection>
142+ </connections>
143+</ui>
144
145=== modified file 'cosas/cosas.ui'
146--- cosas/cosas.ui 2012-08-06 16:58:26 +0000
147+++ cosas/cosas.ui 2012-08-13 12:53:19 +0000
148@@ -174,7 +174,7 @@
149 <bool>true</bool>
150 </property>
151 <property name="columnCount">
152- <number>1</number>
153+ <number>2</number>
154 </property>
155 <property name="topLevelItemCount" stdset="0">
156 <number>0</number>
157@@ -187,6 +187,11 @@
158 <string>title</string>
159 </property>
160 </column>
161+ <column>
162+ <property name="text">
163+ <string notr="true">2</string>
164+ </property>
165+ </column>
166 </widget>
167 </item>
168 </layout>
169
170=== modified file 'cosas/ui.py'
171--- cosas/ui.py 2012-08-06 18:49:20 +0000
172+++ cosas/ui.py 2012-08-13 12:53:19 +0000
173@@ -30,20 +30,14 @@
174 from u1db.remote.http_database import HTTPDatabase
175 from ubuntuone.platform.credentials import CredentialsManagementTool
176
177-DONE_COLOR = QtGui.QColor(183, 183, 183)
178-NOT_DONE_COLOR = QtGui.QColor(0, 0, 0)
179-WHITE = QtGui.QColor(255, 255, 255)
180+FOREGROUND = QtGui.QColor('#1d1f21')
181+DONE = QtGui.QColor('#969896')
182+BACKGROUND = '#FFFFFF'
183+CONFLICT_COLOR = QtGui.QColor('#A54242')
184 TAG_COLORS = [
185- (234, 153, 153),
186- (249, 203, 156),
187- (255, 229, 153),
188- (182, 215, 168),
189- (162, 196, 201),
190- (164, 194, 244),
191- (221, 126, 107),
192- (159, 197, 232),
193- (180, 167, 214),
194- (213, 166, 189)]
195+ '#8C9440', '#de935f', '#5F819D', '#85678F',
196+ '#5E8D87', '#cc6666', '#b5bd68', '#f0c674',
197+ '#81a2be', '#b294bb']
198 U1_URL = 'https://u1db.one.ubuntu.com/~/cosas'
199 TIMEOUT = 1000 * 0.5 * 60 * 60 # 30 minutes
200
201@@ -56,7 +50,7 @@
202 self.task = task
203 # If the task is done, check off the list item.
204 self.store = store
205- self._bg_color = WHITE
206+ self._bg_color = BACKGROUND
207 self._font = font
208 self.main_window = main_window
209
210@@ -64,35 +58,44 @@
211 self._bg_color = color
212
213 def setData(self, column, role, value):
214- if role == QtCore.Qt.CheckStateRole:
215- if value == QtCore.Qt.Checked:
216- self.task.done = True
217- else:
218- self.task.done = False
219- self.store.save_task(self.task)
220- if role == QtCore.Qt.EditRole:
221- text = unicode(value.toString(), 'utf-8')
222- if not text:
223- # There was no text in the edit field so do nothing.
224- return
225- self.update_task_text(text)
226+ if column == 0:
227+ if role == QtCore.Qt.CheckStateRole:
228+ if value == QtCore.Qt.Checked:
229+ self.task.done = True
230+ else:
231+ self.task.done = False
232+ self.store.save_task(self.task)
233+ if role == QtCore.Qt.EditRole:
234+ text = unicode(value.toString(), 'utf-8')
235+ if not text:
236+ # There was no text in the edit field so do nothing.
237+ return
238+ self.update_task_text(text)
239 super(UITask, self).setData(column, role, value)
240
241 def data(self, column, role):
242- if role == QtCore.Qt.EditRole:
243- return self.task.title
244- if role == QtCore.Qt.DisplayRole:
245- return self.task.title
246- if role == QtCore.Qt.FontRole:
247- font = self._font
248- font.setStrikeOut(self.task.done)
249- return font
250 if role == QtCore.Qt.BackgroundRole:
251 return self._bg_color
252 if role == QtCore.Qt.ForegroundRole:
253- return DONE_COLOR if self.task.done else NOT_DONE_COLOR
254- if role == QtCore.Qt.CheckStateRole:
255- return QtCore.Qt.Checked if self.task.done else QtCore.Qt.Unchecked
256+ if self.task.has_conflicts:
257+ return CONFLICT_COLOR
258+ return DONE if self.task.done else FOREGROUND
259+ if column == 0:
260+ if role == QtCore.Qt.FontRole:
261+ font = self._font
262+ font.setStrikeOut(self.task.done)
263+ return font
264+ if role == QtCore.Qt.EditRole:
265+ return self.task.title
266+ if role == QtCore.Qt.DisplayRole:
267+ return self.task.title
268+ if role == QtCore.Qt.CheckStateRole:
269+ return (
270+ QtCore.Qt.Checked if self.task.done else
271+ QtCore.Qt.Unchecked)
272+ elif column == 1:
273+ if role == QtCore.Qt.DisplayRole:
274+ return '!' if self.task.has_conflicts else ''
275 return super(UITask, self).data(column, role)
276
277 def update_task_text(self, text):
278@@ -111,19 +114,39 @@
279 self.store.save_task(self.task)
280
281
282-class TaskDelegate(QtGui.QStyledItemDelegate):
283- """Delegate for rendering tasks."""
284-
285- pen = QtGui.QPen(QtGui.QColor(102, 102, 102), 2, style=QtCore.Qt.DotLine)
286-
287- def paint(self, painter, option, index):
288- # Save current state of painter before we modify anything.
289- painter.save()
290- painter.setPen(self.pen)
291- painter.drawRect(option.rect)
292- # Return painter to original stats.
293- painter.restore()
294- super(TaskDelegate, self).paint(painter, option, index)
295+class Conflicts(QtGui.QDialog):
296+
297+ def __init__(self, other, conflicts):
298+ super(Conflicts, self).__init__()
299+ self.selected_doc = None
300+ uifile = os.path.join(
301+ os.path.abspath(os.path.dirname(__file__)), 'conflicts.ui')
302+ uic.loadUi(uifile, self)
303+ self.other = other
304+ self.revs = []
305+ for conflict in conflicts:
306+
307+ self.revs.append(conflict.rev)
308+
309+ # XXX: this does not deserve any prizes, but it was the quickest
310+ # way I could figure out to use loop variables in a 'closure' and
311+ # not just get the last value everywhere.
312+ def toggled(value, doc=conflict):
313+ if value:
314+ self.selected_doc = doc
315+
316+ radio = QtGui.QRadioButton(conflict.title)
317+ self.conflicts.layout().addWidget(radio)
318+ if conflict.done:
319+ font = radio.font()
320+ font.setStrikeOut(True)
321+ radio.setFont(font)
322+ radio.toggled.connect(toggled)
323+
324+ def accept(self):
325+ if self.selected_doc:
326+ self.other.resolve(self.selected_doc, self.revs)
327+ super(Conflicts, self).accept()
328
329
330 class Sync(QtGui.QDialog):
331@@ -221,7 +244,12 @@
332 # create or update the indexes if they are not up-to-date
333 self.store.initialize_db()
334 # hook up the delegate
335- self.todo_list.setItemDelegate(TaskDelegate())
336+ header = self.todo_list.header()
337+ header.setResizeMode(0, 1) # stretch first column
338+ header.setResizeMode(1, 2) # second column fixed
339+ header.setDefaultSectionSize(20)
340+
341+ header.setStretchLastSection(False)
342 # Initialize some variables we will use to keep track of the tags.
343 self._tag_docs = defaultdict(list)
344 self._tag_buttons = {}
345@@ -249,22 +277,25 @@
346 self.delete()
347 return
348 if event.key() == QtCore.Qt.Key_Return:
349+ current = self.todo_list.currentItem()
350+ if current.task.has_conflicts:
351+ self.open_conflicts_window(current.task.doc_id)
352+ return
353 if not self.editing:
354 self.editing = True
355- self.todo_list.openPersistentEditor(
356- self.todo_list.currentItem())
357+ self.todo_list.openPersistentEditor(current)
358 return
359 else:
360- self.todo_list.closePersistentEditor(
361- self.todo_list.currentItem())
362+ self.todo_list.closePersistentEditor(current)
363 self.editing = False
364+ return
365 super(Main, self).keyPressEvent(event)
366
367 def get_tag_color(self):
368 """Get a color number to use for a new tag."""
369 # Remove a color from the list of available ones and return it.
370 if not self.colors:
371- return WHITE
372+ return BACKGROUND
373 return self.colors.pop(0)
374
375 def connect_events(self):
376@@ -273,11 +304,22 @@
377 self.title_edit.returnPressed.connect(self.update)
378 self.action_synchronize.triggered.connect(self.open_sync_window)
379 self.buttons_toggle.clicked.connect(self.show_buttons)
380+ self.todo_list.itemClicked.connect(self.maybe_open_conflicts)
381+
382+ def maybe_open_conflicts(self, item, column):
383+ if not item.task.has_conflicts:
384+ return
385+ self.open_conflicts_window(item.task.doc_id)
386
387 def open_sync_window(self):
388 window = Sync(self)
389 window.exec_()
390
391+ def open_conflicts_window(self, doc_id):
392+ conflicts = self.store.db.get_doc_conflicts(doc_id)
393+ window = Conflicts(self, conflicts)
394+ window.exec_()
395+
396 def show_buttons(self):
397 """Show the frame with the tag buttons."""
398 self.buttons_toggle.clicked.disconnect(self.show_buttons)
399@@ -334,7 +376,8 @@
400 # Wrap the task in a UITask object.
401 item = UITask(
402 task, self.todo_list, self.store, self.todo_list.font(), self)
403- item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
404+ if not task.has_conflicts:
405+ item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
406 self.todo_list.addTopLevelItem(item)
407 if not task.tags:
408 return
409@@ -345,7 +388,7 @@
410 if task.tags:
411 item.set_color(self._tag_colors[task.tags[0]]['qcolor'])
412 else:
413- item.set_color(WHITE)
414+ item.set_color(BACKGROUND)
415
416 def add_tag(self, doc_id, tag):
417 """Create a link between the task with id doc_id and the tag, and
418@@ -361,11 +404,11 @@
419 # Add a tag filter button for this tag to the UI.
420 button = QtGui.QPushButton(tag)
421 color = self.get_tag_color()
422- qcolor = QtGui.QColor(*color)
423+ qcolor = QtGui.QColor(color)
424 self._tag_colors[tag] = {
425 'color_tuple': color,
426 'qcolor': qcolor}
427- button.setStyleSheet('background-color: rgb(%d, %d, %d)' % color)
428+ button.setStyleSheet('background-color: %s' % color)
429 button._todo_tag = tag
430 # Make the button an on/off button.
431 button.setCheckable(True)
432@@ -426,7 +469,7 @@
433 if new_tags:
434 item.set_color(self._tag_colors[list(new_tags)[0]]['qcolor'])
435 return
436- item.set_color(WHITE)
437+ item.set_color(BACKGROUND)
438
439 def get_ubuntuone_credentials(self):
440 cmt = CredentialsManagementTool()
441@@ -483,6 +526,10 @@
442 self.refresh_filter()
443 self.update_status_bar("last synced: %s" % (datetime.now(),))
444
445+ def resolve(self, doc, revs):
446+ self.store.db.resolve_doc(doc, revs)
447+ self.refresh_filter()
448+
449
450 if __name__ == "__main__":
451 # TODO: Unfortunately, to be able to use ubuntuone.platform.credentials on

Subscribers

People subscribed via source and target branches