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