Initialized repo.
This commit is contained in:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
41
src/gtk/help-overlay.ui
Normal file
41
src/gtk/help-overlay.ui
Normal 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
122
src/main.py
Normal 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
35
src/meson.build
Normal 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)
|
7
src/text-viewer.gresource.xml
Normal file
7
src/text-viewer.gresource.xml
Normal 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
46
src/text-viewer.in
Executable 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
170
src/window.py
Normal 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
83
src/window.ui
Normal 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>
|
Reference in New Issue
Block a user