Ejemplo n.º 1
0
    def __init__(self, kataja_app, tree='', plugin='', image_out='', no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        silent = bool(image_out)
        self.init_done = False
        self._stored_init_state = True
        self.disable_signaling()
        self.outgoing = []
        kataja_app.processEvents()

        self.use_tooltips = True
        self.setWindowTitle("Kataja")
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner, QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1024, 800)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.classes = classes
        self.save_prefs = not no_prefs
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager(self)
        self.plugin_manager = PluginManager()
        self.document = None
        ctrl.late_init(self)  # sets ctrl.main
        capture_stdout(log, self.log_stdout_as_debug, ctrl)
        classes.late_init()  # make all default classes available
        prefs.import_node_classes(classes)  # add node styles defined at class to prefs
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.plugin_manager.find_plugins(prefs.plugins_path or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.print_manager = PrintManager()
        self.view_manager = ViewManager()
        self.graph_scene = GraphScene()
        self.recorder = Recorder(self.graph_scene)
        self.graph_view = GraphView(self.graph_scene)
        self.view_manager.late_init(self.graph_scene, self.graph_view)
        self.ui_manager = UIManager(self)
        if not silent:
            self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.create_default_document()
        self.color_manager.update_custom_colors()
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        if not silent:
            self.setCentralWidget(self.graph_view)
            self.setGeometry(x, y, w, h)
            self.show()
            self.raise_()
            kataja_app.processEvents()
            self.activateWindow()
        if tree:
            plugin = plugin or 'FreeDrawing'
        else:
            plugin = plugin or prefs.active_plugin_name or 'FreeDrawing'
        self.plugin_manager.enable_plugin(plugin)
        self.document.load_default_forests(tree=tree)
        self.document.play = not silent
        if not silent:
            self.enable_signaling()
            self.viewport_resized.emit()
            self.forest_changed.emit()
            self.action_finished(undoable=False, play=True)
            if self.forest:
                self.forest.undo_manager.flush_pile()
        else:
            self.action_finished(undoable=False, play=False)
        #gestures = [QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture, QtCore.Qt.PanGesture,
        #            QtCore.Qt.PinchGesture, QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture]
        # for gesture in gestures:
        #    self.grabGesture(gesture)

        if image_out:
            self.print_manager.print_all(running_environment.default_userspace_path, image_out)
            quit()
Ejemplo n.º 2
0
class KatajaMain(QtWidgets.QMainWindow):
    """ Qt's main window. When this is closed, application closes. Graphics are
    inside this, in scene objects with view widgets. This window also manages
    keypresses and menus. """

    active_edge_color_changed = QtCore.pyqtSignal()
    color_themes_changed = QtCore.pyqtSignal()
    document_changed = QtCore.pyqtSignal()
    forest_changed = QtCore.pyqtSignal()
    parse_changed = QtCore.pyqtSignal()
    palette_changed = QtCore.pyqtSignal()
    scope_changed = QtCore.pyqtSignal()
    selection_changed = QtCore.pyqtSignal()
    ui_font_changed = QtCore.pyqtSignal()
    viewport_moved = QtCore.pyqtSignal()
    viewport_resized = QtCore.pyqtSignal()
    visualisation_changed = QtCore.pyqtSignal()

    def __init__(self, kataja_app, tree='', plugin='', image_out='', no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        silent = bool(image_out)
        self.init_done = False
        self._stored_init_state = True
        self.disable_signaling()
        self.outgoing = []
        kataja_app.processEvents()

        self.use_tooltips = True
        self.setWindowTitle("Kataja")
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner, QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1024, 800)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.classes = classes
        self.save_prefs = not no_prefs
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager(self)
        self.plugin_manager = PluginManager()
        self.document = None
        ctrl.late_init(self)  # sets ctrl.main
        capture_stdout(log, self.log_stdout_as_debug, ctrl)
        classes.late_init()  # make all default classes available
        prefs.import_node_classes(classes)  # add node styles defined at class to prefs
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.plugin_manager.find_plugins(prefs.plugins_path or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.print_manager = PrintManager()
        self.view_manager = ViewManager()
        self.graph_scene = GraphScene()
        self.recorder = Recorder(self.graph_scene)
        self.graph_view = GraphView(self.graph_scene)
        self.view_manager.late_init(self.graph_scene, self.graph_view)
        self.ui_manager = UIManager(self)
        if not silent:
            self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.create_default_document()
        self.color_manager.update_custom_colors()
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        if not silent:
            self.setCentralWidget(self.graph_view)
            self.setGeometry(x, y, w, h)
            self.show()
            self.raise_()
            kataja_app.processEvents()
            self.activateWindow()
        if tree:
            plugin = plugin or 'FreeDrawing'
        else:
            plugin = plugin or prefs.active_plugin_name or 'FreeDrawing'
        self.plugin_manager.enable_plugin(plugin)
        self.document.load_default_forests(tree=tree)
        self.document.play = not silent
        if not silent:
            self.enable_signaling()
            self.viewport_resized.emit()
            self.forest_changed.emit()
            self.action_finished(undoable=False, play=True)
            if self.forest:
                self.forest.undo_manager.flush_pile()
        else:
            self.action_finished(undoable=False, play=False)
        #gestures = [QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture, QtCore.Qt.PanGesture,
        #            QtCore.Qt.PinchGesture, QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture]
        # for gesture in gestures:
        #    self.grabGesture(gesture)

        if image_out:
            self.print_manager.print_all(running_environment.default_userspace_path, image_out)
            quit()

    @property
    def forest(self):
        return self.document.forest

    def update_style_sheet(self):
        c = ctrl.cm.drawing()
        ui = ctrl.cm.ui()
        f = qt_prefs.get_font(g.UI_FONT)
        fm = qt_prefs.get_font(g.MAIN_FONT)
        fc = qt_prefs.get_font(g.CONSOLE_FONT)
        self.setStyleSheet(stylesheet % {
            'draw': c.name(),
            'lighter': c.lighter().name(),
            'paper': ctrl.cm.paper().name(),
            'ui': ui.name(),
            'ui_lighter': ui.lighter().name(),
            'ui_font': f.family(),
            'ui_font_size': f.pointSize(),
            'ui_font_larger': int(f.pointSize() * 1.2),
            'ui_darker': ui.darker().name(),
            'main_font': fm.family(),
            'main_font_size': fm.pointSize(),
            'heading_font_size': fm.pointSize() * 2,
            'console_font': fc.family(),
            'console_font_size': fc.pointSize(),
        })

    def leaveEvent(self, event):
        ctrl.ui.force_hide_help()

    def disable_signaling(self):
        # shut down side effects
        self._stored_init_state = self.init_done
        self.init_done = False
        ctrl.disable_undo()
        self.blockSignals(True)
        # ----------------------

    def enable_signaling(self):
        # resume with side effects
        self.blockSignals(False)
        ctrl.resume_undo()
        self.init_done = self._stored_init_state
        # ----------------------


    # Preferences ###################################

    def reset_preferences(self):
        """

        :return:
        """
        prefs.restore_default_preferences(qt_prefs, running_environment, classes, log)
        self.color_themes_changed.emit()
        if self.ui_manager.preferences_dialog:
            self.ui_manager.preferences_dialog.close()
        self.ui_manager.preferences_dialog = PreferencesDialog(self)
        self.ui_manager.preferences_dialog.open()
        self.ui_manager.preferences_dialog.trigger_all_updates()

    # Document / Project #########################

    def start_new_document(self, name='Example'):
        document = classes.KatajaDocument(name=name)
        self.set_document(document)
        return document

    def set_document(self, document):
        if document is not self.document:
            if self.document:
                self.document.retire_from_display()
            self.document = document
            self.document_changed.emit()
            if document:
                document.update_forest()
                plug = f'{prefs.active_plugin_name} — ' if prefs.active_plugin_name else ''
                self.setWindowTitle(f'Kataja — {plug}{document.name}')

    def create_default_document(self):
        """ Put empty Kataja document in place -- you want to do this after
        plugins have changed the classes that implement these.
        :return:
        """
        self.start_new_document()

    def clear_document(self):
        """ Empty everything - maybe necessary before changing plugin """
        self.set_document(None)

    # ### Visualization
    # #############################################################

    def redraw(self):
        """ Call for forest redraw
        :return: None
        """
        if self.forest and self.forest.in_display:
            self.forest.draw()

    def log_stdout_as_debug(self, text):
        self.outgoing.append(text)
        if text.endswith('\n') or text.endswith('\r'):
            log.debug((''.join(self.outgoing)).strip())
            self.outgoing = []

    def attach_widget_to_log_handler(self, browserwidget):
        """ This has to be done once: we have a logger set up before there is any output widget,
        once the widget is created it is connected to logger.
        :param browserwidget:
        :return:
        """
        self.app.log_handler.set_widget(browserwidget)

    # ## Actions #######################################################

    def action_finished(self, m='', undoable=True, error=None, play=False):
        """ Write action to undo stack, report back to user and redraw trees
        if necessary
        :param m: message for undo
        :param undoable: are we supposed to take a snapshot of changes after
        this action.
        :param error message
        """
        if error:
            log.error(error)
        elif m:
            log.info(m)
        if self.forest:
            if ctrl.action_redraw:
                self.forest.draw()
            if undoable and not error:
                self.forest.undo_manager.take_snapshot(m)
        if play:
            self.graph_scene.start_animations()
        ctrl.ui.update_actions()

    def trigger_action(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        if self.init_done:
            action = self.ui_manager.actions[name]
            action.run_command(*args, **kwargs)

    def trigger_but_suppress_undo(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.trigger_but_suppress_undo(*args, **kwargs)

    def enable_actions(self):
        """ Restores menus """
        for action in self.ui_manager.actions.values():
            action.setDisabled(False)

    def disable_actions(self):
        """ Actions shouldn't be initiated when there is other multi-phase
        action going on """
        for action in self.ui_manager.actions.values():
            action.setDisabled(True)

    # Color theme #################################

    def change_color_theme(self, mode, force=False):
        """
        triggered by color mode selector in colors panel

        :param mode:
        """
        if self.document:
            if mode != self.document.settings.get('color_theme') or force:
                self.document.settings.set('color_theme', mode)
                self.update_colors()


    # Not called from anywhere yet, but useful
    def release_selected(self, **kw):
        """

        :param kw:
        :return:
        """
        for node in ctrl.get_selected_nodes():
            node.release()
        self.action_finished()
        return True

    # ## Other window events
    ###################################################

    def timerEvent(self, event):
        """ Timer event only for printing, for 'snapshot' effect
        :param event:
        """
        self.print_manager.snapframe_timer(event)

    def closeEvent(self, event):
        """ Shut down the program, give some debug info
        :param event:
        """
        QtWidgets.QMainWindow.closeEvent(self, event)
        if ctrl.print_garbage:
            # import objgraph
            log.debug('garbage stats: ' + str(gc.get_count()))
            gc.collect()
            log.debug('after collection: ' + str(gc.get_count()))
            if gc.garbage:
                log.debug('garbage: ' + str(gc.garbage))

                # objgraph.show_most_common_types(limit =40)
        if self.save_prefs:
            prefs.save_preferences()
        log.info('...done')

    def update_colors(self, randomise=False, animate=True):
        """ This is the master palette change.
        Its effects should propagate to all objects in scene and ui, either through updated
        style sheets, paletteChanged -events or 'palette_changed' -signals.
        :param randomise:
        :param animate:
        :return:
        """
        cm = self.color_manager
        old_gradient_base = cm.paper()
        cm.update_colors(randomise=randomise)
        self.app.setPalette(cm.get_qt_palette())
        self.update_style_sheet()
        ctrl.main.palette_changed.emit()
        if cm.gradient:
            if old_gradient_base != cm.paper() and animate:
                self.graph_scene.fade_background_gradient(old_gradient_base, cm.paper())
            else:
                self.graph_scene.setBackgroundBrush(cm.gradient)
        else:
            self.graph_scene.setBackgroundBrush(qt_prefs.no_brush)
        self.update()

    ### Applying specific preferences globally.
    # These are on_change -methods for various preferences -- these are called if changing a
    # preference should have immediate consequences. They are hosted here because they need to
    # have access to prefs, qt_prefs, main etc.

    def prepare_easing_curve(self):
        qt_prefs.prepare_easing_curve(prefs.curve, prefs.move_frames)

    def update_color_theme(self):
        self.change_color_theme(prefs.color_theme, force=True)

    def update_visualization(self):
        self.forest.set_visualization(prefs.visualization)
        self.redraw()

    def resize_ui_font(self):
        qt_prefs.toggle_large_ui_font(prefs.large_ui_text, prefs.fonts)
        self.update_style_sheet()
Ejemplo n.º 3
0
    def __init__(self, kataja_app, no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        kataja_app.processEvents()
        SavedObject.__init__(self)
        self.use_tooltips = True
        self.available_plugins = {}
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner,
                       QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner,
                       QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1152, 720)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.save_prefs = not no_prefs
        self.forest = None
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager()
        self.settings_manager = Settings()
        self.forest_keepers = []
        self.forest_keeper = None
        ctrl.late_init(self)
        classes.late_init()
        prefs.import_node_classes(classes)
        self.syntax = SyntaxConnection()
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.settings_manager.set_prefs(prefs)
        self.color_manager.update_custom_colors()
        self.find_plugins(prefs.plugins_path
                          or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.graph_scene = GraphScene(main=self, graph_view=None)
        self.graph_view = GraphView(main=self, graph_scene=self.graph_scene)
        self.graph_scene.graph_view = self.graph_view
        self.ui_manager = UIManager(self)
        self.settings_manager.set_ui_manager(self.ui_manager)
        self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.init_forest_keepers()
        self.settings_manager.set_document(self.forest_keeper)
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.forest = Forest()
        self.settings_manager.set_forest(self.forest)
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        self.setCentralWidget(self.graph_view)
        self.setGeometry(x, y, w, h)
        self.setWindowTitle(self.tr("Kataja"))
        self.print_started = False
        self.show()
        self.raise_()
        kataja_app.processEvents()
        self.activateWindow()
        self.status_bar = self.statusBar()
        self.install_plugins()
        self.load_initial_treeset()
        log.info('Welcome to Kataja! (h) for help')
        #ctrl.call_watchers(self.forest_keeper, 'forest_changed')
        # toolbar = QtWidgets.QToolBar()
        # toolbar.setFixedSize(480, 40)
        # self.addToolBar(toolbar)
        gestures = [
            QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture,
            QtCore.Qt.PanGesture, QtCore.Qt.PinchGesture,
            QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture
        ]
        #for gesture in gestures:
        #    self.grabGesture(gesture)
        self.action_finished(undoable=False)
        self.forest.undo_manager.flush_pile()
Ejemplo n.º 4
0
class KatajaMain(SavedObject, QtWidgets.QMainWindow):
    """ Qt's main window. When this is closed, application closes. Graphics are
    inside this, in scene objects with view widgets. This window also manages
    keypresses and menus. """
    unique = True

    def __init__(self, kataja_app, no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        kataja_app.processEvents()
        SavedObject.__init__(self)
        self.use_tooltips = True
        self.available_plugins = {}
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner,
                       QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner,
                       QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1152, 720)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.save_prefs = not no_prefs
        self.forest = None
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager()
        self.settings_manager = Settings()
        self.forest_keepers = []
        self.forest_keeper = None
        ctrl.late_init(self)
        classes.late_init()
        prefs.import_node_classes(classes)
        self.syntax = SyntaxConnection()
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.settings_manager.set_prefs(prefs)
        self.color_manager.update_custom_colors()
        self.find_plugins(prefs.plugins_path
                          or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.graph_scene = GraphScene(main=self, graph_view=None)
        self.graph_view = GraphView(main=self, graph_scene=self.graph_scene)
        self.graph_scene.graph_view = self.graph_view
        self.ui_manager = UIManager(self)
        self.settings_manager.set_ui_manager(self.ui_manager)
        self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.init_forest_keepers()
        self.settings_manager.set_document(self.forest_keeper)
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.forest = Forest()
        self.settings_manager.set_forest(self.forest)
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        self.setCentralWidget(self.graph_view)
        self.setGeometry(x, y, w, h)
        self.setWindowTitle(self.tr("Kataja"))
        self.print_started = False
        self.show()
        self.raise_()
        kataja_app.processEvents()
        self.activateWindow()
        self.status_bar = self.statusBar()
        self.install_plugins()
        self.load_initial_treeset()
        log.info('Welcome to Kataja! (h) for help')
        #ctrl.call_watchers(self.forest_keeper, 'forest_changed')
        # toolbar = QtWidgets.QToolBar()
        # toolbar.setFixedSize(480, 40)
        # self.addToolBar(toolbar)
        gestures = [
            QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture,
            QtCore.Qt.PanGesture, QtCore.Qt.PinchGesture,
            QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture
        ]
        #for gesture in gestures:
        #    self.grabGesture(gesture)
        self.action_finished(undoable=False)
        self.forest.undo_manager.flush_pile()

    def update_style_sheet(self):
        c = ctrl.cm.drawing()
        ui = ctrl.cm.ui()
        f = qt_prefs.get_font(g.UI_FONT)
        self.setStyleSheet(
            stylesheet % {
                'draw': c.name(),
                'lighter': c.lighter().name(),
                'paper': ctrl.cm.paper().name(),
                'ui': ui.name(),
                'ui_lighter': ui.lighter().name(),
                'ui_font': f.family(),
                'ui_font_size': f.pointSize(),
                'ui_font_larger': int(f.pointSize() * 1.2),
                'ui_darker': ui.darker().name()
            })

    def find_plugins(self, plugins_path):
        """ Find the plugins dir for the running configuration and read the metadata of plugins.
        Don't try to load actual python code yet
        :return: None
        """
        if not plugins_path:
            return
        self.available_plugins = {}
        plugins_path = os.path.normpath(plugins_path)
        os.makedirs(plugins_path, exist_ok=True)
        sys.path.append(plugins_path)
        base_ends = len(plugins_path.split('/'))
        for root, dirs, files in os.walk(plugins_path):
            path_parts = root.split('/')
            if len(path_parts) == base_ends + 1 and not path_parts[base_ends].startswith('__') \
                    and 'plugin.json' in files:
                success = False
                try:
                    plugin_file = open(root + '/plugin.json', 'r')
                    data = json.load(plugin_file)
                    plugin_file.close()
                    success = True
                except:
                    log.error(sys.exc_info())
                    print(sys.exc_info())
                if success:
                    mod_name = path_parts[base_ends]
                    data['module_name'] = mod_name
                    data['module_path'] = root
                    self.available_plugins[mod_name] = data

    def enable_plugin(self, plugin_key, reload=False):
        """ Start one plugin: save data, replace required classes with plugin classes, load data.

        """
        self.active_plugin_setup = self.load_plugin(plugin_key)
        if not self.active_plugin_setup:
            return
        self.clear_all()
        ctrl.disable_undo()
        if reload:
            available = []
            for key in sys.modules:
                if key.startswith(plugin_key):
                    available.append(key)
            if getattr(self.active_plugin_setup, 'reload_order', None):
                to_reload = [
                    x for x in self.active_plugin_setup.reload_order
                    if x in available
                ]
            else:
                to_reload = sorted(available)
            for mod_name in to_reload:
                importlib.reload(sys.modules[mod_name])
                print('reloaded ', mod_name)
                log.info('reloaded module %s' % mod_name)

        if hasattr(self.active_plugin_setup, 'plugin_parts'):
            for classobj in self.active_plugin_setup.plugin_parts:
                base_class = classes.find_base_model(classobj)
                if base_class:
                    classes.add_mapping(base_class, classobj)
                    m = "replacing %s with %s " % (base_class.__name__,
                                                   classobj.__name__)
                else:
                    m = "adding %s " % classobj.__name__
                log.info(m)
                print(m)
        if hasattr(self.active_plugin_setup, 'help_file'):
            dir_path = os.path.dirname(
                os.path.realpath(self.active_plugin_setup.__file__))
            print(dir_path)
            self.ui_manager.set_help_source(dir_path,
                                            self.active_plugin_setup.help_file)
        if hasattr(self.active_plugin_setup, 'start_plugin'):
            self.active_plugin_setup.start_plugin(self, ctrl, prefs)
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = plugin_key

    def disable_current_plugin(self):
        """ Disable the current plugin and load the default trees instead.
        :param clear: if True, have empty treeset, if False, try to load default kataja treeset."""
        if not self.active_plugin_setup:
            return
        ctrl.disable_undo()
        if hasattr(self.active_plugin_setup, 'tear_down_plugin'):
            self.active_plugin_setup.tear_down_plugin(self, ctrl, prefs)
        self.clear_all()
        # print(classes.base_name_to_plugin_class)
        # if hasattr(self.active_plugin_setup, 'plugin_parts'):
        #     for classobj in self.active_plugin_setup.plugin_parts:
        #         class_name = classobj.__name__
        #         if class_name:
        #             log.info(f'removing {class_name}')
        #             print(f'removing {class_name}')
        #             classes.remove_class(class_name)
        classes.restore_default_classes()
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = ''

    def load_plugin(self, plugin_module):
        setup = None
        importlib.invalidate_caches()
        if plugin_module in self.available_plugins:
            retry = True
            while retry:
                try:
                    setup = importlib.import_module(plugin_module + ".setup")
                    retry = False
                except:
                    e = sys.exc_info()
                    error_dialog = ErrorDialog(self)
                    error_dialog.set_error(
                        '%s, line %s\n%s: %s' %
                        (plugin_module + ".setup.py", e[2].tb_lineno,
                         e[0].__name__, e[1]))
                    error_dialog.set_traceback(traceback.format_exc())
                    retry = error_dialog.exec_()
                    setup = None
        return setup

    def install_plugins(self):
        """ If there are plugins defined in preferences to be used, activate them now.
        :return: None
        """
        if prefs.active_plugin_name:
            log.info('Installing plugin %s...' % prefs.active_plugin_name)
            self.enable_plugin(prefs.active_plugin_name, reload=False)
        self.ui_manager.update_plugin_menu()

    def reset_preferences(self):
        """

        :return:
        """
        prefs.restore_default_preferences(qt_prefs, running_environment,
                                          classes)
        ctrl.call_watchers(self, 'color_themes_changed')
        if self.ui_manager.preferences_dialog:
            self.ui_manager.preferences_dialog.close()
        self.ui_manager.preferences_dialog = PreferencesDialog(self)
        self.ui_manager.preferences_dialog.open()
        self.ui_manager.preferences_dialog.trigger_all_updates()

    def init_forest_keepers(self):
        """ Put empty forest keepers (Kataja documents) in place -- you want to do this after
        plugins have changed the classes that implement these.
        :return:
        """
        self.forest_keepers = [classes.get('KatajaDocument')()]
        self.forest_keeper = self.forest_keepers[0]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')

    def load_initial_treeset(self):
        """ Loads and initializes a new set of trees. Has to be done before
        the program can do anything sane.
        """

        self.forest_keeper.create_forests(clear=False)
        self.change_forest()
        self.ui_manager.update_projects_menu()

    def create_new_project(self):
        names = [fk.name for fk in self.forest_keepers]
        name_base = 'New project'
        name = 'New project'
        c = 1
        while name in names:
            name = '%s %s' % (name_base, c)
            c += 1
        self.forest.retire_from_drawing()
        self.forest_keepers.append(classes.KatajaDocument(name=name))
        self.forest_keeper = self.forest_keepers[-1]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    def switch_project(self, i):
        self.forest.retire_from_drawing()
        self.forest_keeper = self.forest_keepers[i]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    # ### Visualization
    # #############################################################

    def change_forest(self):
        """ Tells the scene to remove current trees and related data and
        change it to a new one. Signal 'forest_changed' is already sent by forest keeper.
        """
        ctrl.disable_undo()
        if self.forest:
            self.forest.retire_from_drawing()
        if not self.forest_keeper.forest:
            self.forest_keeper.create_forests(clear=True)
        self.forest = self.forest_keeper.forest
        self.settings_manager.set_forest(self.forest)
        if self.forest.is_parsed:
            if self.forest.derivation_steps:
                ds = self.forest.derivation_steps
                if not ds.activated:
                    print('jumping to derivation step: ',
                          ds.derivation_step_index)
                    ds.jump_to_derivation_step(ds.derivation_step_index)
            else:
                print('no derivation steps')
        self.forest.prepare_for_drawing()
        ctrl.resume_undo()
        #if self.forest.undo_manager.

    def redraw(self):
        """ Call for forest redraw
        :return: None
        """
        self.forest.draw()

    def attach_widget_to_log_handler(self, browserwidget):
        """ This has to be done once: we have a logger set up before there is any output widget,
        once the widget is created it is connected to logger.
        :param browserwidget:
        :return:
        """
        self.app.log_handler.set_widget(browserwidget)

    #    def mousePressEvent(self, event):
#        """ KatajaMain doesn't do anything with mousePressEvents, it delegates
#        :param event:
#        them downwards. This is for debugging. """
#        QtWidgets.QMainWindow.mousePressEvent(self, event)

#    def keyPressEvent(self, event):
#        # if not self.key_manager.receive_key_press(event):
#        """
#
#        :param event:
#        :return:
#        """
#        return QtWidgets.QMainWindow.keyPressEvent(self, event)

# ## Menu management #######################################################

    def action_finished(self, m='', undoable=True, error=None):
        """ Write action to undo stack, report back to user and redraw trees
        if necessary
        :param m: message for undo
        :param undoable: are we supposed to take a snapshot of changes after
        this action.
        :param error message
        """
        if error:
            log.error(error)
        elif m:
            log.info(m)
        if ctrl.action_redraw:
            ctrl.forest.draw()
        if undoable and not error:
            ctrl.forest.undo_manager.take_snapshot(m)
        ctrl.graph_scene.start_animations()
        ctrl.ui.update_actions()

    def trigger_action(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.action_triggered(*args, **kwargs)

    def trigger_but_suppress_undo(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.trigger_but_suppress_undo(*args, **kwargs)

    def enable_actions(self):
        """ Restores menus """
        for action in self.ui_manager.actions.values():
            action.setDisabled(False)

    def disable_actions(self):
        """ Actions shouldn't be initiated when there is other multi-phase
        action going on """
        for action in self.ui_manager.actions.values():
            action.setDisabled(True)

    def change_color_theme(self, mode, force=False):
        """
        triggered by color mode selector in colors panel

        :param mode:
        """
        if mode != ctrl.settings.get('color_theme') or force:
            if ctrl.settings.document:
                ctrl.settings.set('color_theme', mode, level=g.DOCUMENT)
            ctrl.settings.set('color_theme', mode, level=g.PREFS)
            self.update_colors()

    def timerEvent(self, event):
        """ Timer event only for printing, for 'snapshot' effect
        :param event:
        """
        def find_path(fixed_part, extension, counter=0):
            """ Generate file names until free one is found
            :param fixed_part: blah
            :param extension: blah
            :param counter: blah
            """
            if not counter:
                fpath = fixed_part + extension
            else:
                fpath = fixed_part + str(counter) + extension
            if os.path.exists(fpath):
                fpath = find_path(fixed_part, extension, counter + 1)
            return fpath

        if not self.print_started:
            return
        else:
            self.print_started = False
        self.killTimer(event.timerId())
        # Prepare file and path
        path = prefs.print_file_path or prefs.userspace_path or \
            running_environment.default_userspace_path
        if not path.endswith('/'):
            path += '/'
        if not os.path.exists(path):
            print("bad path for printing (print_file_path in preferences) , "
                  "using '.' instead.")
            path = './'
        filename = prefs.print_file_name
        if filename.endswith(('.pdf', '.png')):
            filename = filename[:-4]
        # Prepare image
        self.graph_scene.removeItem(self.graph_scene.photo_frame)
        self.graph_scene.photo_frame = None
        # Prepare printer
        png = prefs.print_format == 'png'
        source = self.graph_scene.print_rect()

        if png:
            full_path = find_path(path + filename, '.png', 0)
            scale = 4
            target = QtCore.QRectF(QtCore.QPointF(0, 0), source.size() * scale)
            writer = QtGui.QImage(target.size().toSize(),
                                  QtGui.QImage.Format_ARGB32_Premultiplied)
            writer.fill(QtCore.Qt.transparent)
            painter = QtGui.QPainter()
            painter.begin(writer)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            iwriter = QtGui.QImageWriter(full_path)
            iwriter.write(writer)
            log.info(
                "printed to %s as PNG (%spx x %spx, %sx size)." %
                (full_path, int(target.width()), int(target.height()), scale))

        else:
            dpi = 25.4
            full_path = find_path(path + filename, '.pdf', 0)
            target = QtCore.QRectF(0, 0,
                                   source.width() / 2.0,
                                   source.height() / 2.0)

            writer = QtGui.QPdfWriter(full_path)
            writer.setResolution(dpi)
            writer.setPageSizeMM(target.size())
            writer.setPageMargins(QtCore.QMarginsF(0, 0, 0, 0))
            painter = QtGui.QPainter()
            painter.begin(writer)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            log.info("printed to %s as PDF with %s dpi." % (full_path, dpi))

        # Thank you!
        # Restore image
        self.graph_scene.setBackgroundBrush(self.color_manager.gradient)

    # Not called from anywhere yet, but useful
    def release_selected(self, **kw):
        """

        :param kw:
        :return:
        """
        for node in ctrl.selected:
            node.release()
        self.action_finished()
        return True

    def clear_all(self):
        """ Empty everything - maybe necessary before loading new data. """
        if self.forest:
            self.forest.retire_from_drawing()
        self.forest_keeper = None
        # Garbage collection doesn't mix well with animations that are still running
        #print('garbage stats:', gc.get_count())
        #gc.collect()
        #print('after collection:', gc.get_count())
        #if gc.garbage:
        #    print('garbage:', gc.garbage)
        self.forest_keepers.append(classes.KatajaDocument(clear=True))
        self.forest_keeper = self.forest_keepers[-1]
        self.settings_manager.set_document(self.forest_keeper)
        self.forest = None

    # ## Other window events
    ###################################################

    def closeEvent(self, event):
        """ Shut down the program, give some debug info
        :param event:
        """
        QtWidgets.QMainWindow.closeEvent(self, event)
        if ctrl.print_garbage:
            # import objgraph
            log.debug('garbage stats: ' + str(gc.get_count()))
            gc.collect()
            log.debug('after collection: ' + str(gc.get_count()))
            if gc.garbage:
                log.debug('garbage: ' + str(gc.garbage))

                # objgraph.show_most_common_types(limit =40)
        if self.save_prefs:
            prefs.save_preferences()
        log.info('...done')

    @time_me
    def create_save_data(self):
        """
        Make a large dictionary of all objects with all of the complex stuff
        and circular references stripped out.
        :return: dict
        """
        savedata = {}
        open_references = {}
        savedata['save_scheme_version'] = 0.4
        self.save_object(savedata, open_references)
        max_rounds = 10
        c = 0
        while open_references and c < max_rounds:
            c += 1
            #print(len(savedata))
            #print('---------------------------')
            for obj in list(open_references.values()):
                if hasattr(obj, 'uid'):
                    obj.save_object(savedata, open_references)
                else:
                    print('cannot save open reference object ', obj)
        assert (c < max_rounds)
        print('total savedata: %s chars in %s items.' %
              (len(str(savedata)), len(savedata)))
        # print(savedata)
        return savedata

    def update_colors(self, randomise=False, animate=True):
        t = time.time()
        cm = self.color_manager
        old_gradient_base = cm.paper()
        cm.update_colors(randomise=randomise)
        self.app.setPalette(cm.get_qt_palette())
        self.update_style_sheet()
        ctrl.call_watchers(self, 'palette_changed')
        if cm.gradient:
            if old_gradient_base != cm.paper() and animate:
                self.graph_scene.fade_background_gradient(
                    old_gradient_base, cm.paper())
            else:
                self.graph_scene.setBackgroundBrush(cm.gradient)
        else:
            self.graph_scene.setBackgroundBrush(qt_prefs.no_brush)
        self.update()

    ### Applying specific preferences globally.
    # These are on_change -methods for various preferences -- these are called if changing a
    # preference should have immediate consequences. They are hosted here because they need to
    # have access to prefs, qt_prefs, main etc.

    def prepare_easing_curve(self):
        qt_prefs.prepare_easing_curve(prefs.curve, prefs.move_frames)

    def update_color_theme(self):
        self.change_color_theme(prefs.color_theme, force=True)

    def update_visualization(self):
        ctrl.forest.set_visualization(prefs.visualization)
        self.main.redraw()

    def resize_ui_font(self):
        qt_prefs.toggle_large_ui_font(prefs.large_ui_text, prefs.fonts)
        self.update_style_sheet()

    def watch_alerted(self, obj, signal, field_name, value):
        """ Receives alerts from signals that this object has chosen to listen. These signals
         are declared in 'self.watchlist'.

         This method will try to sort out the received signals and act accordingly.

        :param obj: the object causing the alarm
        :param signal: identifier for type of the alarm
        :param field_name: name of the field of the object causing the alarm
        :param value: value given to the field
        :return:
        """
        pass

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    forest_keeper = SavedField("forest_keeper")
    forest = SavedField("forest")
Ejemplo n.º 5
0
    def __init__(self, kataja_app, no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        kataja_app.processEvents()
        SavedObject.__init__(self)
        self.use_tooltips = True
        self.available_plugins = {}
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner,
                       QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1152, 720)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.save_prefs = not no_prefs
        self.forest = None
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager()
        self.settings_manager = Settings()
        self.forest_keepers = []
        self.forest_keeper = None
        ctrl.late_init(self)
        classes.late_init()
        prefs.import_node_classes(classes)
        self.syntax = SyntaxConnection()
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.settings_manager.set_prefs(prefs)
        self.color_manager.update_custom_colors()
        self.find_plugins(prefs.plugins_path or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.graph_scene = GraphScene(main=self, graph_view=None)
        self.graph_view = GraphView(main=self, graph_scene=self.graph_scene)
        self.graph_scene.graph_view = self.graph_view
        ctrl.add_watcher(self, 'ui_font_changed')
        self.ui_manager = UIManager(self)
        self.settings_manager.set_ui_manager(self.ui_manager)
        self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.init_forest_keepers()
        self.settings_manager.set_document(self.forest_keeper)
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.forest = Forest()
        self.settings_manager.set_forest(self.forest)
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        self.setCentralWidget(self.graph_view)
        self.setGeometry(x, y, w, h)
        self.setWindowTitle(self.tr("Kataja"))
        self.print_started = False
        self.show()
        self.raise_()
        kataja_app.processEvents()
        self.activateWindow()
        self.status_bar = self.statusBar()
        self.install_plugins()
        self.load_initial_treeset()
        log.info('Welcome to Kataja! (h) for help')
        #ctrl.call_watchers(self.forest_keeper, 'forest_changed')
        # toolbar = QtWidgets.QToolBar()
        # toolbar.setFixedSize(480, 40)
        # self.addToolBar(toolbar)
        gestures = [QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture, QtCore.Qt.PanGesture,
                    QtCore.Qt.PinchGesture, QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture]
        #for gesture in gestures:
        #    self.grabGesture(gesture)
        self.action_finished(undoable=False)
        self.forest.undo_manager.flush_pile()
Ejemplo n.º 6
0
class KatajaMain(SavedObject, QtWidgets.QMainWindow):
    """ Qt's main window. When this is closed, application closes. Graphics are
    inside this, in scene objects with view widgets. This window also manages
    keypresses and menus. """
    unique = True

    def __init__(self, kataja_app, no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        kataja_app.processEvents()
        SavedObject.__init__(self)
        self.use_tooltips = True
        self.available_plugins = {}
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner,
                       QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1152, 720)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.save_prefs = not no_prefs
        self.forest = None
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager()
        self.settings_manager = Settings()
        self.forest_keepers = []
        self.forest_keeper = None
        ctrl.late_init(self)
        classes.late_init()
        prefs.import_node_classes(classes)
        self.syntax = SyntaxConnection()
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.settings_manager.set_prefs(prefs)
        self.color_manager.update_custom_colors()
        self.find_plugins(prefs.plugins_path or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.graph_scene = GraphScene(main=self, graph_view=None)
        self.graph_view = GraphView(main=self, graph_scene=self.graph_scene)
        self.graph_scene.graph_view = self.graph_view
        ctrl.add_watcher(self, 'ui_font_changed')
        self.ui_manager = UIManager(self)
        self.settings_manager.set_ui_manager(self.ui_manager)
        self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.init_forest_keepers()
        self.settings_manager.set_document(self.forest_keeper)
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.forest = Forest()
        self.settings_manager.set_forest(self.forest)
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        self.setCentralWidget(self.graph_view)
        self.setGeometry(x, y, w, h)
        self.setWindowTitle(self.tr("Kataja"))
        self.print_started = False
        self.show()
        self.raise_()
        kataja_app.processEvents()
        self.activateWindow()
        self.status_bar = self.statusBar()
        self.install_plugins()
        self.load_initial_treeset()
        log.info('Welcome to Kataja! (h) for help')
        #ctrl.call_watchers(self.forest_keeper, 'forest_changed')
        # toolbar = QtWidgets.QToolBar()
        # toolbar.setFixedSize(480, 40)
        # self.addToolBar(toolbar)
        gestures = [QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture, QtCore.Qt.PanGesture,
                    QtCore.Qt.PinchGesture, QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture]
        #for gesture in gestures:
        #    self.grabGesture(gesture)
        self.action_finished(undoable=False)
        self.forest.undo_manager.flush_pile()

    def update_style_sheet(self):
        c = ctrl.cm.drawing()
        ui = ctrl.cm.ui()
        f = qt_prefs.get_font(g.UI_FONT)
        self.setStyleSheet(stylesheet % {'draw': c.name(), 'lighter': c.lighter().name(),
                                         'paper': ctrl.cm.paper().name(),
                                         'ui': ui.name(), 'ui_lighter': ui.lighter().name(),
                                         'ui_font': f.family(), 'ui_font_size': f.pointSize(),
                                         'ui_font_larger': int(f.pointSize() * 1.2)})

    def find_plugins(self, plugins_path):
        """ Find the plugins dir for the running configuration and read the metadata of plugins.
        Don't try to load actual python code yet
        :return: None
        """
        if not plugins_path:
            return
        self.available_plugins = {}
        plugins_path = os.path.normpath(plugins_path)
        os.makedirs(plugins_path, exist_ok=True)
        sys.path.append(plugins_path)
        base_ends = len(plugins_path.split('/'))
        for root, dirs, files in os.walk(plugins_path):
            path_parts = root.split('/')
            if len(path_parts) == base_ends + 1 and not path_parts[base_ends].startswith('__') \
                    and 'plugin.json' in files:
                success = False
                try:
                    plugin_file = open(root + '/plugin.json', 'r')
                    data = json.load(plugin_file)
                    plugin_file.close()
                    success = True
                except:
                    log.error(sys.exc_info())
                    print(sys.exc_info())
                if success:
                    mod_name = path_parts[base_ends]
                    data['module_name'] = mod_name
                    data['module_path'] = root
                    self.available_plugins[mod_name] = data

    def enable_plugin(self, plugin_key, reload=False):
        """ Start one plugin: save data, replace required classes with plugin classes, load data.

        """
        self.active_plugin_setup = self.load_plugin(plugin_key)
        if not self.active_plugin_setup:
            return
        self.clear_all()
        ctrl.disable_undo()
        if reload:
            available = []
            for key in sys.modules:
                if key.startswith(plugin_key):
                    available.append(key)
            if getattr(self.active_plugin_setup, 'reload_order', None):
                to_reload = [x for x in self.active_plugin_setup.reload_order if x in available]
            else:
                to_reload = sorted(available)
            for mod_name in to_reload:
                importlib.reload(sys.modules[mod_name])
                print('reloaded ', mod_name)
                log.info('reloaded module %s' % mod_name)

        if hasattr(self.active_plugin_setup, 'plugin_parts'):
            for classobj in self.active_plugin_setup.plugin_parts:
                base_class = classes.find_base_model(classobj)
                if base_class:
                    classes.add_mapping(base_class, classobj)
                    m = "replacing %s with %s " % (base_class.__name__, classobj.__name__)
                else:
                    m = "adding %s " % classobj.__name__
                log.info(m)
                print(m)
        if hasattr(self.active_plugin_setup, 'help_file'):
            dir_path = os.path.dirname(os.path.realpath(self.active_plugin_setup.__file__))
            print(dir_path)
            self.ui_manager.set_help_source(dir_path, self.active_plugin_setup.help_file)
        if hasattr(self.active_plugin_setup, 'start_plugin'):
            self.active_plugin_setup.start_plugin(self, ctrl, prefs)
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = plugin_key

    def disable_current_plugin(self):
        """ Disable the current plugin and load the default trees instead.
        :param clear: if True, have empty treeset, if False, try to load default kataja treeset."""
        if not self.active_plugin_setup:
            return
        ctrl.disable_undo()
        if hasattr(self.active_plugin_setup, 'tear_down_plugin'):
            self.active_plugin_setup.tear_down_plugin(self, ctrl, prefs)
        self.clear_all()
        # print(classes.base_name_to_plugin_class)
        # if hasattr(self.active_plugin_setup, 'plugin_parts'):
        #     for classobj in self.active_plugin_setup.plugin_parts:
        #         class_name = classobj.__name__
        #         if class_name:
        #             log.info(f'removing {class_name}')
        #             print(f'removing {class_name}')
        #             classes.remove_class(class_name)
        classes.restore_default_classes()
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = ''

    def load_plugin(self, plugin_module):
        setup = None
        importlib.invalidate_caches()
        if plugin_module in self.available_plugins:
            retry = True
            while retry:
                try:
                    setup = importlib.import_module(plugin_module + ".setup")
                    retry = False
                except:
                    e = sys.exc_info()
                    error_dialog = ErrorDialog(self)
                    error_dialog.set_error('%s, line %s\n%s: %s' % (
                    plugin_module + ".setup.py", e[2].tb_lineno, e[0].__name__, e[1]))
                    error_dialog.set_traceback(traceback.format_exc())
                    retry = error_dialog.exec_()
                    setup = None
        return setup

    def install_plugins(self):
        """ If there are plugins defined in preferences to be used, activate them now.
        :return: None
        """
        if prefs.active_plugin_name:
            log.info('Installing plugin %s...' % prefs.active_plugin_name)
            self.enable_plugin(prefs.active_plugin_name, reload=False)
        self.ui_manager.update_plugin_menu()

    def reset_preferences(self):
        """

        :return:
        """
        prefs.restore_default_preferences(qt_prefs, running_environment, classes)
        ctrl.call_watchers(self, 'color_themes_changed')
        if self.ui_manager.preferences_dialog:
            self.ui_manager.preferences_dialog.close()
        self.ui_manager.preferences_dialog = PreferencesDialog(self)
        self.ui_manager.preferences_dialog.open()
        self.ui_manager.preferences_dialog.trigger_all_updates()


    def init_forest_keepers(self):
        """ Put empty forest keepers (Kataja documents) in place -- you want to do this after
        plugins have changed the classes that implement these.
        :return:
        """
        self.forest_keepers = [classes.get('KatajaDocument')()]
        self.forest_keeper = self.forest_keepers[0]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')

    def load_initial_treeset(self):
        """ Loads and initializes a new set of trees. Has to be done before
        the program can do anything sane.
        """

        self.forest_keeper.create_forests(clear=False)
        self.change_forest()
        self.ui_manager.update_projects_menu()

    def create_new_project(self):
        names = [fk.name for fk in self.forest_keepers]
        name_base = 'New project'
        name = 'New project'
        c = 1
        while name in names:
            name = '%s %s' % (name_base, c)
            c += 1
        self.forest.retire_from_drawing()
        self.forest_keepers.append(classes.KatajaDocument(name=name))
        self.forest_keeper = self.forest_keepers[-1]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    def switch_project(self, i):
        self.forest.retire_from_drawing()
        self.forest_keeper = self.forest_keepers[i]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    # ### Visualization
    # #############################################################

    def change_forest(self):
        """ Tells the scene to remove current trees and related data and
        change it to a new one. Signal 'forest_changed' is already sent by forest keeper.
        """
        ctrl.disable_undo()
        if self.forest:
            self.forest.retire_from_drawing()
        if not self.forest_keeper.forest:
            self.forest_keeper.create_forests(clear=True)
        self.forest = self.forest_keeper.forest
        self.settings_manager.set_forest(self.forest)
        if self.forest.is_parsed:
            if self.forest.derivation_steps:
                ds = self.forest.derivation_steps
                if not ds.activated:
                    print('jumping to derivation step: ', ds.derivation_step_index)
                    ds.jump_to_derivation_step(ds.derivation_step_index)
            else:
                print('no derivation steps')
        self.forest.prepare_for_drawing()
        ctrl.resume_undo()

    def redraw(self):
        """ Call for forest redraw
        :return: None
        """
        self.forest.draw()

    def attach_widget_to_log_handler(self, browserwidget):
        """ This has to be done once: we have a logger set up before there is any output widget,
        once the widget is created it is connected to logger.
        :param browserwidget:
        :return:
        """
        self.app.log_handler.set_widget(browserwidget)

    #    def mousePressEvent(self, event):
#        """ KatajaMain doesn't do anything with mousePressEvents, it delegates
#        :param event:
#        them downwards. This is for debugging. """
#        QtWidgets.QMainWindow.mousePressEvent(self, event)

#    def keyPressEvent(self, event):
#        # if not self.key_manager.receive_key_press(event):
#        """
#
#        :param event:
#        :return:
#        """
#        return QtWidgets.QMainWindow.keyPressEvent(self, event)

    # ## Menu management #######################################################

    def action_finished(self, m='', undoable=True, error=None):
        """ Write action to undo stack, report back to user and redraw trees
        if necessary
        :param m: message for undo
        :param undoable: are we supposed to take a snapshot of changes after
        this action.
        :param error message
        """
        if error:
            log.error(error)
        elif m:
            log.info(m)
        if ctrl.action_redraw:
            ctrl.forest.draw()
        if undoable and not error:
            ctrl.forest.undo_manager.take_snapshot(m)
        ctrl.graph_scene.start_animations()
        ctrl.ui.update_actions()

    def trigger_action(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.action_triggered(*args, **kwargs)

    def trigger_but_suppress_undo(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.trigger_but_suppress_undo(*args, **kwargs)

    def enable_actions(self):
        """ Restores menus """
        for action in self.ui_manager.actions.values():
            action.setDisabled(False)

    def disable_actions(self):
        """ Actions shouldn't be initiated when there is other multi-phase
        action going on """
        for action in self.ui_manager.actions.values():
            action.setDisabled(True)

    def change_color_theme(self, mode, force=False):
        """
        triggered by color mode selector in colors panel

        :param mode:
        """
        if mode != ctrl.settings.get('color_theme') or force:
            if ctrl.settings.document:
                ctrl.settings.set('color_theme', mode, level=g.DOCUMENT)
            ctrl.settings.set('color_theme', mode, level=g.PREFS)
            self.update_colors()

    def timerEvent(self, event):
        """ Timer event only for printing, for 'snapshot' effect
        :param event:
        """
        def find_path(fixed_part, extension, counter=0):
            """ Generate file names until free one is found
            :param fixed_part: blah
            :param extension: blah
            :param counter: blah
            """
            if not counter:
                fpath = fixed_part + extension
            else:
                fpath = fixed_part + str(counter) + extension
            if os.path.exists(fpath):
                fpath = find_path(fixed_part, extension, counter + 1)
            return fpath

        if not self.print_started:
            return
        else:
            self.print_started = False
        self.killTimer(event.timerId())
        # Prepare file and path
        path = prefs.print_file_path or prefs.userspace_path or \
            running_environment.default_userspace_path
        if not path.endswith('/'):
            path += '/'
        if not os.path.exists(path):
            print(
                "bad path for printing (print_file_path in preferences) , "
                "using '.' instead.")
            path = './'
        filename = prefs.print_file_name
        if filename.endswith(('.pdf', '.png')):
            filename = filename[:-4]
        # Prepare image
        self.graph_scene.removeItem(self.graph_scene.photo_frame)
        self.graph_scene.photo_frame = None
        # Prepare printer
        png = prefs.print_format == 'png'
        source = self.graph_scene.print_rect()

        if png:
            full_path = find_path(path + filename, '.png', 0)
            scale = 4
            target = QtCore.QRectF(QtCore.QPointF(0, 0), source.size() * scale)
            writer = QtGui.QImage(target.size().toSize(),
                                  QtGui.QImage.Format_ARGB32_Premultiplied)
            writer.fill(QtCore.Qt.transparent)
            painter = QtGui.QPainter()
            painter.begin(writer)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            iwriter = QtGui.QImageWriter(full_path)
            iwriter.write(writer)
            log.info("printed to %s as PNG (%spx x %spx, %sx size)." % (
                     full_path, int(target.width()), int(target.height()), scale))

        else:
            dpi = 25.4
            full_path = find_path(path + filename, '.pdf', 0)
            target = QtCore.QRectF(0, 0, source.width() / 2.0,
                                   source.height() / 2.0)

            writer = QtGui.QPdfWriter(full_path)
            writer.setResolution(dpi)
            writer.setPageSizeMM(target.size())
            writer.setPageMargins(QtCore.QMarginsF(0, 0, 0, 0))
            painter = QtGui.QPainter()
            painter.begin(writer)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            log.info("printed to %s as PDF with %s dpi." % (full_path, dpi))

        # Thank you!
        # Restore image
        self.graph_scene.setBackgroundBrush(self.color_manager.gradient)

    # Not called from anywhere yet, but useful
    def release_selected(self, **kw):
        """

        :param kw:
        :return:
        """
        for node in ctrl.selected:
            node.release()
        self.action_finished()
        return True

    def clear_all(self):
        """ Empty everything - maybe necessary before loading new data. """
        if self.forest:
            self.forest.retire_from_drawing()
        self.forest_keeper = None
        # Garbage collection doesn't mix well with animations that are still running
        #print('garbage stats:', gc.get_count())
        #gc.collect()
        #print('after collection:', gc.get_count())
        #if gc.garbage:
        #    print('garbage:', gc.garbage)
        self.forest_keepers.append(classes.KatajaDocument(clear=True))
        self.forest_keeper = self.forest_keepers[-1]
        self.settings_manager.set_document(self.forest_keeper)
        self.forest = None

    # ## Other window events
    ###################################################

    def closeEvent(self, event):
        """ Shut down the program, give some debug info
        :param event:
        """
        QtWidgets.QMainWindow.closeEvent(self, event)
        if ctrl.print_garbage:
            # import objgraph
            log.debug('garbage stats: ' + str(gc.get_count()))
            gc.collect()
            log.debug('after collection: ' + str(gc.get_count()))
            if gc.garbage:
                log.debug('garbage: ' + str(gc.garbage))

                # objgraph.show_most_common_types(limit =40)
        if self.save_prefs:
            prefs.save_preferences()
        log.info('...done')

    @time_me
    def create_save_data(self):
        """
        Make a large dictionary of all objects with all of the complex stuff
        and circular references stripped out.
        :return: dict
        """
        savedata = {}
        open_references = {}
        savedata['save_scheme_version'] = 0.4
        self.save_object(savedata, open_references)
        max_rounds = 10
        c = 0
        while open_references and c < max_rounds:
            c += 1
            #print(len(savedata))
            #print('---------------------------')
            for obj in list(open_references.values()):
                if hasattr(obj, 'uid'):
                    obj.save_object(savedata, open_references)
                else:
                    print('cannot save open reference object ', obj)
        assert(c < max_rounds)
        print('total savedata: %s chars in %s items.' % (
        len(str(savedata)), len(savedata)))
        # print(savedata)
        return savedata

    def update_colors(self, randomise=False, animate=True):
        cm = self.color_manager
        old_gradient_base = cm.paper()
        cm.update_colors(randomise=randomise)
        self.app.setPalette(cm.get_qt_palette())
        self.update_style_sheet()
        ctrl.call_watchers(self, 'palette_changed')
        if cm.gradient:
            if old_gradient_base != cm.paper() and animate:
                self.graph_scene.fade_background_gradient(old_gradient_base, cm.paper())
            else:
                self.graph_scene.setBackgroundBrush(cm.gradient)
        else:
            self.graph_scene.setBackgroundBrush(qt_prefs.no_brush)
        self.update()

    def watch_alerted(self, obj, signal, field_name, value):
        """ Receives alerts from signals that this object has chosen to listen. These signals
         are declared in 'self.watchlist'.

         This method will try to sort out the received signals and act accordingly.

        :param obj: the object causing the alarm
        :param signal: identifier for type of the alarm
        :param field_name: name of the field of the object causing the alarm
        :param value: value given to the field
        :return:
        """
        if signal == 'ui_font_changed':
            self.update_style_sheet()

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    forest_keeper = SavedField("forest_keeper")
    forest = SavedField("forest")