Initialized repo.

This commit is contained in:
2024-02-02 20:13:15 +01:00
commit fbe5cee417
23 changed files with 1465 additions and 0 deletions

0
src/__init__.py Normal file
View File

41
src/gtk/help-overlay.ui Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes" context="shortcut window">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Open</property>
<property name="action-name">win.open</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Save as</property>
<property name="action-name">win.save-as</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Show Shortcuts</property>
<property name="action-name">win.show-help-overlay</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Quit</property>
<property name="action-name">app.quit</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

122
src/main.py Normal file
View File

@@ -0,0 +1,122 @@
# main.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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw, GLib
from .window import TextViewerWindow
class TextViewerApplication(Adw.Application):
"""The main application singleton class."""
def __init__(self):
super().__init__(application_id='com.example.textviewer',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
self.create_action('about', self.on_about_action)
# keyboard SHORTCUTS for window actions
self.set_accels_for_action('win.open', ['<Ctrl>o']) # add a keyboard shortcut for the open action
self.set_accels_for_action('win.save-as', ['<Ctrl>s']) # add a keyboard shortcut for the save-as action
# the SETTINGS storage
self.settings = Gio.Settings(schema_id="com.example.textviewer")
# set the STYLE from SETTINGS
force_dark_mode = self.settings.get_boolean("force-dark-mode")
style_manager = Adw.StyleManager.get_default()
if force_dark_mode:
style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
else:
style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)
# the dark mode ACTION
dark_mode_action = Gio.SimpleAction(name="force_dark_mode",
state=GLib.Variant.new_boolean(force_dark_mode))
dark_mode_action.connect("activate", self.toggle_dark_mode)
dark_mode_action.connect("change-state", self.change_color_scheme)
self.add_action(dark_mode_action)
def do_activate(self):
"""Called when the application is activated.
We raise the application's main window, creating it if
necessary.
"""
win = self.props.active_window
if not win:
win = TextViewerWindow(application=self)
win.present()
def toggle_dark_mode(self, action, _):
state = action.get_state()
old_state = state.get_boolean()
new_state = not old_state
action.change_state(GLib.Variant.new_boolean(new_state))
def change_color_scheme(self, action, new_state):
force_dark_mode = new_state.get_boolean()
style_manager = Adw.StyleManager.get_default()
if force_dark_mode:
style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
else:
style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)
action.set_state(new_state)
self.settings.set_boolean("force-dark-mode", force_dark_mode)
def on_about_action(self, widget, _):
"""Callback for the app.about action."""
about = Adw.AboutWindow(transient_for=self.props.active_window,
application_name='text-viewer',
application_icon='com.example.textviewer',
developer_name='Benjamin Burkhardt',
version='0.1.0',
developers=['Benjamin Burkhardt'],
copyright='© 2024 Benjamin Burkhardt')
about.present()
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
Args:
name: the name of the action
callback: the function to be called when the action is
activated
shortcuts: an optional list of accelerators
"""
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)
if shortcuts:
self.set_accels_for_action(f"app.{name}", shortcuts)
def main(version):
"""The application's entry point."""
app = TextViewerApplication()
return app.run(sys.argv)

35
src/meson.build Normal file
View File

@@ -0,0 +1,35 @@
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
moduledir = pkgdatadir / 'text_viewer'
gnome = import('gnome')
gnome.compile_resources('text-viewer',
'text-viewer.gresource.xml',
gresource_bundle: true,
install: true,
install_dir: pkgdatadir,
)
python = import('python')
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').path())
conf.set('VERSION', meson.project_version())
conf.set('localedir', get_option('prefix') / get_option('localedir'))
conf.set('pkgdatadir', pkgdatadir)
configure_file(
input: 'text-viewer.in',
output: 'text-viewer',
configuration: conf,
install: true,
install_dir: get_option('bindir'),
install_mode: 'r-xr--r--'
)
text_viewer_sources = [
'__init__.py',
'main.py',
'window.py',
]
install_data(text_viewer_sources, install_dir: moduledir)

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/example/textviewer">
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>
</gresources>

46
src/text-viewer.in Executable file
View File

@@ -0,0 +1,46 @@
#!@PYTHON@
# text-viewer.in
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import signal
import locale
import gettext
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
locale.bindtextdomain('text-viewer', localedir)
locale.textdomain('text-viewer')
gettext.install('text-viewer', localedir)
if __name__ == '__main__':
import gi
from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'text-viewer.gresource'))
resource._register()
from text_viewer import main
sys.exit(main.main(VERSION))

170
src/window.py Normal file
View File

@@ -0,0 +1,170 @@
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

83
src/window.ui Normal file
View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="Adw" version="1.0"/>
<template class="TextViewerWindow" parent="AdwApplicationWindow">
<property name="default-width">600</property>
<property name="default-height">300</property>
<property name="title">Text Viewer</property>
<style>
<class name="devel"/>
</style>
<property name="content">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<child type="start">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="open_button">
<property name="label">Open</property>
<property name="action-name">win.open</property>
</object>
</child>
<child type="end">
<object class="GtkLabel" id="cursor_pos_text">
<property name="label">Ln 0, Col 0</property>
<style>
<class name="dim-label"/>
<class name="numeric"/>
</style>
</object>
</child>
</object>
</child>
<property name="content">
<object class="AdwToastOverlay" id="toast_overlay">
<property name="child">
<object class="GtkScrolledWindow">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="child">
<object class="GtkTextView" id="main_text_view">
<property name="monospace">true</property>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</template>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Save as...</attribute>
<attribute name="action">win.save-as</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Force dark mode</attribute>
<attribute name="action">app.force_dark_mode</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About Text-viewer</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>