class DebuggerWidget(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self.setWindowTitle("First Aid - Debugger")

        self.text_edits = {
        }  # fully expanded path of the file -> associated SourceWidget
        self.toolbar = self.addToolBar("General")
        self.toolbar.setObjectName("ToolbarGeneral")

        self.tab_widget = QTabWidget()
        self.tab_widget.setTabsClosable(True)
        self.tab_widget.tabCloseRequested.connect(self.on_tab_close_requested)
        self.tab_widget.currentChanged.connect(self.on_pos_changed)

        self.setCentralWidget(self.tab_widget)

        _icon = lambda x: QIcon(
            os.path.join(os.path.dirname(__file__), "icons", x + ".svg"))

        self.action_load = self.toolbar.addAction(_icon("folder-outline"),
                                                  "Load Python file (Ctrl+O)",
                                                  self.on_load)
        self.action_load.setShortcut("Ctrl+O")
        self.action_run = self.toolbar.addAction(_icon("run"),
                                                 "Run Python file (Ctrl+R)",
                                                 self.on_run)
        self.action_run.setShortcut("Ctrl+R")
        self.action_bp = self.toolbar.addAction(_icon("record"),
                                                "Toggle breakpoint (F9)",
                                                self.on_toggle_breakpoint)
        self.action_bp.setShortcut("F9")
        self.toolbar.addSeparator()
        self.action_continue = self.toolbar.addAction(_icon("play"),
                                                      "Continue (F5)",
                                                      self.on_continue)
        self.action_continue.setShortcut("F5")
        self.action_step_into = self.toolbar.addAction(
            _icon("debug-step-into"), "Step into (F11)", self.on_step_into)
        self.action_step_into.setShortcut("F11")
        self.action_step_over = self.toolbar.addAction(
            _icon("debug-step-over"), "Step over (F10)", self.on_step_over)
        self.action_step_over.setShortcut("F10")
        self.action_step_out = self.toolbar.addAction(_icon("debug-step-out"),
                                                      "Step out (Shift+F11)",
                                                      self.on_step_out)
        self.action_step_out.setShortcut("Shift+F11")
        self.action_run_to_cursor = self.toolbar.addAction(
            _icon("cursor-default-outline"), "Run to cursor (Ctrl+F10)",
            self.on_run_to_cursor)
        self.action_run_to_cursor.setShortcut("Ctrl+F10")

        self.vars_view = VariablesView()
        self.frames_view = FramesView()

        self.dock_frames = QDockWidget("Frames", self)
        self.dock_frames.setObjectName("DockFrames")
        self.dock_frames.setWidget(self.frames_view)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_frames)

        self.dock_vars = QDockWidget("Variables", self)
        self.dock_vars.setObjectName("DockVariables")
        self.dock_vars.setWidget(self.vars_view)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_vars)

        self.resize(800, 800)

        self.debugger = Debugger(self)

        self.update_buttons()

        settings = QSettings()
        self.restoreGeometry(
            settings.value("/plugins/firstaid/debugger-geometry", ''))
        self.restoreState(
            settings.value("/plugins/firstaid/debugger-windowstate", ''))

        filenames = settings.value("/plugins/firstaid/debugger-files", [])
        if filenames is None:
            filenames = []

        # load files from previous session
        for filename in filenames:
            self.load_file(filename)

        if self.tab_widget.count() > 1:
            self.tab_widget.setCurrentIndex(0)

        # start tracing
        self.start_tracing()

    def start_tracing(self):
        """ called from constructor or when the debugger window is opened again """
        sys.settrace(self.debugger.trace_function)

    def closeEvent(self, event):

        # disable tracing
        sys.settrace(None)

        settings = QSettings()
        settings.setValue("/plugins/firstaid/debugger-geometry",
                          self.saveGeometry())
        settings.setValue("/plugins/firstaid/debugger-windowstate",
                          self.saveState())

        filenames = self.text_edits.keys()
        settings.setValue("/plugins/firstaid/debugger-files", filenames)

        QMainWindow.closeEvent(self, event)

    def load_file(self, filename):
        filename = os.path.realpath(filename)

        if filename in self.text_edits:
            self.switch_to_file(filename)
            return  # already there...
        try:
            self.text_edits[filename] = SourceWidget(filename)
        except IOError:
            # TODO: display warning we failed to read the file
            return
        tab_text = os.path.basename(filename)
        self.tab_widget.addTab(self.text_edits[filename], tab_text)
        self.tab_widget.setTabToolTip(self.tab_widget.count() - 1, filename)
        self.tab_widget.setCurrentWidget(self.text_edits[filename])
        self.text_edits[filename].cursorPositionChanged.connect(
            self.on_pos_changed)
        self.on_pos_changed()

    def switch_to_file(self, filename):
        if filename in self.text_edits:
            self.tab_widget.setCurrentWidget(self.text_edits[filename])

    def unload_file(self, filename):
        for index in xrange(self.tab_widget.count()):
            if self.text_edits[filename] == self.tab_widget.widget(index):
                self.tab_widget.removeTab(index)
                del self.text_edits[filename]
                break

    def on_load(self):

        settings = QSettings()
        folder = settings.value("firstaid/lastFolder", '')

        filename = QFileDialog.getOpenFileName(self, "Load", folder,
                                               "Python files (*.py)")
        if filename == '':
            return

        settings.setValue("firstaid/lastFolder", os.path.dirname(filename))

        self.load_file(filename)

    def on_tab_close_requested(self, index):
        self.unload_file(self.tab_widget.widget(index).filename)

    def on_pos_changed(self):
        if not self.current_text_edit():
            self.statusBar().showMessage("[no file]")
            return
        c = self.current_text_edit().textCursor()
        line = c.blockNumber() + 1
        col = c.positionInBlock() + 1
        self.statusBar().showMessage("%d:%d" % (line, col))

    def on_run(self):
        globals = None
        locals = None
        if globals is None:
            import __main__
            globals = __main__.__dict__
        if locals is None:
            locals = globals
        execfile(self.tab_widget.currentWidget().filename, globals, locals)

    def current_text_edit(self):
        return self.tab_widget.currentWidget()

    def on_toggle_breakpoint(self):
        if self.current_text_edit():
            self.current_text_edit().toggle_breakpoint()

    def update_buttons(self):
        active = self.debugger.stopped
        self.action_step_into.setEnabled(active)
        self.action_step_over.setEnabled(active)
        self.action_step_out.setEnabled(active)
        self.action_run_to_cursor.setEnabled(active)
        self.action_continue.setEnabled(active)

    def on_step_into(self):
        self.debugger.stepping = True
        self.debugger.next_step = None
        self.debugger.ev_loop.exit(0)

    def on_step_over(self):
        self.debugger.stepping = True
        self.debugger.next_step = (
            'over', self.debugger.current_frame.f_code.co_filename,
            self.debugger.current_frame.f_lineno)
        self.debugger.ev_loop.exit(0)

    def on_step_out(self):
        self.debugger.stepping = True
        self.debugger.next_step = ('out',
                                   frame_depth(self.debugger.current_frame))
        self.debugger.ev_loop.exit(0)

    def on_run_to_cursor(self):
        self.debugger.stepping = True
        filename = self.tab_widget.currentWidget().filename
        line_no = self.tab_widget.currentWidget().textCursor().blockNumber(
        ) + 1
        self.debugger.next_step = ('at', filename, line_no)
        self.debugger.ev_loop.exit(0)

    def on_continue(self):
        self.debugger.stepping = False
        self.current_text_edit().debug_line = -1
        self.current_text_edit().update_highlight()
        self.vars_view.setVariables({})
        self.frames_view.setTraceback(None)
        self.debugger.ev_loop.exit(0)
class DebugWidget(QWidget):
    def __init__(self, exc_info, parent=None):
        QWidget.__init__(self, parent)

        etype, value, tb = exc_info

        self.tb = tb
        self.entries = traceback.extract_tb(tb)

        self.setWindowTitle('Python Error')

        msg = unicode(value).replace("\n", "<br>").replace(" ", "&nbsp;")
        self.error = QLabel("<h1>" + etype.__name__ + "</h1><b>" + msg +
                            "</b>")
        self.error.setTextInteractionFlags(Qt.TextSelectableByMouse)

        self.frames = FramesView()
        self.frames.setTraceback(tb)
        self.frames.selectionModel().currentChanged.connect(
            self.current_frame_changed)

        self.source = SourceView()

        self.splitterSrc = QSplitter(Qt.Horizontal)
        self.splitterSrc.addWidget(self.frames)
        self.splitterSrc.addWidget(self.source)
        self.splitterSrc.setStretchFactor(0, 1)
        self.splitterSrc.setStretchFactor(1, 2)

        self.variables = VariablesView()

        self.console = ConsoleWidget(exc_info)

        self.splitterMain = QSplitter(Qt.Vertical)
        self.splitterMain.addWidget(self.splitterSrc)
        self.splitterMain.addWidget(self.variables)
        self.splitterMain.addWidget(self.console)

        l = QVBoxLayout()
        l.addWidget(self.error)
        l.addWidget(self.splitterMain)
        self.setLayout(l)

        self.resize(800, 600)

        s = QSettings()
        self.splitterSrc.restoreState(s.value("/FirstAid/splitterSrc", ""))
        self.splitterMain.restoreState(s.value("/FirstAid/splitterMain", ""))
        self.restoreGeometry(s.value("/FirstAid/geometry", ""))

        # select the last frame
        self.frames.setCurrentIndex(
            self.frames.model().index(len(self.entries) - 1))

    def closeEvent(self, event):
        s = QSettings()
        s.setValue("/FirstAid/splitterSrc", self.splitterSrc.saveState())
        s.setValue("/FirstAid/splitterMain", self.splitterMain.saveState())
        s.setValue("/FirstAid/geometry", self.saveGeometry())
        QWidget.closeEvent(self, event)

    def current_frame_changed(self, current, previous):
        row = current.row()
        if row >= 0 and row < len(self.entries):
            self.go_to_frame(row)

    def go_to_frame(self, index):

        filename = self.entries[index][0]
        lineno = self.entries[index][1]

        self.source.openFile(filename)
        self.source.jumpToLine(lineno)

        local_vars = frame_from_traceback(self.tb, index).f_locals
        self.variables.setVariables(local_vars)

        self.console.go_to_frame(index)
class DebuggerWidget(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)

        self.setWindowTitle("First Aid - Debugger")

        self.text_edits = {}
        self.toolbar = self.addToolBar("General")
        self.toolbar.setObjectName("ToolbarGeneral")

        self.tab_widget = QTabWidget()
        self.tab_widget.setTabsClosable(True)
        self.tab_widget.tabCloseRequested.connect(self.on_tab_close_requested)
        self.tab_widget.currentChanged.connect(self.on_pos_changed)

        self.setCentralWidget(self.tab_widget)

        _icon = lambda x: QIcon(os.path.join(os.path.dirname(__file__), "icons", x+".svg"))

        self.action_load = self.toolbar.addAction(_icon("folder-outline"), "Load Python file (Ctrl+O)", self.on_load)
        self.action_load.setShortcut("Ctrl+O")
        self.action_run = self.toolbar.addAction(_icon("run"), "Run Python file (Ctrl+R)", self.on_run)
        self.action_run.setShortcut("Ctrl+R")
        self.action_bp = self.toolbar.addAction(_icon("record"), "Toggle breakpoint (F9)", self.on_toggle_breakpoint)
        self.action_bp.setShortcut("F9")
        self.toolbar.addSeparator()
        self.action_continue = self.toolbar.addAction(_icon("play"), "Continue (F5)", self.on_continue)
        self.action_continue.setShortcut("F5")
        self.action_step_into = self.toolbar.addAction(_icon("debug-step-into"), "Step into (F11)", self.on_step_into)
        self.action_step_into.setShortcut("F11")
        self.action_step_over = self.toolbar.addAction(_icon("debug-step-over"), "Step over (F10)", self.on_step_over)
        self.action_step_over.setShortcut("F10")
        self.action_step_out = self.toolbar.addAction(_icon("debug-step-out"), "Step out (Shift+F11)", self.on_step_out)
        self.action_step_out.setShortcut("Shift+F11")
        self.action_run_to_cursor = self.toolbar.addAction(_icon("cursor-default-outline"), "Run to cursor (Ctrl+F10)", self.on_run_to_cursor)
        self.action_run_to_cursor.setShortcut("Ctrl+F10")

        self.vars_view = VariablesView()
        self.frames_view = FramesView()

        self.dock_frames = QDockWidget("Frames", self)
        self.dock_frames.setObjectName("DockFrames")
        self.dock_frames.setWidget(self.frames_view)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_frames)

        self.dock_vars = QDockWidget("Variables", self)
        self.dock_vars.setObjectName("DockVariables")
        self.dock_vars.setWidget(self.vars_view)
        self.addDockWidget(Qt.BottomDockWidgetArea, self.dock_vars)

        self.resize(800,800)

        self.debugger = Debugger(self)

        self.update_buttons()

        settings = QSettings()
        self.restoreGeometry(settings.value("/plugins/firstaid/debugger-geometry", ''))
        self.restoreState(settings.value("/plugins/firstaid/debugger-windowstate", ''))

        filenames = settings.value("/plugins/firstaid/debugger-files", [])
        if filenames is None:
            filenames = []

        # load files from previous session
        for filename in filenames:
            self.load_file(filename)

        if self.tab_widget.count() > 1:
            self.tab_widget.setCurrentIndex(0)

        # start tracing
        self.start_tracing()

    def start_tracing(self):
        """ called from constructor or when the debugger window is opened again """
        sys.settrace(self.debugger.trace_function)

    def closeEvent(self, event):

        # disable tracing
        sys.settrace(None)

        settings = QSettings()
        settings.setValue("/plugins/firstaid/debugger-geometry", self.saveGeometry())
        settings.setValue("/plugins/firstaid/debugger-windowstate", self.saveState())

        filenames = self.text_edits.keys()
        settings.setValue("/plugins/firstaid/debugger-files", filenames)

        QMainWindow.closeEvent(self, event)


    def load_file(self, filename):
        if filename in self.text_edits:
            self.switch_to_file(filename)
            return   # already there...
        try:
            self.text_edits[filename] = SourceWidget(filename)
        except IOError:
            # TODO: display warning we failed to read the file
            return
        tab_text = os.path.basename(filename)
        self.tab_widget.addTab(self.text_edits[filename], tab_text)
        self.tab_widget.setTabToolTip(self.tab_widget.count()-1, filename)
        self.tab_widget.setCurrentWidget(self.text_edits[filename])
        self.text_edits[filename].cursorPositionChanged.connect(self.on_pos_changed)
        self.on_pos_changed()

    def switch_to_file(self, filename):
        if filename in self.text_edits:
            self.tab_widget.setCurrentWidget(self.text_edits[filename])

    def unload_file(self, filename):
        for index in xrange(self.tab_widget.count()):
            if self.text_edits[filename] == self.tab_widget.widget(index):
                self.tab_widget.removeTab(index)
                del self.text_edits[filename]
                break

    def on_load(self):

        settings = QSettings()
        folder = settings.value("firstaid/lastFolder", '')

        filename = QFileDialog.getOpenFileName(self, "Load", folder, "Python files (*.py)")
        if filename == '':
            return

        settings.setValue("firstaid/lastFolder", os.path.dirname(filename))

        self.load_file(filename)

    def on_tab_close_requested(self, index):
        self.unload_file(self.tab_widget.widget(index).filename)

    def on_pos_changed(self):
        if not self.current_text_edit():
            self.statusBar().showMessage("[no file]")
            return
        c = self.current_text_edit().textCursor()
        line = c.blockNumber() + 1
        col = c.positionInBlock() + 1
        self.statusBar().showMessage("%d:%d" % (line, col))

    def on_run(self):
        globals = None
        locals = None
        if globals is None:
            import __main__
            globals = __main__.__dict__
        if locals is None:
            locals = globals
        execfile(self.tab_widget.currentWidget().filename, globals, locals)

    def current_text_edit(self):
        return self.tab_widget.currentWidget()

    def on_toggle_breakpoint(self):
        if self.current_text_edit():
            self.current_text_edit().toggle_breakpoint()

    def update_buttons(self):
        active = self.debugger.stopped
        self.action_step_into.setEnabled(active)
        self.action_step_over.setEnabled(active)
        self.action_step_out.setEnabled(active)
        self.action_run_to_cursor.setEnabled(active)
        self.action_continue.setEnabled(active)


    def on_step_into(self):
        self.debugger.stepping = True
        self.debugger.next_step = None
        self.debugger.ev_loop.exit(0)

    def on_step_over(self):
        self.debugger.stepping = True
        self.debugger.next_step = ('over', self.debugger.current_frame.f_code.co_filename, self.debugger.current_frame.f_lineno)
        self.debugger.ev_loop.exit(0)

    def on_step_out(self):
        self.debugger.stepping = True
        self.debugger.next_step = ('out', frame_depth(self.debugger.current_frame))
        self.debugger.ev_loop.exit(0)

    def on_run_to_cursor(self):
        self.debugger.stepping = True
        filename = self.tab_widget.currentWidget().filename
        line_no = self.tab_widget.currentWidget().textCursor().blockNumber() + 1
        self.debugger.next_step = ('at', filename, line_no)
        self.debugger.ev_loop.exit(0)

    def on_continue(self):
        self.debugger.stepping = False
        self.current_text_edit().debug_line = -1
        self.current_text_edit().update_highlight()
        self.vars_view.setVariables({})
        self.frames_view.setTraceback(None)
        self.debugger.ev_loop.exit(0)
class DebugWidget(QWidget):
    def __init__(self, exc_info, parent=None):
        QWidget.__init__(self, parent)

        etype, value, tb = exc_info

        self.tb = tb
        self.entries = traceback.extract_tb(tb)

        self.setWindowTitle('Python Error')

        msg = unicode(value).replace("\n", "<br>").replace(" ", "&nbsp;")
        self.error = QLabel("<h1>"+etype.__name__+"</h1><b>"+msg+"</b>")
        self.error.setTextInteractionFlags(Qt.TextSelectableByMouse)

        self.frames = FramesView()
        self.frames.setTraceback(tb)
        self.frames.selectionModel().currentChanged.connect(self.current_frame_changed)

        self.source = SourceView()

        self.splitterSrc = QSplitter(Qt.Horizontal)
        self.splitterSrc.addWidget(self.frames)
        self.splitterSrc.addWidget(self.source)
        self.splitterSrc.setStretchFactor(0, 1)
        self.splitterSrc.setStretchFactor(1, 2)

        self.variables = VariablesView()

        self.console = ConsoleWidget(exc_info)

        self.splitterMain = QSplitter(Qt.Vertical)
        self.splitterMain.addWidget(self.splitterSrc)
        self.splitterMain.addWidget(self.variables)
        self.splitterMain.addWidget(self.console)

        l = QVBoxLayout()
        l.addWidget(self.error)
        l.addWidget(self.splitterMain)
        self.setLayout(l)

        self.resize(800,600)

        s = QSettings()
        self.splitterSrc.restoreState(s.value("/FirstAid/splitterSrc", ""))
        self.splitterMain.restoreState(s.value("/FirstAid/splitterMain", ""))
        self.restoreGeometry(s.value("/FirstAid/geometry", ""))

        # select the last frame
        self.frames.setCurrentIndex(self.frames.model().index(len(self.entries)-1))

    def closeEvent(self, event):
        s = QSettings()
        s.setValue("/FirstAid/splitterSrc", self.splitterSrc.saveState())
        s.setValue("/FirstAid/splitterMain", self.splitterMain.saveState())
        s.setValue("/FirstAid/geometry", self.saveGeometry())
        QWidget.closeEvent(self, event)

    def current_frame_changed(self, current, previous):
        row = current.row()
        if row >= 0 and row < len(self.entries):
            self.go_to_frame(row)

    def go_to_frame(self, index):

        filename = self.entries[index][0]
        lineno = self.entries[index][1]

        self.source.openFile(filename)
        self.source.jumpToLine(lineno)

        local_vars = frame_from_traceback(self.tb, index).f_locals
        self.variables.setVariables(local_vars)

        self.console.go_to_frame(index)