1# -*- coding: utf-8 -*- 2 3#------------------------------------------------------------------------------- 4 5# This file is part of Code_Saturne, a general-purpose CFD tool. 6# 7# Copyright (C) 1998-2021 EDF S.A. 8# 9# This program is free software; you can redistribute it and/or modify it under 10# the terms of the GNU General Public License as published by the Free Software 11# Foundation; either version 2 of the License, or (at your option) any later 12# version. 13# 14# This program is distributed in the hope that it will be useful, but WITHOUT 15# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17# details. 18# 19# You should have received a copy of the GNU General Public License along with 20# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 21# Street, Fifth Floor, Boston, MA 02110-1301, USA. 22 23#------------------------------------------------------------------------------- 24 25""" 26This module defines the following classes: 27- QFileEditor 28""" 29 30#------------------------------------------------------------------------------- 31# Standard modules 32#------------------------------------------------------------------------------- 33 34import sys, os, shutil 35from code_saturne.Base import QtGui, QtCore, QtWidgets 36 37# Check if QString exists 38has_qstring = True 39try: 40 from code_saturne.Base.QtCore import QString 41 _fromUtf8 = QString.fromUtf8 42except ImportError: 43 has_qstring = False 44 def _fromUtf8(s): 45 return s 46 47 def QString(s): 48 return s 49 50try: 51 # PyQt5 52 from code_saturne.Base.QtWidgets import QMainWindow, QMessageBox, \ 53 QAction, QFileDialog, QTextEdit, QPlainTextEdit, QSizePolicy, QMenu, QMessageBox 54except Exception: 55 # PyQt4 56 from code_saturne.Base.QtGui import QMainWindow, QMessageBox, \ 57 QAction, QFileDialog, QTextEdit, QPlainTextEdit, QSizePolicy, QMenu, QMessagBox 58 59import resource_base_rc 60 61#------------------------------------------------------------------------------- 62# Local constants 63#------------------------------------------------------------------------------- 64 65_tab_size = 2 66 67#------------------------------------------------------------------------------- 68# Local functions and/or definitions 69#------------------------------------------------------------------------------- 70 71def loc_format(color, style=''): 72 """ 73 Returns a TextCharFormat with the proper attributes 74 """ 75 76 c = QtGui.QColor() 77 c.setNamedColor(color) 78 79 f = QtGui.QTextCharFormat() 80 f.setForeground(c) 81 82 # Bold font 83 if 'bold' in style: 84 f.setFontWeight(QtGui.QFont.Bold) 85 86 # Italic font 87 if 'italic' in style: 88 f.setFontItalic(True) 89 90 return f 91 92format_styles = {'keyword' : loc_format('blue', 'bold'), 93 'operator' : loc_format('red', 'bold'), 94 'brace' : loc_format('orange', 'bold'), 95 'string' : loc_format('magenta', 'italic'), 96 'comment' : loc_format('darkGreen', 'italic'), 97 'expression' : loc_format('black')} 98 99#------------------------------------------------------------------------------- 100# HighlightingRule class 101#------------------------------------------------------------------------------- 102 103class HighlightingRule(): 104 105 # --------------------------------------------------------------- 106 def __init__(self, pattern, format): 107 108 self.pattern = pattern 109 self.format = format 110 # --------------------------------------------------------------- 111 112 113#------------------------------------------------------------------------------- 114# CodeEditor with line numbering 115#------------------------------------------------------------------------------- 116 117class LineNumberArea(QtWidgets.QWidget): 118 119 def __init__(self, editor): 120 # Handle the python2/python3 differences for super 121 super(LineNumberArea, self).__init__(editor) 122 123 self.editor = editor 124 125 def sizeHint(self): 126 return QtCore.QSize(self.editor.lineNumberAreaWidth(),0) 127 128 def paintEvent(self, event): 129 self.editor.lineNumberAreaPaintEvent(event) 130 131class CodeEditor(QPlainTextEdit): 132 def __init__(self): 133 # Handle the python2/python3 differences for super 134 super(CodeEditor, self).__init__() 135 136 self.lineNumberArea = LineNumberArea(self) 137 138 self.blockCountChanged.connect(self.updateLineNumberAreaWidth) 139 self.updateRequest.connect(self.updateLineNumberArea) 140 self.cursorPositionChanged.connect(self.highlightCurrentLine) 141 142 self.updateLineNumberAreaWidth(0) 143 144 145 def lineNumberAreaWidth(self): 146 digits = 1 147 count = max(1, self.blockCount()) 148 while count >= 10: 149 count /= 10 150 digits += 1 151 space = 3 + self.fontMetrics().width('9') * digits 152 return space 153 154 155 def updateLineNumberAreaWidth(self, _): 156 self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0) 157 158 159 def updateLineNumberArea(self, rect, dy): 160 161 if dy: 162 self.lineNumberArea.scroll(0, dy) 163 else: 164 self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), 165 rect.height()) 166 167 if rect.contains(self.viewport().rect()): 168 self.updateLineNumberAreaWidth(0) 169 170 171 def resizeEvent(self, event): 172 # Handle the python2/python3 differences for super 173 super(CodeEditor, self).resizeEvent(event) 174 175 cr = self.contentsRect(); 176 self.lineNumberArea.setGeometry(QtCore.QRect(cr.left(), cr.top(), 177 self.lineNumberAreaWidth(), cr.height())) 178 179 180 def lineNumberAreaPaintEvent(self, event): 181 mypainter = QtGui.QPainter(self.lineNumberArea) 182 183 mypainter.fillRect(event.rect(), QtCore.Qt.lightGray) 184 185 block = self.firstVisibleBlock() 186 blockNumber = block.blockNumber() 187 top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() 188 bottom = top + self.blockBoundingRect(block).height() 189 190 # Just to make sure I use the right font 191 height = self.fontMetrics().height() 192 while block.isValid() and (top <= event.rect().bottom()): 193 if block.isVisible() and (bottom >= event.rect().top()): 194 number = str(blockNumber + 1) 195 mypainter.setPen(QtCore.Qt.black) 196 mypainter.drawText(0, top, self.lineNumberArea.width(), height, 197 QtCore.Qt.AlignRight, number) 198 199 block = block.next() 200 top = bottom 201 bottom = top + self.blockBoundingRect(block).height() 202 blockNumber += 1 203 204 205 def highlightCurrentLine(self): 206 extraSelections = [] 207 208 if not self.isReadOnly(): 209 selection = QTextEdit.ExtraSelection() 210 211 lineColor = QtGui.QColor(QtCore.Qt.yellow).lighter(160) 212 213 selection.format.setBackground(lineColor) 214 selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True) 215 selection.cursor = self.textCursor() 216 selection.cursor.clearSelection() 217 extraSelections.append(selection) 218 self.setExtraSelections(extraSelections) 219 220#------------------------------------------------------------------------------- 221# QtextHighlighter class 222#------------------------------------------------------------------------------- 223 224class QtextHighlighter(QtGui.QSyntaxHighlighter): 225 """ 226 Syntax highighting 227 """ 228 229 # --------------------------------------------------------------- 230 def __init__(self, parent, extension): 231 232 QtGui.QSyntaxHighlighter.__init__(self, parent) 233 self.parent = parent 234 self.highlightingRules = [] 235 236 # Keywords (C or Fortran) 237 fortran_kw = ['if', 'else', 'endif', 'do', 'enddo', 'end', 238 'implicit none', 'use', 'subroutine', 'function', 239 'double precision', 'real', 'integer', 'char', 240 'allocatable', 'allocate', 'deallocate', 'dimension', 241 'select case', 'call'] 242 243 c_kw = ['if', 'else', 'for', 'switch', 'while', 244 '\#', 'include', 'pass', 'return', 'del', 'delete', 245 'assert', 'true', 'false', 'continue', 'break', 246 'fprintf', 'bft_printf', 'bft_printf_flush', 'bft_error', 247 'cs_real_t', 'cs_lnum_t', 'cs_real_3_t', 'int', 'char', 248 'string', 'void', 'double', 'const', 249 'BEGIN_C_DECLS', 'END_C_DECLS'] 250 251 py_kw = ['if', 'elif', 'for', 'range', 'while', 'return', 'def', 252 'True', 'False'] 253 254 self.kw = [] 255 # Fortran 256 if extension in ['f90', 'F90', 'F', 'f77']: 257 for kw in fortran_kw: 258 self.kw.append(kw) 259 self.kw.append(kw.upper()) 260 # C/C++ 261 elif extension in ['c', 'cpp', 'cxx', 'c++']: 262 for kw in c_kw: 263 self.kw.append(kw) 264 self.kw.append(kw.upper()) 265 # Python 266 elif extension == 'py': 267 for kw in py_kw: 268 self.kw.append(kw) 269 270 271 # Operators 272 self.op = ['=', '==', '!=', '<', '>', '<=', '>=', 273 '\+', '-', '\*', '/', '\%', '\*\*', 274 '\+=', '-=', '\*=', '/=', '->', '=>', 275 '\^', '\|', '\&', '\|\|', '\&\&'] 276 277 # Braces 278 self.br = ['\(', '\)', '\{', '\}', '\[', '\]'] 279 280 # RULES 281 for kw in self.kw: 282 p = QtCore.QRegExp("\\b"+kw+ '\\b') 283 rule = HighlightingRule(p, format_styles['keyword']) 284 self.highlightingRules.append(rule) 285 286 for op in self.op: 287 p = QtCore.QRegExp(op) 288 rule = HighlightingRule(p, format_styles['operator']) 289 self.highlightingRules.append(rule) 290 291 for br in self.br: 292 p = QtCore.QRegExp(br) 293 rule = HighlightingRule(p, format_styles['brace']) 294 self.highlightingRules.append(rule) 295 296 # strings 297 ps = QtCore.QRegExp('"[^"\\]*(\\.[^"\\]*)*"') 298 rs = HighlightingRule(ps, format_styles['string']) 299 self.highlightingRules.append(rs) 300 301 # comments 302 pc = QtCore.QRegExp('//[^\n]*') 303 rc = HighlightingRule(pc, format_styles['comment']) 304 self.highlightingRules.append(rc) 305 306 pcf = QtCore.QRegExp('![^\n]*') 307 rcf = HighlightingRule(pcf, format_styles['comment']) 308 self.highlightingRules.append(rcf) 309 310 # numerals 311 pn1 = QtCore.QRegExp('[+-]?[0-9]+[lL]?') 312 rn1 = HighlightingRule(pn1, format_styles['expression']) 313 self.highlightingRules.append(rn1) 314 pn2 = QtCore.QRegExp('[+-]?0[xX][0-9A-Fa-f]+[lL]?') 315 rn2 = HighlightingRule(pn2, format_styles['expression']) 316 self.highlightingRules.append(rn2) 317 pn3 = QtCore.QRegExp('[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?') 318 rn3 = HighlightingRule(pn3, format_styles['expression']) 319 self.highlightingRules.append(rn3) 320 # --------------------------------------------------------------- 321 322 323 # --------------------------------------------------------------- 324 def highlightBlock(self, text): 325 """ 326 Apply the syntax highlighting 327 """ 328 for rule in self.highlightingRules: 329 exp = QtCore.QRegExp(rule.pattern) 330 index = exp.indexIn(text) 331 332 while index >= 0: 333 length = exp.matchedLength() 334 ok_to_highlight = True 335 if len(text) > index+length: 336 if text[index+length] not in self.op+[' ']: 337 ok_to_highlight = False 338 if text[index:index+length] not in self.op+self.br: 339 ok_to_highlight = True 340 341 if ok_to_highlight: 342 self.setFormat(index, length, rule.format) 343 if has_qstring: 344 index = text.indexOf(exp, index + length) 345 else: 346 index = text.find(exp.cap(), index + length) 347 348 self.setCurrentBlockState(0) 349 350 # C/C++ comments 351 self.highlightCommentsOverLines(text, "/\\*", "\\*/") 352 # --------------------------------------------------------------- 353 354 355 # --------------------------------------------------------------- 356 def highlightCommentsOverLines(self, text, dls, dle): 357 358 startExpression = QtCore.QRegExp(dls) 359 endExpression = QtCore.QRegExp(dle) 360 ref_state = 1 361 362 if self.previousBlockState() == ref_state: 363 start = 0 364 add = 0 365 366 else: 367 start = startExpression.indexIn(text) 368 add = startExpression.matchedLength() 369 370 371 while start >= 0: 372 end = endExpression.indexIn(text, start + add) 373 374 if end >= add: 375 length = end - start + add + endExpression.matchedLength() 376 self.setCurrentBlockState(0) 377 378 else: 379 self.setCurrentBlockState(ref_state) 380 if has_qstring: 381 length = text.length() - start + add 382 else: 383 length = len(text) - start + add 384 385 self.setFormat(start, length, format_styles['comment']) 386 start = endExpression.indexIn(text, start + length) 387 # --------------------------------------------------------------- 388 389 390#------------------------------------------------------------------------------- 391# QMessageBox which expands 392#------------------------------------------------------------------------------- 393 394class QExpandingMessageBox(QMessageBox): 395 """ 396 A QMessageBox which expands. 397 """ 398 399 def __init__(self, parent=None): 400 QMessageBox.__init__(self,parent=parent) 401 self.setSizeGripEnabled(True) 402 403 def event(self, ev): 404 405 result = QMessageBox.event(self, ev) 406 407 self.setMinimumHeight(10) 408 self.setMaximumHeight(16777215) 409 self.setMinimumWidth(10) 410 self.setMaximumWidth(16777215) 411 self.setSizePolicy(QSizePolicy.Expanding, 412 QSizePolicy.Expanding) 413 414 text = self.findChild(QTextEdit) 415 if text != None: 416 self.setMinimumHeight(10) 417 self.setMaximumHeight(16777215) 418 self.setMinimumWidth(1050) 419 self.setMaximumWidth(16777215) 420 421 text.setMinimumHeight(10) 422 text.setMaximumHeight(16777215) 423 text.setMinimumWidth(1000) 424 text.setMaximumWidth(16777215) 425 text.setSizePolicy(QSizePolicy.Expanding, 426 QSizePolicy.Expanding) 427 428 return result 429 430#------------------------------------------------------------------------------- 431# QFileEditor class 432#------------------------------------------------------------------------------- 433 434class FormWidget(QtWidgets.QWidget): 435 """ 436 Main widget used to include both the browser and the editor zone 437 """ 438 439 # --------------------------------------------------------------- 440 def __init__(self, parent, wlist): 441 super(FormWidget, self).__init__(parent) 442 443 self.layout = QtWidgets.QGridLayout(self) 444 445 n = len(wlist) - 1 446 for i, w in enumerate(wlist): 447 if i < n: 448 w.setMaximumWidth(400) 449 self.layout.addWidget(w, i, 0) 450 else: 451 self.layout.addWidget(w, 0, 1, 2, 1) 452 453 self.setLayout(self.layout) 454 # --------------------------------------------------------------- 455 456 457#------------------------------------------------------------------------------- 458# QFileSystemModel with modified header 459#------------------------------------------------------------------------------- 460 461class FileSystemModel(QtWidgets.QFileSystemModel): 462 463 def __init__(self, title): 464 """ 465 """ 466 QtWidgets.QFileSystemModel.__init__(self) 467 self.title = title 468 469 470 def headerData(self, section, orientation, role): 471 if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: 472 if section == 0: 473 return self.tr(self.title) 474 return None 475 476 477#------------------------------------------------------------------------------- 478# Explorer class 479#------------------------------------------------------------------------------- 480 481class Explorer(): 482 """ 483 Editor class. Used for file editing and/or viewing 484 """ 485 486 # --------------------------------------------------------------- 487 def __init__(self, parent=None, root_dir=None, dir_type=None, 488 case_name=None, readOnly=False): 489 490 self.parent = parent 491 492 self.root_dir = root_dir 493 self.dir_type = dir_type 494 495 self.readOnly = readOnly 496 self.readerMode = readOnly 497 498 # Explorer 499 self.explorer = self._initFileExplorer() 500 self._initExplorerActions(case_name) 501 502 # --------------------------------------------------------------- 503 504 505 # --------------------------------------------------------------- 506 def _initFileExplorer(self): 507 """ 508 Create the File explorer object based on the QFileSystemModel widget. 509 """ 510 511 if self.dir_type == 'SHARE': 512 name = 'Reference' 513 elif self.dir_type in ('SRC', 'DATA'): 514 name = 'User files' 515 else: 516 name = 'Name' 517 518 model = FileSystemModel(name) 519 if self.root_dir: 520 model.setRootPath(self.root_dir) 521 522 tree = QtWidgets.QTreeView(None) 523 524 tree.setModel(model) 525 tree.setSortingEnabled(True) 526 tree.setWindowTitle('Explorer') 527 if self.root_dir: 528 tree.setRootIndex(model.index(self.root_dir)) 529 530 # Hide unnecessary columns 531 nc = tree.header().count() 532 533 for i in range(1, nc): 534 tree.hideColumn(i) 535 536 # Right click menu 537 tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 538 tree.customContextMenuRequested.connect(self.explorerContextMenu) 539 540 # Double click 541 tree.doubleClicked.connect(self._explorerDoubleClick) 542 543 return tree; 544 # --------------------------------------------------------------- 545 546 547 # --------------------------------------------------------------- 548 def _initExplorerActions(self, case_name=None): 549 """ 550 Create explorer actions dictionary 551 """ 552 553 if case_name: 554 case_dir_name = str(case_name) 555 else: 556 case_dir_name = 'SRC' 557 558 _editAction = QAction(self.explorer.model()) 559 _editAction.setText('Edit file') 560 _editAction.triggered.connect(self.parent._editSelectedFile) 561 562 _viewAction = QAction(self.explorer.model()) 563 _viewAction.setText('View file') 564 _viewAction.triggered.connect(self.parent._viewSelectedFile) 565 566 _copyAction = QAction(self.explorer.model()) 567 _copyAction.setText('Copy to ' + case_dir_name) 568 _copyAction.triggered.connect(self.parent._copySelectedFile) 569 570 _removeAction = QAction(self.explorer.model()) 571 _removeAction.setText('Remove from ' + case_dir_name) 572 _removeAction.triggered.connect(self.parent._removeSelectedFile) 573 574 _restoreAction = QAction(self.explorer.model()) 575 _restoreAction.setText('Move to ' + case_dir_name) 576 _restoreAction.triggered.connect(self.parent._restoreSelectedFile) 577 578 _deleteAction = QAction(self.explorer.model()) 579 _deleteAction.setText('Delete') 580 _deleteAction.triggered.connect(self.parent._deleteSelectedFile) 581 582 self._explorerActions = {'edit': _editAction, 583 'view': _viewAction, 584 'copy': _copyAction, 585 'remove': _removeAction, 586 'restore': _restoreAction, 587 'delete': _deleteAction} 588 # --------------------------------------------------------------- 589 590 591 # --------------------------------------------------------------- 592 def _updateCurrentSelection(self): 593 """ 594 Update the current selection 595 """ 596 # Find file position (SRC, REFERENCE, EXAMPLES, other) 597 path2file = '' 598 for idx in self.explorer.selectedIndexes(): 599 fname = idx.data(QtCore.Qt.DisplayRole) 600 c = idx 601 p = c.parent() 602 ps = p.data(QtCore.Qt.DisplayRole) 603 while True: 604 ctxt = c.data(QtCore.Qt.DisplayRole) 605 ptxt = p.data(QtCore.Qt.DisplayRole) 606 if ptxt in [None, self.parent.case_name]: 607 pe = ptxt 608 break 609 path2file = ptxt + '/' + path2file 610 c = p 611 p = c.parent() 612 613 self.parent._currentSelection = {'filename':fname, 614 'subpath' :path2file, 615 'filedir' :ps, 616 'origdir' :pe} 617 618 return 619 # --------------------------------------------------------------- 620 621 622 # --------------------------------------------------------------- 623 def _explorerDoubleClick(self): 624 """ 625 Double click action 626 """ 627 628 self._updateCurrentSelection() 629 630 clicked = os.path.join(self.parent._currentSelection['subpath'], 631 self.parent._currentSelection['filename']) 632 633 # To ensure that os.path.isdir works correctly we use the full path 634 # to the object which is selected in the menu 635 if self.root_dir: 636 clicked = os.path.join(self.root_dir, clicked) 637 638 edit_list = ['SRC'] 639 640 if not os.path.isdir(clicked): 641 if self.parent._currentSelection['filedir'] in edit_list: 642 self.parent._editSelectedFile() 643 else: 644 self.parent._viewSelectedFile() 645 646 # --------------------------------------------------------------- 647 648 649 # --------------------------------------------------------------- 650 def explorerContextMenu(self, position): 651 """ 652 Custom menu for the mouse right-click. 653 Depends on whether the file is in the SRC, SRC/subfolder 654 or RESU/subfolder. 655 Possible actions are 'edit', 'view' and 'copy' (to SRC) 656 """ 657 658 self._updateCurrentSelection() 659 660 path2file = self.parent._currentSelection['subpath'] 661 fname = self.parent._currentSelection['filename'] 662 pe = self.parent._currentSelection['origdir'] 663 ps = self.parent._currentSelection['filedir'] 664 665 self._contextMenu = QMenu() 666 667 if (path2file == '' or path2file == None ) and self.root_dir: 668 path2file = self.root_dir 669 670 if self.dir_type == 'SHARE': 671 if not os.path.isdir(os.path.join(path2file, fname)): 672 self._contextMenu.addAction(self._explorerActions['view']) 673 self._contextMenu.addAction(self._explorerActions['copy']) 674 elif pe == 'SRC': 675 if not os.path.isdir(os.path.join(path2file, fname)): 676 if ps == 'SRC': 677 self._contextMenu.addAction(self._explorerActions['edit']) 678 self._contextMenu.addAction(self._explorerActions['remove']) 679 elif ps in ['EXAMPLES', 'REFERENCE']: 680 self._contextMenu.addAction(self._explorerActions['view']) 681 self._contextMenu.addAction(self._explorerActions['copy']) 682 elif ps in ['DRAFT']: 683 self._contextMenu.addAction(self._explorerActions['view']) 684 self._contextMenu.addAction(self._explorerActions['restore']) 685 self._contextMenu.addAction(self._explorerActions['delete']) 686 elif pe == 'DATA': 687 if not os.path.isdir(os.path.join(path2file, fname)): 688 if ps == 'DATA': 689 if fname not in ('setup.xml', 'run.cfg'): 690 self._contextMenu.addAction(self._explorerActions['edit']) 691 self._contextMenu.addAction(self._explorerActions['remove']) 692 else: 693 self._contextMenu.addAction(self._explorerActions['view']) 694 elif ps in ['REFERENCE']: 695 self._contextMenu.addAction(self._explorerActions['view']) 696 self._contextMenu.addAction(self._explorerActions['copy']) 697 elif ps in ['DRAFT']: 698 self._contextMenu.addAction(self._explorerActions['view']) 699 self._contextMenu.addAction(self._explorerActions['restore']) 700 self._contextMenu.addAction(self._explorerActions['delete']) 701 else: 702 if not os.path.isdir(os.path.join(path2file, fname)): 703 self._contextMenu.addAction(self._explorerActions['view']) 704 705 self._contextMenu.exec_(self.explorer.viewport().mapToGlobal(position)) 706 # --------------------------------------------------------------- 707 708 709#------------------------------------------------------------------------------- 710# QFileEditor class 711#------------------------------------------------------------------------------- 712 713class QFileEditor(QMainWindow): 714 """ 715 Editor class. Used for file editing and/or viewing 716 """ 717 718 # --------------------------------------------------------------- 719 def __init__(self, parent=None, case_dir=None, reference_dir=None, 720 readOnly=False, noOpen=False, useHighlight=True): 721 super(QFileEditor, self).__init__(parent) 722 self.setGeometry(50, 50, 500, 300) 723 724 self.setWindowTitle("code_saturne built-in file editor") 725 self.parent = parent 726 727 self.case_dir = case_dir 728 if self.case_dir: 729 self.case_name = os.path.split(case_dir)[-1] 730 731 self.last_dir = case_dir 732 733 self.readOnly = readOnly 734 self.readerMode = readOnly 735 736 # Activate text highlight 737 self.useHighlight = useHighlight 738 739 self.opened = False 740 self.saved = True 741 742 # Open file action 743 open_img_path = ":/icons/22x22/document-open.png" 744 icon_open = QtGui.QIcon() 745 icon_open.addPixmap(QtGui.QPixmap(_fromUtf8(open_img_path)), 746 QtGui.QIcon.Normal, 747 QtGui.QIcon.Off) 748 self.openFileAction = QAction(icon_open, "Open", self) 749 self.openFileAction.setShortcut("Ctrl+O") 750 self.openFileAction.setStatusTip('Open File') 751 self.openFileAction.triggered.connect(self.openFileForAction) 752 753 # New file action 754 new_img_path = ":/icons/22x22/document-new.png" 755 icon_new = QtGui.QIcon() 756 icon_new.addPixmap(QtGui.QPixmap(_fromUtf8(new_img_path)), 757 QtGui.QIcon.Normal, 758 QtGui.QIcon.Off) 759 self.newFileAction = QAction(icon_new, "New", self) 760 self.newFileAction.setShortcut("Ctrl+E") 761 self.newFileAction.setStatusTip('Create new file') 762 self.newFileAction.triggered.connect(self.newFile) 763 764 # Save action 765 save_img_path = ":/icons/22x22/document-save.png" 766 icon_save = QtGui.QIcon() 767 icon_save.addPixmap(QtGui.QPixmap(_fromUtf8(save_img_path)), 768 QtGui.QIcon.Normal, 769 QtGui.QIcon.Off) 770 self.saveFileAction = QAction(icon_save, "Save", self) 771 self.saveFileAction.setShortcut("Ctrl+S") 772 self.saveFileAction.setStatusTip('Save file') 773 self.saveFileAction.triggered.connect(self.saveFile) 774 775 # Save as action 776 saveas_img_path = ":/icons/22x22/document-save-as.png" 777 icon_saveas = QtGui.QIcon() 778 icon_saveas.addPixmap(QtGui.QPixmap(_fromUtf8(saveas_img_path)), 779 QtGui.QIcon.Normal, 780 QtGui.QIcon.Off) 781 self.saveFileAsAction = QAction(icon_saveas, "Save as", self) 782 self.saveFileAsAction.setStatusTip('Save file as') 783 self.saveFileAsAction.triggered.connect(self.saveFileAs) 784 785 # Close file action 786 close_img_path = ":/icons/22x22/process-stop.png" 787 icon_close = QtGui.QIcon() 788 icon_close.addPixmap(QtGui.QPixmap(_fromUtf8(close_img_path)), 789 QtGui.QIcon.Normal, 790 QtGui.QIcon.Off) 791 self.closeFileAction = QAction(icon_close, "Close file", self) 792 self.closeFileAction.setShortcut("Ctrl+Q") 793 self.closeFileAction.setStatusTip('Close opened file') 794 self.closeFileAction.triggered.connect(self.closeOpenedFile) 795 796 # Exit editor action 797 quit_img_path = ":/icons/22x22/system-log-out.png" 798 icon_quit = QtGui.QIcon() 799 icon_quit.addPixmap(QtGui.QPixmap(_fromUtf8(quit_img_path)), 800 QtGui.QIcon.Normal, 801 QtGui.QIcon.Off) 802 self.quitAction = QAction(icon_quit, "Quit", self) 803 self.quitAction.setStatusTip('Quit the editor') 804 self.quitAction.triggered.connect(self.closeApplication) 805 806 self.statusBar() 807 808 # File toolbar 809 self.toolbar = self.addToolBar("Options") 810 811 self.toolbar.addAction(self.newFileAction) 812 if not noOpen: 813 self.toolbar.addAction(self.openFileAction) 814 self.toolbar.addAction(self.saveFileAction) 815 self.toolbar.addAction(self.saveFileAsAction) 816 self.toolbar.addAction(self.closeFileAction) 817 self.toolbar.addAction(self.quitAction) 818 819 # File menu 820 self.mainMenu = self.menuBar() 821 822 self.fileMenu = self.mainMenu.addMenu('&File') 823 self.fileMenu.addAction(self.newFileAction) 824 if not noOpen: 825 self.fileMenu.addAction(self.openFileAction) 826 self.fileMenu.addAction(self.saveFileAction) 827 self.fileMenu.addAction(self.saveFileAsAction) 828 self.fileMenu.addAction(self.closeFileAction) 829 self.fileMenu.addAction(self.quitAction) 830 831 # Explorer 832 self.explorer = Explorer(parent=self, 833 root_dir=self.case_dir, 834 dir_type=self.case_name, 835 case_name=self.case_name) 836 837 # Explorer 838 self.explorer_ref = None 839 if reference_dir: 840 self.explorer_ref = Explorer(parent=self, 841 root_dir=reference_dir, 842 dir_type='SHARE', 843 case_name=self.case_name) 844 845 # Editor 846 self.textEdit = self._initFileEditor() 847 848 # Settings 849 settings = QtCore.QSettings() 850 851 try: 852 # API 2 853 self.restoreGeometry(settings.value("MainWindow/Geometry", QtCore.QByteArray())) 854 self.restoreState(settings.value("MainWindow/State", QtCore.QByteArray())) 855 except: 856 # API 1 857 self.recentFiles = settings.value("RecentFiles").toStringList() 858 self.restoreGeometry(settings.value("MainWindow/Geometry").toByteArray()) 859 self.restoreState(settings.value("MainWindow/State").toByteArray()) 860 861 # file attributes 862 self.filename = "" 863 self.file_extension = "" 864 865 if self.explorer_ref: 866 self.mainWidget = FormWidget(self, [self.explorer.explorer, 867 self.explorer_ref.explorer, 868 self.textEdit]) 869 else: 870 self.mainWidget = FormWidget(self, [self.explorer.explorer, 871 self.textEdit]) 872 873 self.setCentralWidget(self.mainWidget) 874 # --------------------------------------------------------------- 875 876 877 # --------------------------------------------------------------- 878 def _initFileEditor(self): 879 """ 880 Create the Editor widget based on QTextEdit 881 """ 882 883 # Font 884 base_font = QtGui.QFont() 885 base_font.setFamily("Courier") 886 base_font.setStyleHint(QtGui.QFont.Monospace) 887 base_font.setFixedPitch(True) 888 base_font.setPointSize(10) 889 890 font_metrics = QtGui.QFontMetrics(base_font) 891 _tab_string = '' 892 for i in range(_tab_size): 893 _tab_string += ' ' 894 895 # Main text zone 896 textEdit = CodeEditor() 897 textEdit.setFont(base_font) 898 textEdit.textChanged.connect(self.updateFileState) 899 textEdit.setReadOnly(self.readOnly) 900 policy = textEdit.sizePolicy() 901 policy.setHorizontalPolicy(QSizePolicy.Expanding) 902 textEdit.setSizePolicy(policy) 903 904 # tab 905 textEdit.setTabStopWidth(font_metrics.width(_tab_string)) 906 907 return textEdit 908 # --------------------------------------------------------------- 909 910 911 # --------------------------------------------------------------- 912 def _initFileExplorer(self, base_dir=None, name="User Files"): 913 """ 914 Create the File explorer object based on the QFileSystemModel widget. 915 """ 916 917 #model = QtWidgets.QFileSystemModel() 918 model = FileSystemModel(name) 919 rootp = '' 920 if base_dir: 921 rootp = base_dir 922 elif self.case_dir: 923 rootp = self.case_dir 924 925 model.setRootPath(rootp) 926 927 tree = QtWidgets.QTreeView(None) 928 929 tree.setModel(model) 930 tree.setSortingEnabled(True) 931 tree.setWindowTitle('Explorer') 932 tree.setRootIndex(model.index(rootp)) 933 934 # Hide unnecessary columns 935 nc = tree.header().count() 936 937 for i in range(1, nc): 938 tree.hideColumn(i) 939 940 # Right click menu 941 tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 942 tree.customContextMenuRequested.connect(self.explorerContextMenu) 943 944 # Double click 945 tree.doubleClicked.connect(self._explorerDoubleClick) 946 947 return tree; 948 # --------------------------------------------------------------- 949 950 951 # --------------------------------------------------------------- 952 def _editSelectedFile(self): 953 """ 954 Edit action for mouse right-click 955 """ 956 957 self.readOnly = False 958 959 t = "Editor: %s" % (self._currentSelection['filename']) 960 self.setWindowTitle(t) 961 962 fn = os.path.join(self.case_dir, 963 self._currentSelection['subpath'], 964 self._currentSelection['filename']) 965 self.openFile(fn=fn) 966 # --------------------------------------------------------------- 967 968 969 # --------------------------------------------------------------- 970 def _viewSelectedFile(self): 971 """ 972 View action for mouse left-click 973 """ 974 975 self.readOnly = True 976 977 t = "Viewer: %s" % (self._currentSelection['filename']) 978 self.setWindowTitle(t) 979 980 fn = os.path.join(self.case_dir, 981 self._currentSelection['subpath'], 982 self._currentSelection['filename']) 983 self.openFile(fn=fn) 984 # --------------------------------------------------------------- 985 986 987 # --------------------------------------------------------------- 988 def _copySelectedFile(self): 989 """ 990 Copy files in subdirectories, such as REFERENCES or EXAMPLES 991 to the SRC folder. Used by the mouse right-click 992 """ 993 994 src_path = os.path.join(self.case_dir, 995 self._currentSelection['subpath'], 996 self._currentSelection['filename']) 997 998 if self.case_name in ('SRC', 'DATA'): 999 trg_path = os.path.join(self.case_dir, 1000 self._currentSelection['filename']) 1001 else: 1002 sp = self._currentSelection['subpath'] 1003 while '/' in sp and len(sp) > 3: 1004 e1, e2 = os.path.split(sp) 1005 if e2 in ('SRC', 'DATA'): 1006 break 1007 else: 1008 sp = e1 1009 1010 trg_path = os.path.join(sp, self._currentSelection['filename']) 1011 1012 shutil.copy2(src_path, trg_path) 1013 # --------------------------------------------------------------- 1014 1015 1016 # --------------------------------------------------------------- 1017 def _removeSelectedFile(self): 1018 """ 1019 Remove a file from the SRC dir 1020 """ 1021 1022 title = "Remove file" 1023 question = "Remove %s from the SRC folder (Stored in DRAFT) ?" % (self._currentSelection['filename']) 1024 1025 choice = QMessageBox.question(self, 1026 title, 1027 question, 1028 QMessageBox.Yes | QMessageBox.No) 1029 1030 if choice == QMessageBox.Yes: 1031 fn = os.path.join(self.case_dir, 1032 self._currentSelection['subpath'], 1033 self._currentSelection['filename']) 1034 1035 1036 draft = os.path.join(self.case_dir, 1037 self._currentSelection['subpath'], 1038 'DRAFT') 1039 if not os.path.exists(draft): 1040 os.mkdir(draft) 1041 fn2 = os.path.join(draft, self._currentSelection['filename']) 1042 1043 if os.path.exists(fn2): 1044 q = 'A file named %s allready exists in DRAFT.\nDo you want to overwrite it?' % (self._currentSelection['filename']) 1045 choice2 = QMessageBox.question(self, 1046 '', 1047 q, 1048 QMessageBox.Yes | QMessageBox.No) 1049 if choice2 == QMessageBox.No: 1050 return 1051 1052 shutil.move(fn, fn2) 1053 else: 1054 pass 1055 # --------------------------------------------------------------- 1056 1057 1058 # --------------------------------------------------------------- 1059 def _restoreSelectedFile(self): 1060 """ 1061 Move a file from DRAFT to the SRC folder 1062 """ 1063 1064 title = "Move to SRC" 1065 question = "Move file %s from DRAFT to SRC folder ?" % (self._currentSelection['filename']) 1066 1067 choice = QMessageBox.question(self, 1068 title, 1069 question, 1070 QMessageBox.Yes | QMessageBox.No) 1071 1072 if choice == QMessageBox.Yes: 1073 fn = os.path.join(self.case_dir, 1074 self._currentSelection['subpath'], 1075 self._currentSelection['filename']) 1076 1077 fn2 = os.path.join(self.case_dir, self._currentSelection['filename']) 1078 1079 if os.path.exists(fn2): 1080 q = 'A file named %s allready exists in SRC\nDo you want to overwrite it?' % (self._currentSelection['filename']) 1081 choice2 = QMessageBox.question(self, '', q, 1082 QMessageBox.Yes | QMessageBox.No) 1083 1084 if choice2 == QMessageBox.No: 1085 return 1086 1087 shutil.move(fn, fn2) 1088 1089 else: 1090 pass 1091 # --------------------------------------------------------------- 1092 1093 1094 # --------------------------------------------------------------- 1095 def _deleteSelectedFile(self): 1096 """ 1097 Remove a file from the SRC dir 1098 """ 1099 1100 title = "Delete file" 1101 question = "Really delete %s ?" % (self._currentSelection['filename']) 1102 1103 choice = QMessageBox.question(self, 1104 title, 1105 question, 1106 QMessageBox.Yes | QMessageBox.No) 1107 1108 if choice == QMessageBox.Yes: 1109 fn = os.path.join(self.case_dir, 1110 self._currentSelection['subpath'], 1111 self._currentSelection['filename']) 1112 1113 try: 1114 os.remove(fn) 1115 except Exception: 1116 # TODO add error popup 1117 pass 1118 1119 d = os.path.split(fn)[0] 1120 if os.path.basename(d) in ('DRAFT', 'STASH'): 1121 l = os.listdir(d) 1122 if len(l) < 1: 1123 try: 1124 os.rmdir(d) 1125 except Exception: 1126 pass 1127 else: 1128 pass 1129 # --------------------------------------------------------------- 1130 1131 1132 # --------------------------------------------------------------- 1133 def updateFileState(self, new_state = False): 1134 """ 1135 Update file state (saved or not) 1136 """ 1137 self.saved = new_state 1138 # To ensure syntax highlighting while modifying the text 1139 self.textEdit.viewport().update() 1140 # --------------------------------------------------------------- 1141 1142 1143 # --------------------------------------------------------------- 1144 def openFile(self, fn = None): 1145 """ 1146 Open a file in the editor 1147 """ 1148 1149 if not self.saved: 1150 self.closeOpenedFile() 1151 1152 if fn: 1153 self.filename = fn 1154 else: 1155 self.filename = QFileDialog.getOpenFileName(self, 'Open File', 1156 self.last_dir) 1157 1158 if self.filename: 1159 self.last_dir = os.path.split(self.filename)[0] 1160 1161 self.textEdit.setReadOnly(self.readOnly) 1162 self.saveFileAction.setEnabled(not self.readOnly) 1163 1164 if self.filename != None and self.filename != '': 1165 file = open(self.filename, 'r') 1166 self.file_extension = self.filename.split('.')[-1] 1167 1168 self.newFile() 1169 with file: 1170 text = file.read() 1171 self.textEdit.setPlainText(text) 1172 self.updateFileState(True) 1173 # --------------------------------------------------------------- 1174 1175 1176 # --------------------------------------------------------------- 1177 def openFileForAction(self, fn = None): 1178 1179 if self.readOnly != self.readerMode: 1180 self.readOnly = self.readerMode 1181 1182 self.openFile(fn) 1183 # --------------------------------------------------------------- 1184 1185 1186 # --------------------------------------------------------------- 1187 def newFile(self): 1188 """ 1189 Create a new file (blank) 1190 """ 1191 1192 self.opened = True 1193 self.updateFileState(False) 1194 if self.useHighlight: 1195 hl = QtextHighlighter(self.textEdit.document(), self.file_extension) 1196 self.textEdit.show() 1197 # --------------------------------------------------------------- 1198 1199 1200 # --------------------------------------------------------------- 1201 def saveFile(self): 1202 """ 1203 Save file 1204 """ 1205 if not self.opened: 1206 return 1207 1208 if self.filename != None and self.filename != '': 1209 file = open(self.filename,'w') 1210 text = self.textEdit.toPlainText() 1211 file.write(text) 1212 file.close() 1213 1214 self.updateFileState(True) 1215 1216 else: 1217 self.saveFileAs() 1218 # --------------------------------------------------------------- 1219 1220 1221 # --------------------------------------------------------------- 1222 def saveFileAs(self): 1223 """ 1224 Save file as 1225 """ 1226 if not self.opened: 1227 return 1228 1229 ret = QFileDialog.getSaveFileName(self, 'Save File') 1230 1231 if type(ret) == str: 1232 self.filename = ret 1233 elif type(ret) == tuple: 1234 self.filename = ret[0] 1235 else: 1236 raise Exception("Uknown return type for 'QFileDialog.getSaveFileName'") 1237 1238 self.last_dir = os.path.split(self.filename)[0] 1239 1240 if self.filename != None and self.filename != '': 1241 file = open(self.filename,'w') 1242 text = self.textEdit.toPlainText() 1243 file.write(text) 1244 file.close() 1245 1246 self.updateFileState(True) 1247 # --------------------------------------------------------------- 1248 1249 1250 # --------------------------------------------------------------- 1251 def closeOpenedFile(self): 1252 """ 1253 Close an opened file 1254 """ 1255 1256 if self.saved == False and self.readOnly == False: 1257 choice = QMessageBox.question(self, 'Built-in editor', 1258 'File changed.\nDo you want to save?', 1259 QMessageBox.Yes | QMessageBox.No) 1260 if choice == QMessageBox.Yes: 1261 self.saveFile() 1262 else: 1263 pass 1264 1265 self.saved = True 1266 self.opened = False 1267 1268 self.filename = '' 1269 self.textEdit.setPlainText('') 1270 # --------------------------------------------------------------- 1271 1272 1273 # --------------------------------------------------------------- 1274 def closeApplication(self): 1275 """ 1276 Close the editor 1277 """ 1278 if self.opened == True: 1279 choice = QMessageBox.question(self, 'Built-in editor', 1280 "Exit text editor?", 1281 QMessageBox.Yes | QMessageBox.No) 1282 else: 1283 choice = QMessageBox.Yes 1284 1285 if choice == QMessageBox.Yes: 1286 self.closeOpenedFile() 1287 1288 settings = QtCore.QSettings() 1289 settings.setValue("MainWindow/Geometry", 1290 self.saveGeometry()) 1291 1292 self.close() 1293 return 0 1294 else: 1295 return 1 1296 # --------------------------------------------------------------- 1297 1298 1299 # --------------------------------------------------------------- 1300 def closeEvent(self, event): 1301 1302 decision = self.closeApplication() 1303 if decision == 1: 1304 event.ignore() 1305 # --------------------------------------------------------------- 1306 1307 1308#------------------------------------------------------------------------------- 1309# END OF FILE 1310#------------------------------------------------------------------------------- 1311