PyGTK 3 - Cut, Copy, Paste, Select All Functions - How to simply allow default handling after clicking on menu item?

The “Ctrl+C” and “Ctrl+V” shortcuts (as well as the “right click menu”) are available by default in any GTK application, for example a simple hello world app with only a SourceView (see below).
But if I add a menu item “Edit->Copy” and assign the “Ctrl+C” accelerator to it and a corresponding callback function, than it obviously stops working since I am intercepting the signal with my own method. So, how can I trigger the default cut/copy/paste/select_all functionalities inside my custom method?

Note: returning False works for the Paste function but not for Copy/Cut/Select All

Simple example - In this case all functions (cut/copy/paste/select all) work fine.

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '3.0') 
from gi.repository import Gtk, Gdk, Pango, GObject, GtkSource

class MyOwnApp(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Hello World")
        self.set_default_size(500, 500)

        self.vbox = Gtk.VBox()

        editor = GtkSource.View.new()
        editor.set_show_line_numbers(True)
        editor.set_auto_indent(True)
        editor_buffer = editor.get_buffer()
        self.vbox.pack_start(editor, False, False, 0)

        self.add(self.vbox)

 win = MyOwnApp()
 win.connect("destroy", Gtk.main_quit)
 win.show_all()
 Gtk.main()

If I add a menu item with a callback they don’t work anymore.

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '3.0')
from gi.repository import Gtk, Gdk, Pango, GObject, GtkSource

class MyOwnApp(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Hello World")

        self.set_default_size(900, 900)

        box_outer = Gtk.VBox()

        # MENUBAR setup
        menuBar = Gtk.MenuBar()
        # Set accelerators
        agr = Gtk.AccelGroup()
        self.add_accel_group(agr)

        # File menu
        file_menu_dropdown = Gtk.MenuItem("File")
        menuBar.append(file_menu_dropdown)
        file_menu = Gtk.Menu()
        file_menu_dropdown.set_submenu(file_menu)
        
        # File menu Items
        file_exit = Gtk.MenuItem("Exit")
        key, mod = Gtk.accelerator_parse("<Control>Q")
        file_exit.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        file_exit.connect("activate", self.quit)

        file_menu.append(file_exit)

        # Edit menu
        edit_menu_dropdown = Gtk.MenuItem("Edit")
        menuBar.append(edit_menu_dropdown)
        edit_menu = Gtk.Menu()
        edit_menu_dropdown.set_submenu(edit_menu)

        # Edit menu Items
        edit_cut = Gtk.MenuItem("Cut")
        key, mod = Gtk.accelerator_parse("<Control>X")
        edit_cut.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_cut.connect("activate", self.on_toolbutton_cut_clicked)

        edit_copy = Gtk.MenuItem("Copy")
        key, mod = Gtk.accelerator_parse("<Control>C")
        edit_copy.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_copy.connect("activate", self.on_toolbutton_copy_clicked)

        edit_paste = Gtk.MenuItem("Paste")
        key, mod = Gtk.accelerator_parse("<Control>V")
        edit_paste.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_paste.connect("activate", self.on_toolbutton_paste_clicked)

        edit_select_all = Gtk.MenuItem("Select All")
        key, mod = Gtk.accelerator_parse("<Control>A")
        edit_select_all.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_select_all.connect("activate", self.on_toolbutton_select_all_clicked)

        edit_menu.append(edit_select_all)
        edit_menu.append(edit_cut)
        edit_menu.append(edit_copy)
        edit_menu.append(edit_paste)

        box_outer.pack_start(menuBar, False, False, 0)
        
        # SourceView
        editor = GtkSource.View.new()
        editor.set_show_line_numbers(True)
        editor.set_auto_indent(True)
        editor_buffer = editor.get_buffer()
        box_outer.pack_start(editor, True, True, 0)

        self.add(box_outer)

    def quit(self,widget=None):
        Gtk.main_quit()

    def on_toolbutton_select_all_clicked(self, widget):
        return False

    def on_toolbutton_cut_clicked(self, widget):
        return False

    def on_toolbutton_copy_clicked(self, widget):
        return False

    def on_toolbutton_paste_clicked(self, widget):
        return False

win = MyOwnApp()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

Any good reason why you are using a lot of discontinued methods and widgets? :slight_smile:

1 Like

Thanks for your reply. There’s a half-good reason for that: I found better / easier to understand documentation for these methods (even if deprecated). So, I started using those and then I will update / refactor the menubar. Actually, since I hit a wall with this issue, I’ve already started looking into Gio.Action and found a good example (after spending an insane amount of hours researching).
Anyway, coming back to the current issue, do you have any solution? Would this problem disappear after updating to Gio.Action, Gio.Menu and Gio.MenuItem? I’m developing an open source app and would really like to move past this issue (which I’m sure is super-easy since almost every app needs to implement a similar functionality)

Using accelerator with a menu item is kinda new for me. You can have a look at this thread on how to use menu with apps : How to create menus for apps using Python?

I’m now reading the docs on how to use accelerators, so I will soon update this thread with a code example. :smile:

So after some trial and error, this is a bit modernised version that mimics your app. I didn’t use menu-bar or other for simplicity.

Code
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gio, Gtk, GtkSource as Gsr


class App(Gtk.Application):

    def __init__(self):
        Gtk.Application.__init__(self, application_id="com.example.example")
        self.headerbar = Gtk.HeaderBar(title="My Editor", show_close_button=True)
        self.win = Gtk.ApplicationWindow()
        self.win.set_titlebar(self.headerbar)
        self.buf = Gsr.Buffer()
        self.textview = Gsr.View(buffer=self.buf)
        self.win.add(self.textview)
        self.pack_menu()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        self.win.set_application(self)

    def do_activate(self):
        Gtk.Application.do_activate(self)
        self.win.show_all()

    def copy_text(self, action, arg):

        print("Now you should copy text")

    def paste_text(self, action, arg):

        print("Now you should paste text")

    def pack_menu(self):

        copy_action = Gio.SimpleAction(name="copy")
        copy_action.connect("activate", self.copy_text)
        paste_action = Gio.SimpleAction(name="paste")
        paste_action.connect("activate", self.paste_text)

        self.add_action(copy_action)
        self.add_action(paste_action)

        menu = Gio.Menu()
        menu.append("Copy", "app.copy")
        menu.append("Paste", "app.paste")

        self.set_accels_for_action("app.copy", ["<Ctrl>C"])
        self.set_accels_for_action("app.paste", ["<Ctrl>P"])

        button = Gtk.Button.new_from_icon_name("menu", Gtk.IconSize.MENU)
        self.headerbar.pack_end(button)

        popover = Gtk.Popover.new_from_model(button, menu)
        button.connect("clicked", lambda *args: popover.popup())

app = App()
app.run()

As noted by you, Ctrl-C activates the callback but doesn’t copy the text. This limitation can be worked out with some nasty work-around, but do we really need it ?

  • It has become a universal law that Ctrl-C copies the selection and Ctrl-V pastes it. So the text-view automatically does that without setting a manual implementation.
  • By trying to control the behavior, we are not letting the key-press pass to the text-view, which results in the expected weird behavior.

So what can you do?
Leave the key-binding but keep the menu-item. When users click the menu-item, let it call the function to simulate the expected behavior. For example, the act of copying can be done with the help of Gtk.TextBuffer.get_selection_bounds and similar methods.

How to show the accelerator near menu-item?
This can be done using Gtk.AccelLabel and populating popover with it. But I don’t know how to use Gio.Action with the same. I looked at a few GNOME apps on how they are done. Actually these apps do not show the accelerators near the menu-item at all.
Instead they have an additional menu-item “Keyboard Shortcuts” that shows us the available shortcuts. It can be done using Gtk.ShortcutsWindow. Quite easy isn’t it ?

I think I answered all your questions, if not point me which one :wink:

First of all thank you very much!! You can’t imagine how happy I am that I finally found a place to further expand my knowledge about GTK-apps development.

This is good, but what happens when the window has two or more text-based widgets (for example a Gtk.Entry plus the GtkSourceView)? If the user is typing inside the Entry widget and presses Ctrl+ C the selected text should be the one inside the Gtk.Entry and not the one inside the GtkSourceView. The solution I can think of is the following one:

  • Fist check which widget has the focus (Gtk.Window.get_focus - see docs)
  • Grab the text buffer of the focused widget
  • Retrieve the selection and copy it to the clipboard
    But I just saw that the Gtk.EntryBuffer (docs) doesn’t seem to have any method to retrieve the selection (you can only retrieve ALL the text).

Main point:
So, after this consideration, what should I do inside the action callback (for example the copy_text one)? How can I perform “cut/copy/paste/select all” functions across different widgets?

Additional considerations:
Anyway, your code is very nice… there is a lot to digest since it is “modernized”. I’ll do a little research because I’m a little confused about the recommended structure of the “modern” Hello World app…

  • Gtk.Application vs Gtk.ApplicationWindow vs Gtk.Window
  • do_startup() & do_activate()
  • if __name__ == '__main__'
  • app.run(sys.argv)… etc

I’ll try to look into all these things and maybe I’ll ask you some clarifications :blush:

1 Like

Thanks for follow-up.

As long as you don’t catch the CtrlC key-press on your own, the widgets assume the default behavior. These are part of universal norms. Like when you press Enter on a button when it’s focused, it automatically takes it as as “activate” signal. Similarly, entry widgets and text-view themselves take care of copy and pasting, so we need not worry ^^

So the solution you proposed is re-inventing the wheel.

The cut-copy-paste can be done using Gtk.Clipboard. The clipboard is advanced such that it can be extended to stuff more than text.

Sorry for the deviation. I actually wrote the code to test how things work, so I was not aware that I will be sharing it. The same reason, I wanted to keep it simple. The docs on Gtk.Application is well-written to kick-start our interest. And don’t forget to ask (me and us) if you have any doubt. :slight_smile:

After too many hours of research, I’m happy to post this solution for all the GTK enthusiast out there!!

With this solution, you can use the cut/copy/paste/selectAll functions across multiple widgets inside a window (with both Gtk.Entry and GtkSource.View).
The key point is that these two widgets use different methods for the cut/copy/paste/selectAll functionalities, but (as expected) they both have default methods to manage these basic functionalities. No need to re-invent the wheel.
Note: The Gtk.Entry widget inherits from the Gtk.Editable interface, which has all the necessary functions to fallback to the default handling of cut/copy/past/selectAll.

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GtkSource', '3.0')
from gi.repository import Gtk, Gdk, GtkSource

class MyWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Hello World")

        self.set_default_size(900, 900)

        box_outer = Gtk.VBox()

        # MENUBAR setup
        menuBar = Gtk.MenuBar()
        # Set accelerators
        agr = Gtk.AccelGroup()
        self.add_accel_group(agr)

        # File menu
        file_menu_dropdown = Gtk.MenuItem("File")
        menuBar.append(file_menu_dropdown)
        file_menu = Gtk.Menu()
        file_menu_dropdown.set_submenu(file_menu)

        #File menu Items
        file_exit = Gtk.MenuItem("Exit")
        key, mod = Gtk.accelerator_parse("<Control>Q")
        file_exit.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        file_exit.connect("activate", self.quit)
        
        file_menu.append(file_exit)

        # Edit menu
        edit_menu_dropdown = Gtk.MenuItem("Edit")
        menuBar.append(edit_menu_dropdown)
        edit_menu = Gtk.Menu()
        edit_menu_dropdown.set_submenu(edit_menu)

        # Edit menu Items
        edit_cut = Gtk.MenuItem("Cut")
        key, mod = Gtk.accelerator_parse("<Control>X")
        edit_cut.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_cut.connect("activate", self.on_toolbutton_cut_clicked)

        edit_copy = Gtk.MenuItem("Copy")
        key, mod = Gtk.accelerator_parse("<Control>C")
        edit_copy.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_copy.connect("activate", self.on_toolbutton_copy_clicked)

        edit_paste = Gtk.MenuItem("Paste")
        key, mod = Gtk.accelerator_parse("<Control>V")
        edit_paste.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_paste.connect("activate", self.on_toolbutton_paste_clicked)

        edit_select_all = Gtk.MenuItem("Select All")
        key, mod = Gtk.accelerator_parse("<Control>A")
        edit_select_all.add_accelerator("activate", agr, key, mod, Gtk.AccelFlags.VISIBLE)
        edit_select_all.connect("activate", self.on_toolbutton_select_all_clicked)

        edit_menu.append(edit_select_all)
        edit_menu.append(edit_cut)
        edit_menu.append(edit_copy)
        edit_menu.append(edit_paste)

        box_outer.pack_start(menuBar, False, False, 0)
        
        entry = Gtk.Entry()
        box_outer.pack_start(entry, False, False, 0)

        editor = GtkSource.View.new()
        editor.set_show_line_numbers(True)
        editor.set_auto_indent(True)
        box_outer.pack_start(editor, True, True, 0)

        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)

        self.add(box_outer)

    def quit(self,widget=None):
        Gtk.main_quit()

    def on_toolbutton_select_all_clicked(self, widget):
        focusedWidget = self.get_focus()
        if focusedWidget is not None:
            if focusedWidget.has_focus():
                if str(type(focusedWidget)) == "<class 'gi.repository.Gtk.Entry'>":
                    focusedWidget.select_region(0, -1)
                elif str(type(focusedWidget)) == "<class 'gi.repository.GtkSource.View'>":
                    editor_buffer = focusedWidget.get_buffer()
                    editor_buffer.select_range(editor_buffer.get_start_iter(), editor_buffer.get_end_iter())
                else:
                    pass

    def on_toolbutton_cut_clicked(self, widget):
        focusedWidget = self.get_focus()
        if focusedWidget is not None:
            if focusedWidget.has_focus():
                if str(type(focusedWidget)) == "<class 'gi.repository.Gtk.Entry'>":
                    focusedWidget.cut_clipboard()
                elif str(type(focusedWidget)) == "<class 'gi.repository.GtkSource.View'>":
                    editor_buffer = focusedWidget.get_buffer()
                    editor_buffer.cut_clipboard(self.clipboard, editor_buffer)
                else:
                    pass

    def on_toolbutton_copy_clicked(self, widget):
        focusedWidget = self.get_focus()
        if focusedWidget is not None:
            if focusedWidget.has_focus():
                if str(type(focusedWidget)) == "<class 'gi.repository.Gtk.Entry'>":
                    focusedWidget.copy_clipboard()
                elif str(type(focusedWidget)) == "<class 'gi.repository.GtkSource.View'>":
                    editor_buffer = focusedWidget.get_buffer()
                    editor_buffer.copy_clipboard(self.clipboard)
                else:
                    pass

    def on_toolbutton_paste_clicked(self, widget):
        focusedWidget = self.get_focus()
        if focusedWidget is not None:
            if focusedWidget.has_focus():
                if str(type(focusedWidget)) == "<class 'gi.repository.Gtk.Entry'>":
                    focusedWidget.paste_clipboard()
                elif str(type(focusedWidget)) == "<class 'gi.repository.GtkSource.View'>":
                    editor_buffer = focusedWidget.get_buffer()
                    editor_buffer.paste_clipboard(self.clipboard, None, editor_buffer)
                else:
                    pass

win = MyWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

Note: I also posted this solution on Stackoverflow (link) since its content seems to get better indexed on search engines.

@j_arun_mani If you have any suggestion or comment, please feel free to share. Thanks for your input.
P.s. Next up I will update the basic structure of this example app (using Gtk.Application etc), but that’s a separate topic.

1 Like

Great you were able to do something. But I strongly encourage you to move from deprecated methods. I re-wrote the same code of yours for your reference.

Code
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("GtkSource", "4")
from gi.repository import Gdk, Gio, Gtk, GtkSource as Gsr


class App(Gtk.Application):

    def __init__(self):
        Gtk.Application.__init__(self, application_id="com.example.example")

        self.win = Gtk.ApplicationWindow(title="Hello World",
                                         default_height=900,
                                         default_width=900)
        self.menu = Gio.Menu()
        self.menubar = Gtk.MenuBar.new_from_model(self.menu)
        self.entry = Gtk.Entry()
        self.buffer = Gsr.Buffer()
        self.textview = Gsr.View(buffer=self.buffer,
                                 show_line_numbers=True,
                                 auto_indent=True)

        self.clipboard = Gtk.Clipboard.get_default(Gdk.Display.get_default())

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.pack_start(self.menubar, False, False, 0)
        box.pack_start(self.entry, False, False, 0)
        box.pack_start(self.textview, False, False, 0)
        self.win.add(box)
        self.pack_menu()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        self.win.set_application(self)

    def do_activate(self):
        Gtk.Application.do_activate(self)
        self.win.show_all()

    def copy_text(self, action, arg):

        focused = self.win.get_focus()
        if focused is self.entry:
            self.entry.copy_clipboard()
        elif focused is self.textview:
            self.buffer.copy_clipboard(self.clipboard)

    def cut_text(self, action, arg):

        focused = self.win.get_focus()
        if focused is self.entry:
            self.entry.cut_clipboard()
        elif focused is self.textview:
            self.buffer.cut_clipboard(self.clipboard, True)

    def paste_text(self, action, arg):

        focused = self.win.get_focus()
        if focused is self.entry:
            self.entry.paste_clipboard()
        elif focused is self.textview:
            self.buffer.paste_clipboard(self.clipboard, None, True)

    def select_text(self, action, arg):

        focused = self.win.get_focus()
        if focused is self.entry:
            self.entry.select_region(0, -1)
        elif focused is self.textview:
            start, end = self.buffer.get_bounds()
            self.buffer.select_range(start, end)

    def quit_(self, action, arg):

        self.quit()

    def pack_menu(self):

        copy_action = Gio.SimpleAction(name="copy")
        copy_action.connect("activate", self.copy_text)

        cut_action = Gio.SimpleAction(name="cut")
        cut_action.connect("activate", self.cut_text)

        paste_action = Gio.SimpleAction(name="paste")
        paste_action.connect("activate", self.paste_text)

        select_action = Gio.SimpleAction(name="select")
        select_action.connect("activate", self.select_text)

        quit_action = Gio.SimpleAction(name="quit")
        quit_action.connect("activate", self.quit_) # Gtk.Application already has a quit method

        self.add_action(copy_action)
        self.add_action(cut_action)
        self.add_action(paste_action)
        self.add_action(select_action)
        self.add_action(quit_action)

        file_menu = Gio.Menu()
        file_menu.append("Quit", "app.quit")

        edit_menu = Gio.Menu()
        edit_menu.append("Copy", "app.copy")
        edit_menu.append("Cut", "app.cut")
        edit_menu.append("Paste", "app.paste")
        edit_menu.append("Select all", "app.select")

        self.menu.append_submenu("File", file_menu)
        self.menu.append_submenu("Edit", edit_menu)

        self.set_accels_for_action("app.copy", ["<Ctrl>C"])
        self.set_accels_for_action("app.cut", ["<Ctrl>X"])
        self.set_accels_for_action("app.paste", ["<Ctrl>V"])
        self.set_accels_for_action("app.select", ["<Ctrl>A"])
        self.set_accels_for_action("app.quit", ["<Ctrl>Q"])


app = App()
app.run()

The difference in our code comes in form of making menu. Also there are some interesting points.

  • Have a look at Human Interface Guidelines available in GNOME website. It tells you about the awesome ways of making beautiful apps. From HIG, having a “Quit” menu-item or button is discouraged. The principle is, when user clicks on the “close” button of the main window, your app quits.
  • Don’t compare types. Instead use Gtk.Widget.set_name and Gtk.Widget.get_name. In the code I used is operator. When your widgets collection grows, you should go with get/set name methods.
  • Comment the self.pack_menu() in __init__ and you will see the app is behaving properly. It’s just you don’t have explicit menu-items for obvious things and a clutter-less interface.
  • Always keep an eye on using warnings, we don’t our users to see the dreadful messages.

The basic goal of every developer is to make the best app.
I hope you won’t find my comments irritating, it’s that I don’t want you to use a less-efficient method just because it’s working.

Cheers :smile:

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.