示例#1
0
class InteractiveScene(Scene):
    """
    To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
    or just tap ctrl to select the mobject under the cursor.

    Pressing command + t will toggle between modes where you either select top level
    mobjects part of the scene, or low level pieces.

    Hold 'g' to grab the selection and move it around
    Hold 'h' to drag it constrained in the horizontal direction
    Hold 'v' to drag it constrained in the vertical direction
    Hold 't' to resize selection, adding 'shift' to resize with respect to a corner

    Command + 'c' copies the ids of selections to clipboard
    Command + 'v' will paste either:
        - The copied mobject
        - A Tex mobject based on copied LaTeX
        - A Text mobject based on copied Text
    Command + 'z' restores selection back to its original state
    Command + 's' saves the selected mobjects to file
    """
    corner_dot_config = dict(
        color=WHITE,
        radius=0.05,
        glow_factor=1.0,
    )
    selection_rectangle_stroke_color = WHITE
    selection_rectangle_stroke_width = 1.0
    colors = MANIM_COLORS
    selection_nudge_size = 0.05

    def setup(self):
        self.selection = Group()
        self.selection_highlight = Group()
        self.selection_rectangle = self.get_selection_rectangle()
        self.color_palette = self.get_color_palette()
        self.unselectables = [
            self.selection,
            self.selection_highlight,
            self.selection_rectangle,
            self.camera.frame
        ]
        self.saved_selection_state = []
        self.select_top_level_mobs = True

        self.is_selecting = False
        self.add(self.selection_highlight)

    def toggle_selection_mode(self):
        self.select_top_level_mobs = not self.select_top_level_mobs
        self.refresh_selection_scope()

    def get_selection_search_set(self):
        mobs = [m for m in self.mobjects if m not in self.unselectables]
        if self.select_top_level_mobs:
            return mobs
        else:
            return [
                submob
                for mob in mobs
                for submob in mob.family_members_with_points()
            ]

    def refresh_selection_scope(self):
        curr = list(self.selection)
        if self.select_top_level_mobs:
            self.selection.set_submobjects([
                mob
                for mob in self.mobjects
                if any(sm in mob.get_family() for sm in curr)
            ])
            self.selection.refresh_bounding_box(recurse_down=True)
        else:
            self.selection.set_submobjects(
                extract_mobject_family_members(
                    curr, exclude_pointless=True,
                )
            )
        self.refresh_selection_highlight()

    def get_selection_rectangle(self):
        rect = Rectangle(
            stroke_color=self.selection_rectangle_stroke_color,
            stroke_width=self.selection_rectangle_stroke_width,
        )
        rect.fix_in_frame()
        rect.fixed_corner = ORIGIN
        rect.add_updater(self.update_selection_rectangle)
        return rect

    def get_color_palette(self):
        palette = VGroup(*(
            Square(fill_color=color, fill_opacity=1, side_length=1)
            for color in self.colors
        ))
        palette.set_stroke(width=0)
        palette.arrange(RIGHT, buff=0.5)
        palette.set_width(FRAME_WIDTH - 0.5)
        palette.to_edge(DOWN, buff=SMALL_BUFF)
        palette.fix_in_frame()
        return palette

    def get_stroke_highlight(self, vmobject):
        outline = vmobject.copy()
        for sm, osm in zip(vmobject.get_family(), outline.get_family()):
            osm.set_fill(opacity=0)
            osm.set_stroke(YELLOW, width=sm.get_stroke_width() + 1.5)
        outline.add_updater(lambda o: o.replace(vmobject))
        return outline

    def get_corner_dots(self, mobject):
        dots = DotCloud(**self.corner_dot_config)
        radius = self.corner_dot_config["radius"]
        if mobject.get_depth() < 1e-2:
            vects = [DL, UL, UR, DR]
        else:
            vects = list(it.product(*3 * [[-1, 1]]))
        dots.add_updater(lambda d: d.set_points([
            mobject.get_corner(v) + v * radius
            for v in vects
        ]))
        return dots

    def get_highlight(self, mobject):
        if isinstance(mobject, VMobject) and mobject.has_points():
            return self.get_stroke_highlight(mobject)
        else:
            return self.get_corner_dots(mobject)

    def refresh_selection_highlight(self):
        self.selection_highlight.set_submobjects([
            self.get_highlight(mob)
            for mob in self.selection
        ])

    def update_selection_rectangle(self, rect):
        p1 = rect.fixed_corner
        p2 = self.mouse_point.get_center()
        rect.set_points_as_corners([
            p1, [p2[0], p1[1], 0],
            p2, [p1[0], p2[1], 0],
            p1,
        ])
        return rect

    def add_to_selection(self, *mobjects):
        mobs = list(filter(lambda m: m not in self.unselectables, mobjects))
        self.selection.add(*mobjects)
        self.selection_highlight.add(*map(self.get_highlight, mobs))
        self.saved_selection_state = [(mob, mob.copy()) for mob in self.selection]

    def toggle_from_selection(self, *mobjects):
        for mob in mobjects:
            if mob in self.selection:
                self.selection.remove(mob)
            else:
                self.add_to_selection(mob)
        self.refresh_selection_highlight()

    def clear_selection(self):
        self.selection.set_submobjects([])
        self.selection_highlight.set_submobjects([])

    def add(self, *new_mobjects: Mobject):
        for mob in new_mobjects:
            mob.make_movable()
        super().add(*new_mobjects)

    # Selection operations

    def copy_selection(self):
        ids = map(id, self.selection)
        pyperclip.copy(",".join(map(str, ids)))

    def paste_selection(self):
        clipboard_str = pyperclip.paste()
        # Try pasting a mobject
        try:
            ids = map(int, clipboard_str.split(","))
            mobs = map(self.id_to_mobject, ids)
            mob_copies = [m.copy() for m in mobs if m is not None]
            self.clear_selection()
            self.add_to_selection(*mob_copies)
            self.play(*(
                FadeIn(mc, run_time=0.5, scale=1.5)
                for mc in mob_copies
            ))
            return
        except ValueError:
            pass
        # Otherwise, treat as tex or text
        if set("\\^=+").intersection(clipboard_str):  # Proxy to text for LaTeX
            try:
                new_mob = Tex(clipboard_str)
            except LatexError:
                return
        else:
            new_mob = Text(clipboard_str)
        self.clear_selection()
        self.add(new_mob)
        self.add_to_selection(new_mob)

    def delete_selection(self):
        self.remove(*self.selection)
        self.clear_selection()

    def saved_selection_to_file(self):
        directory = self.file_writer.get_saved_mobject_directory()
        files = os.listdir(directory)
        for mob in self.selection:
            file_name = str(mob) + "_0.mob"
            index = 0
            while file_name in files:
                file_name = file_name.replace(str(index), str(index + 1))
                index += 1
            if platform.system() == 'Darwin':
                user_name = os.popen(f"""
                    osascript -e '
                    set chosenfile to (choose file name default name "{file_name}" default location "{directory}")
                    POSIX path of chosenfile'
                """).read()
                user_name = user_name.replace("\n", "")
            else:
                user_name = input(
                    f"Enter mobject file name (default is {file_name}): "
                )
            if user_name:
                file_name = user_name
            files.append(file_name)
            self.save_mobect(mob, file_name)

    def undo(self):
        mobs = []
        for mob, state in self.saved_selection_state:
            mob.become(state)
            mobs.append(mob)
            if mob not in self.mobjects:
                self.add(mob)
        self.selection.set_submobjects(mobs)
        self.refresh_selection_highlight()

    def prepare_resizing(self, about_corner=False):
        center = self.selection.get_center()
        mp = self.mouse_point.get_center()
        if about_corner:
            self.scale_about_point = self.selection.get_corner(center - mp)
        else:
            self.scale_about_point = center
        self.scale_ref_vect = mp - self.scale_about_point
        self.scale_ref_width = self.selection.get_width()

    # Event handlers

    def on_key_press(self, symbol: int, modifiers: int) -> None:
        super().on_key_press(symbol, modifiers)
        char = chr(symbol)
        # Enable selection
        if char == SELECT_KEY and modifiers == 0:
            self.is_selecting = True
            self.add(self.selection_rectangle)
            self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
        # Prepare for move
        elif char in [GRAB_KEY, HORIZONTAL_GRAB_KEY, VERTICAL_GRAB_KEY] and modifiers == 0:
            mp = self.mouse_point.get_center()
            self.mouse_to_selection = mp - self.selection.get_center()
        # Prepare for resizing
        elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
            self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
        elif symbol == SHIFT_SYMBOL:
            if self.window.is_key_pressed(ord("t")):
                self.prepare_resizing(about_corner=True)
        # Show color palette
        elif char == COLOR_KEY and modifiers == 0:
            if len(self.selection) == 0:
                return
            if self.color_palette not in self.mobjects:
                self.add(self.color_palette)
            else:
                self.remove(self.color_palette)
        # Command + c -> Copy mobject ids to clipboard
        elif char == "c" and modifiers == COMMAND_MODIFIER:
            self.copy_selection()
        # Command + v -> Paste
        elif char == "v" and modifiers == COMMAND_MODIFIER:
            self.paste_selection()
        # Command + x -> Cut
        elif char == "x" and modifiers == COMMAND_MODIFIER:
            # TODO, this copy won't work, because once the objects are removed,
            # they're not searched for in the pasting.
            self.copy_selection()
            self.delete_selection()
        # Delete
        elif symbol == DELETE_SYMBOL:
            self.delete_selection()
        # Command + a -> Select all
        elif char == "a" and modifiers == COMMAND_MODIFIER:
            self.clear_selection()
            self.add_to_selection(*self.mobjects)
        # Command + g -> Group selection
        elif char == "g" and modifiers == COMMAND_MODIFIER:
            group = self.get_group(*self.selection)
            self.add(group)
            self.clear_selection()
            self.add_to_selection(group)
        # Command + shift + g -> Ungroup the selection
        elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
            pieces = []
            for mob in list(self.selection):
                self.remove(mob)
                pieces.extend(list(mob))
            self.clear_selection()
            self.add(*pieces)
            self.add_to_selection(*pieces)
        # Command + t -> Toggle selection mode
        elif char == "t" and modifiers == COMMAND_MODIFIER:
            self.toggle_selection_mode()
        # Command + z -> Restore selection to original state
        elif char == "z" and modifiers == COMMAND_MODIFIER:
            self.undo()
        # Command + s -> Save selections to file
        elif char == "s" and modifiers == COMMAND_MODIFIER:
            self.saved_selection_to_file()
        # Keyboard movements
        elif symbol in ARROW_SYMBOLS:
            nudge = self.selection_nudge_size
            if (modifiers & SHIFT_MODIFIER):
                nudge *= 10
            vect = [LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)]
            self.selection.shift(nudge * vect)

    def on_key_release(self, symbol: int, modifiers: int) -> None:
        super().on_key_release(symbol, modifiers)
        if chr(symbol) == SELECT_KEY:
            self.is_selecting = False
            self.remove(self.selection_rectangle)
            for mob in reversed(self.get_selection_search_set()):
                if mob.is_movable() and self.selection_rectangle.is_touching(mob):
                    self.add_to_selection(mob)

        elif symbol == SHIFT_SYMBOL:
            if self.window.is_key_pressed(ord(RESIZE_KEY)):
                self.prepare_resizing(about_corner=False)

    def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
        super().on_mouse_motion(point, d_point)
        # Move selection
        if self.window.is_key_pressed(ord("g")):
            self.selection.move_to(point - self.mouse_to_selection)
        # Move selection restricted to horizontal
        elif self.window.is_key_pressed(ord("h")):
            self.selection.set_x((point - self.mouse_to_selection)[0])
        # Move selection restricted to vertical
        elif self.window.is_key_pressed(ord("v")):
            self.selection.set_y((point - self.mouse_to_selection)[1])
        # Scale selection
        elif self.window.is_key_pressed(ord("t")):
            # TODO, allow for scaling about the opposite corner
            vect = point - self.scale_about_point
            scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
            self.selection.set_width(
                scalar * self.scale_ref_width,
                about_point=self.scale_about_point
            )

    def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
        super().on_mouse_release(point, button, mods)
        if self.color_palette in self.mobjects:
            # Search through all mobject on the screne, not just the palette
            to_search = list(it.chain(*(
                mobject.family_members_with_points()
                for mobject in self.mobjects
                if mobject not in self.unselectables
            )))
            mob = self.point_to_mobject(point, to_search)
            if mob is not None:
                self.selection.set_color(mob.get_fill_color())
            self.remove(self.color_palette)
        elif self.window.is_key_pressed(SHIFT_SYMBOL):
            mob = self.point_to_mobject(point)
            if mob is not None:
                self.toggle_from_selection(mob)
        else:
            self.clear_selection()
示例#2
0
class InteractiveScene(Scene):
    """
    To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
    or just tap ctrl to select the mobject under the cursor.

    Pressing command + t will toggle between modes where you either select top level
    mobjects part of the scene, or low level pieces.

    Hold 'g' to grab the selection and move it around
    Hold 'h' to drag it constrained in the horizontal direction
    Hold 'v' to drag it constrained in the vertical direction
    Hold 't' to resize selection, adding 'shift' to resize with respect to a corner

    Command + 'c' copies the ids of selections to clipboard
    Command + 'v' will paste either:
        - The copied mobject
        - A Tex mobject based on copied LaTeX
        - A Text mobject based on copied Text
    Command + 'z' restores selection back to its original state
    Command + 's' saves the selected mobjects to file
    """
    corner_dot_config = dict(
        color=WHITE,
        radius=0.05,
        glow_factor=1.0,
    )
    selection_rectangle_stroke_color = WHITE
    selection_rectangle_stroke_width = 1.0
    palette_colors = MANIM_COLORS
    selection_nudge_size = 0.05
    cursor_location_config = dict(
        font_size=24,
        fill_color=GREY_C,
        num_decimal_places=3,
    )
    time_label_config = dict(
        font_size=24,
        fill_color=GREY_C,
        num_decimal_places=1,
    )
    crosshair_width = 0.2
    crosshair_color = GREY_A

    def setup(self):
        self.selection = Group()
        self.selection_highlight = self.get_selection_highlight()
        self.selection_rectangle = self.get_selection_rectangle()
        self.crosshair = self.get_crosshair()
        self.information_label = self.get_information_label()
        self.color_palette = self.get_color_palette()
        self.unselectables = [
            self.selection, self.selection_highlight, self.selection_rectangle,
            self.crosshair, self.information_label, self.camera.frame
        ]
        self.select_top_level_mobs = True
        self.regenerate_selection_search_set()

        self.is_selecting = False
        self.is_grabbing = False

        self.add(self.selection_highlight)

    def get_selection_rectangle(self):
        rect = Rectangle(
            stroke_color=self.selection_rectangle_stroke_color,
            stroke_width=self.selection_rectangle_stroke_width,
        )
        rect.fix_in_frame()
        rect.fixed_corner = ORIGIN
        rect.add_updater(self.update_selection_rectangle)
        return rect

    def update_selection_rectangle(self, rect: Rectangle):
        p1 = rect.fixed_corner
        p2 = self.mouse_point.get_center()
        rect.set_points_as_corners([
            p1,
            [p2[0], p1[1], 0],
            p2,
            [p1[0], p2[1], 0],
            p1,
        ])
        return rect

    def get_selection_highlight(self):
        result = Group()
        result.tracked_mobjects = []
        result.add_updater(self.update_selection_highlight)
        return result

    def update_selection_highlight(self, highlight: Mobject):
        if set(highlight.tracked_mobjects) == set(self.selection):
            return

        # Otherwise, refresh contents of highlight
        highlight.tracked_mobjects = list(self.selection)
        highlight.set_submobjects(
            [self.get_highlight(mob) for mob in self.selection])
        try:
            index = min((i for i, mob in enumerate(self.mobjects)
                         for sm in self.selection if sm in mob.get_family()))
            self.mobjects.remove(highlight)
            self.mobjects.insert(index - 1, highlight)
        except ValueError:
            pass

    def get_crosshair(self):
        line = Line(LEFT, RIGHT)
        line.insert_n_curves(1)
        lines = line.replicate(2)
        lines[1].rotate(PI / 2)
        crosshair = VMobject()
        crosshair.set_points([*lines[0].get_points(), *lines[1].get_points()])
        crosshair.set_width(self.crosshair_width)
        crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2])
        crosshair.set_animating_status(True)
        crosshair.fix_in_frame()
        return crosshair

    def get_color_palette(self):
        palette = VGroup(
            *(Square(fill_color=color, fill_opacity=1, side_length=1)
              for color in self.palette_colors))
        palette.set_stroke(width=0)
        palette.arrange(RIGHT, buff=0.5)
        palette.set_width(FRAME_WIDTH - 0.5)
        palette.to_edge(DOWN, buff=SMALL_BUFF)
        palette.fix_in_frame()
        return palette

    def get_information_label(self):
        loc_label = VGroup(*(DecimalNumber(**self.cursor_location_config)
                             for n in range(3)))

        def update_coords(loc_label):
            for mob, coord in zip(loc_label, self.mouse_point.get_location()):
                mob.set_value(coord)
            loc_label.arrange(RIGHT, buff=loc_label.get_height())
            loc_label.to_corner(DR, buff=SMALL_BUFF)
            loc_label.fix_in_frame()
            return loc_label

        loc_label.add_updater(update_coords)

        time_label = DecimalNumber(0, **self.time_label_config)
        time_label.to_corner(DL, buff=SMALL_BUFF)
        time_label.fix_in_frame()
        time_label.add_updater(lambda m, dt: m.increment_value(dt))

        return VGroup(loc_label, time_label)

    # Overrides
    def get_state(self):
        return SceneState(self,
                          ignore=[
                              self.selection_highlight,
                              self.selection_rectangle,
                              self.crosshair,
                          ])

    def restore_state(self, scene_state: SceneState):
        super().restore_state(scene_state)
        self.mobjects.insert(0, self.selection_highlight)

    def add(self, *mobjects: Mobject):
        super().add(*mobjects)
        self.regenerate_selection_search_set()

    def remove(self, *mobjects: Mobject):
        super().remove(*mobjects)
        self.regenerate_selection_search_set()

    # def increment_time(self, dt: float) -> None:
    #     super().increment_time(dt)

    # Related to selection

    def toggle_selection_mode(self):
        self.select_top_level_mobs = not self.select_top_level_mobs
        self.refresh_selection_scope()
        self.regenerate_selection_search_set()

    def get_selection_search_set(self) -> list[Mobject]:
        return self.selection_search_set

    def regenerate_selection_search_set(self):
        selectable = list(
            filter(lambda m: m not in self.unselectables, self.mobjects))
        if self.select_top_level_mobs:
            self.selection_search_set = selectable
        else:
            self.selection_search_set = [
                submob for mob in selectable
                for submob in mob.family_members_with_points()
            ]

    def refresh_selection_scope(self):
        curr = list(self.selection)
        if self.select_top_level_mobs:
            self.selection.set_submobjects([
                mob for mob in self.mobjects
                if any(sm in mob.get_family() for sm in curr)
            ])
            self.selection.refresh_bounding_box(recurse_down=True)
        else:
            self.selection.set_submobjects(
                extract_mobject_family_members(
                    curr,
                    exclude_pointless=True,
                ))

    def get_corner_dots(self, mobject: Mobject) -> Mobject:
        dots = DotCloud(**self.corner_dot_config)
        radius = self.corner_dot_config["radius"]
        if mobject.get_depth() < 1e-2:
            vects = [DL, UL, UR, DR]
        else:
            vects = np.array(list(it.product(*3 * [[-1, 1]])))
        dots.add_updater(lambda d: d.set_points(
            [mobject.get_corner(v) + v * radius for v in vects]))
        return dots

    def get_highlight(self, mobject: Mobject) -> Mobject:
        if isinstance(mobject, VMobject) and mobject.has_points(
        ) and not self.select_top_level_mobs:
            length = max([mobject.get_height(), mobject.get_width()])
            result = VHighlight(
                mobject,
                max_stroke_addition=min([50 * length, 10]),
            )
            result.add_updater(lambda m: m.replace(mobject, stretch=True))
            return result
        elif isinstance(mobject, DotCloud):
            return Mobject()
        else:
            return self.get_corner_dots(mobject)

    def add_to_selection(self, *mobjects: Mobject):
        mobs = list(
            filter(
                lambda m: m not in self.unselectables and m not in self.
                selection, mobjects))
        if len(mobs) == 0:
            return
        self.selection.add(*mobs)
        self.selection.set_animating_status(True)

    def toggle_from_selection(self, *mobjects: Mobject):
        for mob in mobjects:
            if mob in self.selection:
                self.selection.remove(mob)
                mob.set_animating_status(False)
            else:
                self.add_to_selection(mob)
        self.refresh_static_mobjects()

    def clear_selection(self):
        for mob in self.selection:
            mob.set_animating_status(False)
        self.selection.set_submobjects([])
        self.refresh_static_mobjects()

    def disable_interaction(self, *mobjects: Mobject):
        for mob in mobjects:
            for sm in mob.get_family():
                self.unselectables.append(sm)
        self.regenerate_selection_search_set()

    def enable_interaction(self, *mobjects: Mobject):
        for mob in mobjects:
            for sm in mob.get_family():
                if sm in self.unselectables:
                    self.unselectables.remove(sm)

    # Functions for keyboard actions

    def copy_selection(self):
        ids = map(id, self.selection)
        pyperclip.copy(",".join(map(str, ids)))

    def paste_selection(self):
        clipboard_str = pyperclip.paste()
        # Try pasting a mobject
        try:
            ids = map(int, clipboard_str.split(","))
            mobs = map(self.id_to_mobject, ids)
            mob_copies = [m.copy() for m in mobs if m is not None]
            self.clear_selection()
            self.play(*(FadeIn(mc, run_time=0.5, scale=1.5)
                        for mc in mob_copies))
            self.add_to_selection(*mob_copies)
            return
        except ValueError:
            pass
        # Otherwise, treat as tex or text
        if set("\\^=+").intersection(clipboard_str):  # Proxy to text for LaTeX
            try:
                new_mob = Tex(clipboard_str)
            except LatexError:
                return
        else:
            new_mob = Text(clipboard_str)
        self.clear_selection()
        self.add(new_mob)
        self.add_to_selection(new_mob)

    def delete_selection(self):
        self.remove(*self.selection)
        self.clear_selection()

    def enable_selection(self):
        self.is_selecting = True
        self.add(self.selection_rectangle)
        self.selection_rectangle.fixed_corner = self.mouse_point.get_center(
        ).copy()

    def gather_new_selection(self):
        self.is_selecting = False
        if self.selection_rectangle in self.mobjects:
            self.remove(self.selection_rectangle)
            additions = []
            for mob in reversed(self.get_selection_search_set()):
                if self.selection_rectangle.is_touching(mob):
                    additions.append(mob)
            self.add_to_selection(*additions)

    def prepare_grab(self):
        mp = self.mouse_point.get_center()
        self.mouse_to_selection = mp - self.selection.get_center()
        self.is_grabbing = True

    def prepare_resizing(self, about_corner=False):
        center = self.selection.get_center()
        mp = self.mouse_point.get_center()
        if about_corner:
            self.scale_about_point = self.selection.get_corner(center - mp)
        else:
            self.scale_about_point = center
        self.scale_ref_vect = mp - self.scale_about_point
        self.scale_ref_width = self.selection.get_width()
        self.scale_ref_height = self.selection.get_height()

    def toggle_color_palette(self):
        if len(self.selection) == 0:
            return
        if self.color_palette not in self.mobjects:
            self.save_state()
            self.add(self.color_palette)
        else:
            self.remove(self.color_palette)

    def display_information(self, show=True):
        if show:
            self.add(self.information_label)
        else:
            self.remove(self.information_label)

    def group_selection(self):
        group = self.get_group(*self.selection)
        self.add(group)
        self.clear_selection()
        self.add_to_selection(group)

    def ungroup_selection(self):
        pieces = []
        for mob in list(self.selection):
            self.remove(mob)
            pieces.extend(list(mob))
        self.clear_selection()
        self.add(*pieces)
        self.add_to_selection(*pieces)

    def nudge_selection(self, vect: np.ndarray, large: bool = False):
        nudge = self.selection_nudge_size
        if large:
            nudge *= 10
        self.selection.shift(nudge * vect)

    def save_selection_to_file(self):
        if len(self.selection) == 1:
            self.save_mobject_to_file(self.selection[0])
        else:
            self.save_mobject_to_file(self.selection)

    def on_key_press(self, symbol: int, modifiers: int) -> None:
        super().on_key_press(symbol, modifiers)
        char = chr(symbol)
        if char == SELECT_KEY and modifiers == 0:
            self.enable_selection()
        elif char in GRAB_KEYS and modifiers == 0:
            self.prepare_grab()
        elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
            self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
        elif symbol == SHIFT_SYMBOL:
            if self.window.is_key_pressed(ord("t")):
                self.prepare_resizing(about_corner=True)
        elif char == COLOR_KEY and modifiers == 0:
            self.toggle_color_palette()
        elif char == INFORMATION_KEY and modifiers == 0:
            self.display_information()
        elif char == "c" and modifiers == COMMAND_MODIFIER:
            self.copy_selection()
        elif char == "v" and modifiers == COMMAND_MODIFIER:
            self.paste_selection()
        elif char == "x" and modifiers == COMMAND_MODIFIER:
            self.copy_selection()
            self.delete_selection()
        elif symbol == DELETE_SYMBOL:
            self.delete_selection()
        elif char == "a" and modifiers == COMMAND_MODIFIER:
            self.clear_selection()
            self.add_to_selection(*self.mobjects)
        elif char == "g" and modifiers == COMMAND_MODIFIER:
            self.group_selection()
        elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
            self.ungroup_selection()
        elif char == "t" and modifiers == COMMAND_MODIFIER:
            self.toggle_selection_mode()
        elif char == "s" and modifiers == COMMAND_MODIFIER:
            self.save_selection_to_file()
        elif symbol in ARROW_SYMBOLS:
            self.nudge_selection(
                vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
                large=(modifiers & SHIFT_MODIFIER),
            )
        # Adding crosshair
        if char == CURSOR_KEY:
            if self.crosshair in self.mobjects:
                self.remove(self.crosshair)
            else:
                self.add(self.crosshair)
        if char == SELECT_KEY:
            self.add(self.crosshair)

        # Conditions for saving state
        if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
            self.save_state()

    def on_key_release(self, symbol: int, modifiers: int) -> None:
        super().on_key_release(symbol, modifiers)
        if chr(symbol) == SELECT_KEY:
            self.gather_new_selection()
            # self.remove(self.crosshair)
        if chr(symbol) in GRAB_KEYS:
            self.is_grabbing = False
        elif chr(symbol) == INFORMATION_KEY:
            self.display_information(False)
        elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(
                ord(RESIZE_KEY)):
            self.prepare_resizing(about_corner=False)

    # Mouse actions
    def handle_grabbing(self, point: np.ndarray):
        diff = point - self.mouse_to_selection
        if self.window.is_key_pressed(ord(GRAB_KEY)):
            self.selection.move_to(diff)
        elif self.window.is_key_pressed(ord(X_GRAB_KEY)):
            self.selection.set_x(diff[0])
        elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
            self.selection.set_y(diff[1])

    def handle_resizing(self, point: np.ndarray):
        if not hasattr(self, "scale_about_point"):
            return
        vect = point - self.scale_about_point
        if self.window.is_key_pressed(CTRL_SYMBOL):
            for i in (0, 1):
                scalar = vect[i] / self.scale_ref_vect[i]
                self.selection.rescale_to_fit(
                    scalar * [self.scale_ref_width, self.scale_ref_height][i],
                    dim=i,
                    about_point=self.scale_about_point,
                    stretch=True,
                )
        else:
            scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
            self.selection.set_width(scalar * self.scale_ref_width,
                                     about_point=self.scale_about_point)

    def handle_sweeping_selection(self, point: np.ndarray):
        mob = self.point_to_mobject(point,
                                    search_set=self.get_selection_search_set(),
                                    buff=SMALL_BUFF)
        if mob is not None:
            self.add_to_selection(mob)

    def choose_color(self, point: np.ndarray):
        # Search through all mobject on the screen, not just the palette
        to_search = [
            sm for mobject in self.mobjects
            for sm in mobject.family_members_with_points()
            if mobject not in self.unselectables
        ]
        mob = self.point_to_mobject(point, to_search)
        if mob is not None:
            self.selection.set_color(mob.get_color())
        self.remove(self.color_palette)

    def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
        super().on_mouse_motion(point, d_point)
        self.crosshair.move_to(point)
        if self.is_grabbing:
            self.handle_grabbing(point)
        elif self.window.is_key_pressed(ord(RESIZE_KEY)):
            self.handle_resizing(point)
        elif self.window.is_key_pressed(
                ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
            self.handle_sweeping_selection(point)

    def on_mouse_release(self, point: np.ndarray, button: int,
                         mods: int) -> None:
        super().on_mouse_release(point, button, mods)
        if self.color_palette in self.mobjects:
            self.choose_color(point)
            return
        mobject = self.point_to_mobject(
            point,
            search_set=self.get_selection_search_set(),
            buff=1e-4,
        )
        if mobject is not None:
            self.toggle_from_selection(mobject)
        else:
            self.clear_selection()