개정판 5a26030c
add project info setting and fix text move
Change-Id: Ia692a27f1f8b2d2e981881aaf1a5ba3cbbe992f4
DTI_PID/DTI_PID/ConfigurationDialog.py | ||
---|---|---|
9 | 9 |
from PyQt5.QtGui import * |
10 | 10 |
from PyQt5.QtWidgets import * |
11 | 11 |
import sqlite3 |
12 |
from AppDocData import AppDocData |
|
13 |
from AppDocData import Config |
|
14 |
from AppDocData import Color |
|
12 |
from AppDocData import AppDocData, Config, Color |
|
15 | 13 |
import Configuration_UI |
16 | 14 |
import pytesseract |
17 | 15 |
import tesseract_ocr_module as TOCR |
DTI_PID/DTI_PID/ProjectInfoDialog.py | ||
---|---|---|
6 | 6 |
from PyQt5.QtCore import * |
7 | 7 |
from PyQt5.QtGui import * |
8 | 8 |
from PyQt5.QtWidgets import * |
9 |
from AppDocData import AppDocData, MessageType |
|
9 |
from AppDocData import AppDocData, MessageType, Config
|
|
10 | 10 |
|
11 | 11 |
import ProjectInfo_UI |
12 | 12 |
|
... | ... | |
22 | 22 |
self.ui.buttonBox.button(QDialogButtonBox.Ok).setIcon(QIcon(':/newPrefix/OK.svg')) |
23 | 23 |
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setIcon(QIcon(':/newPrefix/Remove.svg')) |
24 | 24 |
|
25 |
self.load_data() |
|
26 |
|
|
25 | 27 |
self.ui.tabWidget.setTabVisible(1, False) |
26 | 28 |
|
27 |
def run(self):
|
|
29 |
def load_data(self):
|
|
28 | 30 |
app_doc_data = AppDocData.instance() |
29 | 31 |
|
30 |
name = self.ui.lineEditDuplication.text() |
|
31 |
if self.ui.radioButtonDupilcation.isChecked() and name: |
|
32 |
attrs = app_doc_data.getDuplicatedAttrs(name) |
|
32 |
configs = app_doc_data.getConfigs('Project Info', 'Name') |
|
33 |
self.ui.lineEditProjectName.setText(configs[0].value if 1 == len(configs) else '') |
|
34 |
configs = app_doc_data.getConfigs('Project Info', 'Desc') |
|
35 |
self.ui.lineEditProjectDesc.setText(configs[0].value if 1 == len(configs) else '') |
|
36 |
configs = app_doc_data.getConfigs('Project Info', 'Type') |
|
37 |
self.ui.lineEditProjectType.setText(configs[0].value if 1 == len(configs) else '') |
|
38 |
configs = app_doc_data.getConfigs('Project Info', 'Process Desc') |
|
39 |
self.ui.lineEditProjectMainProcessDesc.setText(configs[0].value if 1 == len(configs) else '') |
|
40 |
configs = app_doc_data.getConfigs('Project Info', 'Duration') |
|
41 |
self.ui.lineEditProjectDuration.setText(configs[0].value if 1 == len(configs) else '') |
|
42 |
configs = app_doc_data.getConfigs('Project Info', 'Owner') |
|
43 |
self.ui.lineEditProjectOwner.setText(configs[0].value if 1 == len(configs) else '') |
|
44 |
configs = app_doc_data.getConfigs('Project Info', 'Contractor') |
|
45 |
self.ui.lineEditProjectContractor.setText(configs[0].value if 1 == len(configs) else '') |
|
46 |
configs = app_doc_data.getConfigs('Project Info', 'Licensor') |
|
47 |
self.ui.lineEditProjectLicensor.setText(configs[0].value if 1 == len(configs) else '') |
|
48 |
configs = app_doc_data.getConfigs('Project Info', 'PnID') |
|
49 |
self.ui.spinBoxNoPnID.setValue(int(configs[0].value) if 1 == len(configs) else 0) |
|
50 |
configs = app_doc_data.getConfigs('Project Info', 'Equipment') |
|
51 |
self.ui.spinBoxNoEquipment.setValue(int(configs[0].value) if 1 == len(configs) else 0) |
|
52 |
configs = app_doc_data.getConfigs('Project Info', 'Instrument') |
|
53 |
self.ui.spinBoxNoInstrument.setValue(int(configs[0].value) if 1 == len(configs) else 0) |
|
54 |
configs = app_doc_data.getConfigs('Project Info', 'Line') |
|
55 |
self.ui.spinBoxNoLine.setValue(int(configs[0].value) if 1 == len(configs) else 0) |
|
56 |
|
|
57 |
def accept(self): |
|
58 |
app_doc_data = AppDocData.instance() |
|
59 |
|
|
60 |
configs = [] |
|
61 |
|
|
62 |
configs.append(Config('Project Info', 'Name', self.ui.lineEditProjectName.text())) |
|
63 |
configs.append(Config('Project Info', 'Desc', self.ui.lineEditProjectDesc.text())) |
|
64 |
configs.append(Config('Project Info', 'Type', self.ui.lineEditProjectType.text())) |
|
65 |
configs.append(Config('Project Info', 'Process Desc', self.ui.lineEditProjectMainProcessDesc.text())) |
|
66 |
configs.append(Config('Project Info', 'Duration', self.ui.lineEditProjectDuration.text())) |
|
67 |
configs.append(Config('Project Info', 'Owner', self.ui.lineEditProjectOwner.text())) |
|
68 |
configs.append(Config('Project Info', 'Contractor', self.ui.lineEditProjectContractor.text())) |
|
69 |
configs.append(Config('Project Info', 'Licensor', self.ui.lineEditProjectLicensor.text())) |
|
70 |
configs.append(Config('Project Info', 'PnID', self.ui.spinBoxNoPnID.value())) |
|
71 |
configs.append(Config('Project Info', 'Equipment', self.ui.spinBoxNoEquipment.value())) |
|
72 |
configs.append(Config('Project Info', 'Instrument', self.ui.spinBoxNoInstrument.value())) |
|
73 |
configs.append(Config('Project Info', 'Line', self.ui.spinBoxNoLine.value())) |
|
33 | 74 |
|
34 |
self.ui.tableWidgetResult.setRowCount(len(attrs)) |
|
35 |
for index in range(len(attrs)): |
|
36 |
self.ui.tableWidgetResult.setItem(index, 0, QTableWidgetItem(attrs[index][0])) |
|
37 |
self.ui.tableWidgetResult.setItem(index, 1, QTableWidgetItem(attrs[index][1])) |
|
38 |
self.ui.tableWidgetResult.setItem(index, 2, QTableWidgetItem(attrs[index][2])) |
|
39 |
|
|
75 |
app_doc_data.saveConfigs(configs) |
|
40 | 76 |
|
77 |
QDialog.accept(self) |
|
78 |
|
|
41 | 79 |
|
42 | 80 |
|
DTI_PID/DTI_PID/ProjectInfo_UI.py | ||
---|---|---|
47 | 47 |
self.lineEditProjectType = QtWidgets.QLineEdit(self.groupBoxText) |
48 | 48 |
self.lineEditProjectType.setObjectName("lineEditProjectType") |
49 | 49 |
self.gridLayout_25.addWidget(self.lineEditProjectType, 2, 1, 1, 2) |
50 |
self.spinBoxNoPnID = QtWidgets.QSpinBox(self.groupBoxText) |
|
51 |
self.spinBoxNoPnID.setMinimumSize(QtCore.QSize(100, 0)) |
|
52 |
self.spinBoxNoPnID.setObjectName("spinBoxNoPnID") |
|
53 |
self.gridLayout_25.addWidget(self.spinBoxNoPnID, 8, 1, 1, 1) |
|
54 | 50 |
self.label_22 = QtWidgets.QLabel(self.groupBoxText) |
55 | 51 |
self.label_22.setObjectName("label_22") |
56 | 52 |
self.gridLayout_25.addWidget(self.label_22, 8, 0, 1, 1) |
... | ... | |
78 | 74 |
self.lineEditProjectDesc.setFont(font) |
79 | 75 |
self.lineEditProjectDesc.setObjectName("lineEditProjectDesc") |
80 | 76 |
self.gridLayout_25.addWidget(self.lineEditProjectDesc, 1, 1, 1, 2) |
81 |
self.spinBoxNoEquipment = QtWidgets.QSpinBox(self.groupBoxText) |
|
82 |
self.spinBoxNoEquipment.setMaximum(99999) |
|
83 |
self.spinBoxNoEquipment.setObjectName("spinBoxNoEquipment") |
|
84 |
self.gridLayout_25.addWidget(self.spinBoxNoEquipment, 9, 1, 1, 1) |
|
85 | 77 |
self.label_7 = QtWidgets.QLabel(self.groupBoxText) |
86 | 78 |
self.label_7.setObjectName("label_7") |
87 | 79 |
self.gridLayout_25.addWidget(self.label_7, 1, 0, 1, 1) |
... | ... | |
112 | 104 |
self.lineEditProjectDuration.setFont(font) |
113 | 105 |
self.lineEditProjectDuration.setObjectName("lineEditProjectDuration") |
114 | 106 |
self.gridLayout_25.addWidget(self.lineEditProjectDuration, 4, 1, 1, 2) |
115 |
self.lineEditProjectLicensor = QtWidgets.QLineEdit(self.groupBoxText) |
|
116 |
self.lineEditProjectLicensor.setObjectName("lineEditProjectLicensor") |
|
117 |
self.gridLayout_25.addWidget(self.lineEditProjectLicensor, 7, 1, 1, 1) |
|
118 | 107 |
self.lineEditProjectContractor = QtWidgets.QLineEdit(self.groupBoxText) |
119 | 108 |
self.lineEditProjectContractor.setObjectName("lineEditProjectContractor") |
120 | 109 |
self.gridLayout_25.addWidget(self.lineEditProjectContractor, 6, 1, 1, 2) |
110 |
self.lineEditProjectLicensor = QtWidgets.QLineEdit(self.groupBoxText) |
|
111 |
self.lineEditProjectLicensor.setObjectName("lineEditProjectLicensor") |
|
112 |
self.gridLayout_25.addWidget(self.lineEditProjectLicensor, 7, 1, 1, 2) |
|
113 |
self.spinBoxNoPnID = QtWidgets.QSpinBox(self.groupBoxText) |
|
114 |
self.spinBoxNoPnID.setMinimumSize(QtCore.QSize(100, 0)) |
|
115 |
self.spinBoxNoPnID.setObjectName("spinBoxNoPnID") |
|
116 |
self.gridLayout_25.addWidget(self.spinBoxNoPnID, 8, 1, 1, 2) |
|
117 |
self.spinBoxNoEquipment = QtWidgets.QSpinBox(self.groupBoxText) |
|
118 |
self.spinBoxNoEquipment.setMaximum(99999) |
|
119 |
self.spinBoxNoEquipment.setObjectName("spinBoxNoEquipment") |
|
120 |
self.gridLayout_25.addWidget(self.spinBoxNoEquipment, 9, 1, 1, 2) |
|
121 | 121 |
self.spinBoxNoInstrument = QtWidgets.QSpinBox(self.groupBoxText) |
122 | 122 |
self.spinBoxNoInstrument.setMaximum(99999) |
123 | 123 |
self.spinBoxNoInstrument.setObjectName("spinBoxNoInstrument") |
124 |
self.gridLayout_25.addWidget(self.spinBoxNoInstrument, 10, 1, 1, 1)
|
|
124 |
self.gridLayout_25.addWidget(self.spinBoxNoInstrument, 10, 1, 1, 2)
|
|
125 | 125 |
self.spinBoxNoLine = QtWidgets.QSpinBox(self.groupBoxText) |
126 | 126 |
self.spinBoxNoLine.setMaximum(99999) |
127 | 127 |
self.spinBoxNoLine.setObjectName("spinBoxNoLine") |
128 |
self.gridLayout_25.addWidget(self.spinBoxNoLine, 11, 1, 1, 1)
|
|
128 |
self.gridLayout_25.addWidget(self.spinBoxNoLine, 11, 1, 1, 2)
|
|
129 | 129 |
self.gridLayout_14.addLayout(self.gridLayout_25, 0, 0, 1, 1) |
130 | 130 |
self.gridLayout_2.addWidget(self.groupBoxText, 0, 1, 1, 1) |
131 | 131 |
self.tabWidget.addTab(self.Recognition, "") |
... | ... | |
224 | 224 |
self.buttonBox.accepted.connect(ProjectInfoDialog.accept) |
225 | 225 |
self.buttonBox.rejected.connect(ProjectInfoDialog.reject) |
226 | 226 |
QtCore.QMetaObject.connectSlotsByName(ProjectInfoDialog) |
227 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectDesc, self.lineEditProjectDuration) |
|
228 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectDuration, self.spinBoxNoPnID) |
|
229 |
ProjectInfoDialog.setTabOrder(self.spinBoxNoPnID, self.spinBoxMinArea) |
|
230 |
ProjectInfoDialog.setTabOrder(self.spinBoxMinArea, self.spinBoxMaxArea) |
|
231 |
ProjectInfoDialog.setTabOrder(self.spinBoxMaxArea, self.spinBoxWidth) |
|
232 |
ProjectInfoDialog.setTabOrder(self.spinBoxWidth, self.spinBoxHeight) |
|
227 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectName, self.lineEditProjectDesc) |
|
228 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectDesc, self.lineEditProjectType) |
|
229 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectType, self.lineEditProjectMainProcessDesc) |
|
230 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectMainProcessDesc, self.lineEditProjectDuration) |
|
231 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectDuration, self.lineEditProjectOwner) |
|
232 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectOwner, self.lineEditProjectContractor) |
|
233 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectContractor, self.lineEditProjectLicensor) |
|
234 |
ProjectInfoDialog.setTabOrder(self.lineEditProjectLicensor, self.spinBoxNoPnID) |
|
235 |
ProjectInfoDialog.setTabOrder(self.spinBoxNoPnID, self.spinBoxNoEquipment) |
|
236 |
ProjectInfoDialog.setTabOrder(self.spinBoxNoEquipment, self.spinBoxNoInstrument) |
|
237 |
ProjectInfoDialog.setTabOrder(self.spinBoxNoInstrument, self.spinBoxNoLine) |
|
238 |
ProjectInfoDialog.setTabOrder(self.spinBoxNoLine, self.tabWidget) |
|
239 |
ProjectInfoDialog.setTabOrder(self.tabWidget, self.comboBoxLineType) |
|
240 |
ProjectInfoDialog.setTabOrder(self.comboBoxLineType, self.spinBoxWidth) |
|
241 |
ProjectInfoDialog.setTabOrder(self.spinBoxWidth, self.spinBoxMaxArea) |
|
242 |
ProjectInfoDialog.setTabOrder(self.spinBoxMaxArea, self.spinBoxLengthToConnectLine) |
|
243 |
ProjectInfoDialog.setTabOrder(self.spinBoxLengthToConnectLine, self.spinBoxMinArea) |
|
244 |
ProjectInfoDialog.setTabOrder(self.spinBoxMinArea, self.spinBoxHeight) |
|
233 | 245 |
ProjectInfoDialog.setTabOrder(self.spinBoxHeight, self.smallLineMinLengthSpinBox) |
234 |
ProjectInfoDialog.setTabOrder(self.smallLineMinLengthSpinBox, self.spinBoxLengthToConnectLine)
|
|
235 |
ProjectInfoDialog.setTabOrder(self.spinBoxLengthToConnectLine, self.comboBoxLineType)
|
|
236 |
ProjectInfoDialog.setTabOrder(self.comboBoxLineType, self.radioButtonDiagonalYes)
|
|
246 |
ProjectInfoDialog.setTabOrder(self.smallLineMinLengthSpinBox, self.radioButtonGapYes)
|
|
247 |
ProjectInfoDialog.setTabOrder(self.radioButtonGapYes, self.radioButtonGapNo)
|
|
248 |
ProjectInfoDialog.setTabOrder(self.radioButtonGapNo, self.radioButtonDiagonalYes)
|
|
237 | 249 |
ProjectInfoDialog.setTabOrder(self.radioButtonDiagonalYes, self.radioButtonDiagonalNo) |
238 |
ProjectInfoDialog.setTabOrder(self.radioButtonDiagonalNo, self.tabWidget) |
|
239 | 250 |
|
240 | 251 |
def retranslateUi(self, ProjectInfoDialog): |
241 | 252 |
_translate = QtCore.QCoreApplication.translate |
... | ... | |
253 | 264 |
self.label_39.setText(_translate("ProjectInfoDialog", "Project Type : ")) |
254 | 265 |
self.label_17.setText(_translate("ProjectInfoDialog", "Licensor : ")) |
255 | 266 |
self.label_50.setText(_translate("ProjectInfoDialog", "Owner : ")) |
256 |
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Recognition), _translate("ProjectInfoDialog", "Recognition"))
|
|
267 |
self.tabWidget.setTabText(self.tabWidget.indexOf(self.Recognition), _translate("ProjectInfoDialog", "Informaiton"))
|
|
257 | 268 |
self.groupBox.setTitle(_translate("ProjectInfoDialog", "Line Detection")) |
258 | 269 |
self.label_45.setText(_translate("ProjectInfoDialog", "Detect Without Symbol : ")) |
259 | 270 |
self.radioButtonGapYes.setText(_translate("ProjectInfoDialog", "Yes")) |
DTI_PID/DTI_PID/Shapes/EngineeringTextItem.py | ||
---|---|---|
314 | 314 |
# QGraphicsTextItem.keyPressEvent(self, event) |
315 | 315 |
|
316 | 316 |
def mouseMoveEvent(self, event): |
317 |
super().mouseMoveEvent(event) |
|
317 |
modifiers = QApplication.keyboardModifiers() |
|
318 |
if modifiers == Qt.ShiftModifier: |
|
319 |
super().mouseMoveEvent(event) |
|
318 | 320 |
|
319 | 321 |
def itemChange(self, change, value): |
320 | 322 |
""" call signals when item's position or rotation is changed """ |
DTI_PID/DTI_PID/UI/ProjectInfo.ui | ||
---|---|---|
43 | 43 |
</property> |
44 | 44 |
<widget class="QWidget" name="Recognition"> |
45 | 45 |
<attribute name="title"> |
46 |
<string>Recognition</string>
|
|
46 |
<string>Informaiton</string>
|
|
47 | 47 |
</attribute> |
48 | 48 |
<layout class="QGridLayout" name="gridLayout_2"> |
49 | 49 |
<item row="0" column="1"> |
... | ... | |
64 | 64 |
<item row="2" column="1" colspan="2"> |
65 | 65 |
<widget class="QLineEdit" name="lineEditProjectType"/> |
66 | 66 |
</item> |
67 |
<item row="8" column="1"> |
|
68 |
<widget class="QSpinBox" name="spinBoxNoPnID"> |
|
69 |
<property name="minimumSize"> |
|
70 |
<size> |
|
71 |
<width>100</width> |
|
72 |
<height>0</height> |
|
73 |
</size> |
|
74 |
</property> |
|
75 |
</widget> |
|
76 |
</item> |
|
77 | 67 |
<item row="8" column="0"> |
78 | 68 |
<widget class="QLabel" name="label_22"> |
79 | 69 |
<property name="text"> |
... | ... | |
128 | 118 |
</property> |
129 | 119 |
</widget> |
130 | 120 |
</item> |
131 |
<item row="9" column="1"> |
|
132 |
<widget class="QSpinBox" name="spinBoxNoEquipment"> |
|
133 |
<property name="maximum"> |
|
134 |
<number>99999</number> |
|
135 |
</property> |
|
136 |
</widget> |
|
137 |
</item> |
|
138 | 121 |
<item row="1" column="0"> |
139 | 122 |
<widget class="QLabel" name="label_7"> |
140 | 123 |
<property name="text"> |
... | ... | |
191 | 174 |
</property> |
192 | 175 |
</widget> |
193 | 176 |
</item> |
194 |
<item row="7" column="1"> |
|
195 |
<widget class="QLineEdit" name="lineEditProjectLicensor"/> |
|
196 |
</item> |
|
197 | 177 |
<item row="6" column="1" colspan="2"> |
198 | 178 |
<widget class="QLineEdit" name="lineEditProjectContractor"/> |
199 | 179 |
</item> |
200 |
<item row="10" column="1"> |
|
180 |
<item row="7" column="1" colspan="2"> |
|
181 |
<widget class="QLineEdit" name="lineEditProjectLicensor"/> |
|
182 |
</item> |
|
183 |
<item row="8" column="1" colspan="2"> |
|
184 |
<widget class="QSpinBox" name="spinBoxNoPnID"> |
|
185 |
<property name="minimumSize"> |
|
186 |
<size> |
|
187 |
<width>100</width> |
|
188 |
<height>0</height> |
|
189 |
</size> |
|
190 |
</property> |
|
191 |
</widget> |
|
192 |
</item> |
|
193 |
<item row="9" column="1" colspan="2"> |
|
194 |
<widget class="QSpinBox" name="spinBoxNoEquipment"> |
|
195 |
<property name="maximum"> |
|
196 |
<number>99999</number> |
|
197 |
</property> |
|
198 |
</widget> |
|
199 |
</item> |
|
200 |
<item row="10" column="1" colspan="2"> |
|
201 | 201 |
<widget class="QSpinBox" name="spinBoxNoInstrument"> |
202 | 202 |
<property name="maximum"> |
203 | 203 |
<number>99999</number> |
204 | 204 |
</property> |
205 | 205 |
</widget> |
206 | 206 |
</item> |
207 |
<item row="11" column="1"> |
|
207 |
<item row="11" column="1" colspan="2">
|
|
208 | 208 |
<widget class="QSpinBox" name="spinBoxNoLine"> |
209 | 209 |
<property name="maximum"> |
210 | 210 |
<number>99999</number> |
... | ... | |
418 | 418 |
</layout> |
419 | 419 |
</widget> |
420 | 420 |
<tabstops> |
421 |
<tabstop>lineEditProjectName</tabstop> |
|
421 | 422 |
<tabstop>lineEditProjectDesc</tabstop> |
423 |
<tabstop>lineEditProjectType</tabstop> |
|
424 |
<tabstop>lineEditProjectMainProcessDesc</tabstop> |
|
422 | 425 |
<tabstop>lineEditProjectDuration</tabstop> |
426 |
<tabstop>lineEditProjectOwner</tabstop> |
|
427 |
<tabstop>lineEditProjectContractor</tabstop> |
|
428 |
<tabstop>lineEditProjectLicensor</tabstop> |
|
423 | 429 |
<tabstop>spinBoxNoPnID</tabstop> |
424 |
<tabstop>spinBoxMinArea</tabstop> |
|
425 |
<tabstop>spinBoxMaxArea</tabstop> |
|
430 |
<tabstop>spinBoxNoEquipment</tabstop> |
|
431 |
<tabstop>spinBoxNoInstrument</tabstop> |
|
432 |
<tabstop>spinBoxNoLine</tabstop> |
|
433 |
<tabstop>tabWidget</tabstop> |
|
434 |
<tabstop>comboBoxLineType</tabstop> |
|
426 | 435 |
<tabstop>spinBoxWidth</tabstop> |
436 |
<tabstop>spinBoxMaxArea</tabstop> |
|
437 |
<tabstop>spinBoxLengthToConnectLine</tabstop> |
|
438 |
<tabstop>spinBoxMinArea</tabstop> |
|
427 | 439 |
<tabstop>spinBoxHeight</tabstop> |
428 | 440 |
<tabstop>smallLineMinLengthSpinBox</tabstop> |
429 |
<tabstop>spinBoxLengthToConnectLine</tabstop>
|
|
430 |
<tabstop>comboBoxLineType</tabstop>
|
|
441 |
<tabstop>radioButtonGapYes</tabstop>
|
|
442 |
<tabstop>radioButtonGapNo</tabstop>
|
|
431 | 443 |
<tabstop>radioButtonDiagonalYes</tabstop> |
432 | 444 |
<tabstop>radioButtonDiagonalNo</tabstop> |
433 |
<tabstop>tabWidget</tabstop> |
|
434 | 445 |
</tabstops> |
435 | 446 |
<resources> |
436 | 447 |
<include location="../res/MainWindow.qrc"/> |
내보내기 Unified diff