# window.py # # Copyright 2024 Benjamin Burkhardt # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later from gi.repository import Adw, Gtk, Gio, GLib # Adw is Adwaita and GTK GTK :) # Gio is needed for actions etc. # and GLib for saving the file! @Gtk.Template(resource_path='/com/example/textviewer/window.ui') class TextViewerWindow(Adw.ApplicationWindow): __gtype_name__ = 'TextViewerWindow' main_text_view = Gtk.Template.Child() open_button = Gtk.Template.Child() cursor_pos_text = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) open_action = Gio.SimpleAction(name="open") open_action.connect("activate", self.open_file_dialog_action) self.add_action(open_action) save_action = Gio.SimpleAction(name="save-as") save_action.connect("activate", self.save_file_dialog_action) self.add_action(save_action) buffer = self.main_text_view.get_buffer() buffer.connect("notify::cursor-position", self.update_cursor_position) self.settings = Gio.Settings(schema_id="com.example.textviewer") self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT) self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT) self.settings.bind("window-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT) # Gio.Settings.bind method takes # 1. the key defined in the settings scheme to bind to # 2. the object which owns the property to bind the setting key to # 3. property: the property, e.g. default-width as defined in the XML! # 4. flags: BindFlags.DEFAULT means it's a kind of bidirectional rw bound def open_file_dialog_action(self, action, _): native = Gtk.FileDialog() native.open(self, None, self.on_open_response) def on_open_response(self, dialog, result): try: file = dialog.open_finish(result) except: # if the user dismisses the dialog, a GError/sth else is raised return # If the user selected a file... if file is not None: # ... open it self.open_file(file) def open_file(self, file): file.load_contents_async(None, self.open_file_complete) def open_file_complete(self, file, result): info = file.query_info("standard::display-name", Gio.FileQueryInfoFlags.NONE) if info: display_name = info.get_attribute_string("standard::display-name") else: display_name = file.get_basename() contents = file.load_contents_finish(result) if not contents[0]: path = file.peek_path() print(f"Unable to open {path}: {contents[1]}") self.toast_overlay.add_toast(Adw.Toast(title=f"Unable to open “{display_name}”")) try: text = contents[1].decode('utf-8') except UnicodeError as err: path = file.peek_path() print(f"Unable to load the contents of {path}: the file is not encoded with UTF-8") self.toast_overlay.add_toast(Adw.Toast(title=f"Invalid encoding for {display_name}")) return self.set_title(display_name) buffer = self.main_text_view.get_buffer() buffer.set_text(text) start = buffer.get_start_iter() buffer.place_cursor(start) def update_cursor_position(self, buffer, _): # Retrieve the value of the "cursor-position" property cursor_pos = buffer.props.cursor_position # Construct the text iterator for the position of the cursor iter = buffer.get_iter_at_offset(cursor_pos) line = iter.get_line() + 1 column = iter.get_line_offset() + 1 # Set the new contents of the label self.cursor_pos_text.set_text(f"Ln {line}, Col {column}") def save_file_dialog_action(self, action, _): native = Gtk.FileDialog() native.save(self, None, self.on_save_response) def on_save_response(self, dialog, result): try: file = dialog.save_finish(result) except: # if the user dismisses the dialog, a GError/sth else is raised return if file is not None: self.save_file(file) def save_file(self, file): buffer = self.main_text_view.get_buffer() # Retrieve the iterator at the start of the buffer start = buffer.get_start_iter() # Retrieve the iterator at the end of the buffer end = buffer.get_end_iter() # Retrieve all the visible text between the two bounds text = buffer.get_text(start, end, False) # If there is nothing to save, return early if not text: return bytes = GLib.Bytes.new(text.encode('utf-8')) # Start the asynchronous operation to save the data into the file file.replace_contents_bytes_async(bytes, None, False, Gio.FileCreateFlags.NONE, None, self.save_file_complete) def save_file_complete(self, file, result): res = file.replace_contents_finish(result) info = file.query_info("standard::display-name", Gio.FileQueryInfoFlags.NONE) if info: display_name = info.get_attribute_string("standard::display-name") else: display_name = file.get_basename() if not res: print(f"Unable to save {display_name}") self.toast_overlay.add_toast(Adw.Toast(title=f"Unable to save {display_name}")) else: self.toast_overlay.add_toast(Adw.Toast(title=f"Saved to {display_name}")) self.set_title(display_name)