generated from itdominator/Python-With-Gtk-Template
Restructuring buffer access to provide better separation for future work
This commit is contained in:
parent
c88fec7a27
commit
7a343e39e8
|
@ -0,0 +1,23 @@
|
|||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('GtkSource', '4')
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
# NOTE: GtkSource 5 allows for smart indent action by allowing us to override the default auto indent logic...
|
||||
# In the long run this will be better because we can check not only for :, ;, { or other things but apply per language such as bash where
|
||||
# there isn't a special char but words...
|
||||
# class AutoIndenter(GtkSource.Indenter):
|
||||
# def __init__(self):
|
||||
# ...
|
||||
#
|
||||
# def indent(self, view, iter):
|
||||
# ...
|
||||
#
|
||||
# def is_trigger(self, view, iter, modifier, keyval):
|
||||
# print(iter.get_char())
|
|
@ -105,4 +105,4 @@ class PythonCompletionProvider(GObject.Object, GtkSource.CompletionProvider):
|
|||
try:
|
||||
return self._theme.load_icon(Gtk.STOCK_ADD, 16, 0)
|
||||
except:
|
||||
return None
|
||||
return None
|
|
@ -64,14 +64,14 @@ class FileEventsMixin:
|
|||
self.update_labels(gfile)
|
||||
return
|
||||
|
||||
file = GtkSource.File()
|
||||
file = GtkSource.File()
|
||||
buffer = self.get_buffer()
|
||||
file.set_location(gfile)
|
||||
self._file_loader = GtkSource.FileLoader.new(self._buffer, file)
|
||||
self._file_loader = GtkSource.FileLoader.new(buffer, file)
|
||||
|
||||
def finish_load_callback(obj, res, user_data=None):
|
||||
def finish_load_callback(obj, res, user_data = None):
|
||||
self._file_loader.load_finish(res)
|
||||
self._document_loaded()
|
||||
self.got_to_line(line)
|
||||
self._document_loaded(line)
|
||||
self.update_labels(gfile)
|
||||
self._loading_file = False
|
||||
|
||||
|
@ -97,7 +97,8 @@ class FileEventsMixin:
|
|||
Gio.FileMonitorEvent.RENAMED,
|
||||
Gio.FileMonitorEvent.MOVED_IN,
|
||||
Gio.FileMonitorEvent.MOVED_OUT]:
|
||||
self._buffer.set_modified(True)
|
||||
buffer = self.get_buffer()
|
||||
buffer.set_modified(True)
|
||||
|
||||
if eve_type in [ Gio.FileMonitorEvent.CHANGES_DONE_HINT ]:
|
||||
if self._ignore_internal_change:
|
||||
|
@ -116,16 +117,16 @@ class FileEventsMixin:
|
|||
def _write_file(self, gfile, save_as = False):
|
||||
if not gfile: return
|
||||
|
||||
buffer = self.get_buffer()
|
||||
with open(gfile.get_path(), 'w') as f:
|
||||
if not save_as:
|
||||
self._ignore_internal_change = True
|
||||
|
||||
start_itr = self._buffer.get_start_iter()
|
||||
end_itr = self._buffer.get_end_iter()
|
||||
text = self._buffer.get_text(start_itr, end_itr, True)
|
||||
start_itr = buffer.get_start_iter()
|
||||
end_itr = buffer.get_end_iter()
|
||||
text = buffer.get_text(start_itr, end_itr, True)
|
||||
|
||||
f.write(text)
|
||||
f.close()
|
||||
|
||||
self._buffer.set_modified(False)
|
||||
buffer.set_modified(False)
|
||||
return gfile
|
||||
|
|
|
@ -13,8 +13,10 @@ from gi.repository import Gtk
|
|||
class MarkEventsMixin:
|
||||
|
||||
def keyboard_insert_mark(self, target_iter = None, is_keyboard_insert = True):
|
||||
buffer = self.get_buffer()
|
||||
|
||||
if not target_iter:
|
||||
target_iter = self._buffer.get_iter_at_mark( self._buffer.get_insert() )
|
||||
target_iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
|
||||
found_mark = self.check_for_insert_marks(target_iter, is_keyboard_insert)
|
||||
if not found_mark:
|
||||
|
@ -22,7 +24,7 @@ class MarkEventsMixin:
|
|||
hash = "%032x" % random_bits
|
||||
mark = Gtk.TextMark.new(name = f"multi_insert_{hash}", left_gravity = False)
|
||||
|
||||
self._buffer.add_mark(mark, target_iter)
|
||||
buffer.add_mark(mark, target_iter)
|
||||
self._multi_insert_marks.append(mark)
|
||||
mark.set_visible(True)
|
||||
|
||||
|
@ -38,13 +40,15 @@ class MarkEventsMixin:
|
|||
self.keyboard_insert_mark(target_iter, is_keyboard_insert = False)
|
||||
|
||||
def check_for_insert_marks(self, target_iter, is_keyboard_insert):
|
||||
marks = target_iter.get_marks()
|
||||
marks = target_iter.get_marks()
|
||||
buffer = self.get_buffer()
|
||||
found_mark = False
|
||||
|
||||
for mark in marks:
|
||||
for _mark in self._multi_insert_marks:
|
||||
if _mark == mark:
|
||||
mark.set_visible(False)
|
||||
self._buffer.delete_mark(mark)
|
||||
buffer.delete_mark(mark)
|
||||
found_mark = True
|
||||
break
|
||||
|
||||
|
@ -60,39 +64,41 @@ class MarkEventsMixin:
|
|||
return found_mark
|
||||
|
||||
def keyboard_clear_marks(self):
|
||||
self._buffer.begin_user_action()
|
||||
buffer = self.get_buffer()
|
||||
|
||||
buffer.begin_user_action()
|
||||
|
||||
for mark in self._multi_insert_marks:
|
||||
mark.set_visible(False)
|
||||
self._buffer.delete_mark(mark)
|
||||
buffer.delete_mark(mark)
|
||||
|
||||
self._multi_insert_marks.clear()
|
||||
self._buffer.end_user_action()
|
||||
buffer.end_user_action()
|
||||
|
||||
|
||||
def _update_multi_line_markers(self, text_str):
|
||||
def _update_multi_line_markers(self, buffer, text_str):
|
||||
for mark in self._multi_insert_marks:
|
||||
iter = self._buffer.get_iter_at_mark(mark)
|
||||
self._buffer.insert(iter, text_str, -1)
|
||||
iter = buffer.get_iter_at_mark(mark)
|
||||
buffer.insert(iter, text_str, -1)
|
||||
|
||||
self.end_user_action()
|
||||
self.end_user_action(buffer)
|
||||
|
||||
def _delete_on_multi_line_markers(self):
|
||||
iter = self._buffer.get_iter_at_mark( self._buffer.get_insert() )
|
||||
self._buffer.backspace(iter, interactive = True, default_editable = True)
|
||||
def _delete_on_multi_line_markers(self, buffer):
|
||||
iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
buffer.backspace(iter, interactive = True, default_editable = True)
|
||||
|
||||
for mark in self._multi_insert_marks:
|
||||
iter = self._buffer.get_iter_at_mark(mark)
|
||||
self._buffer.backspace(iter, interactive = True, default_editable = True)
|
||||
iter = buffer.get_iter_at_mark(mark)
|
||||
buffer.backspace(iter, interactive = True, default_editable = True)
|
||||
|
||||
self.end_user_action()
|
||||
self.end_user_action(buffer)
|
||||
|
||||
def begin_user_action(self):
|
||||
def begin_user_action(self, buffer):
|
||||
if len(self._multi_insert_marks) > 0:
|
||||
self._buffer.begin_user_action()
|
||||
buffer.begin_user_action()
|
||||
self.freeze_multi_line_insert = True
|
||||
|
||||
def end_user_action(self):
|
||||
def end_user_action(self, buffer):
|
||||
if len(self._multi_insert_marks) > 0:
|
||||
self._buffer.end_user_action()
|
||||
buffer.end_user_action()
|
||||
self.freeze_multi_line_insert = False
|
||||
|
|
|
@ -12,6 +12,7 @@ from gi.repository import Gio
|
|||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
# from .auto_indenter import AutoIndenter
|
||||
from .source_view_events import SourceViewEventsMixin
|
||||
from .custom_completion_providers.example_completion_provider import ExampleCompletionProvider
|
||||
from .custom_completion_providers.python_completion_provider import PythonCompletionProvider
|
||||
|
@ -36,7 +37,6 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
self._skip_file_load = False
|
||||
self._ignore_internal_change = False
|
||||
self._loading_file = False
|
||||
self._buffer = self.get_buffer()
|
||||
self._completion = self.get_completion()
|
||||
self._px_value = settings.theming.default_zoom
|
||||
|
||||
|
@ -55,6 +55,7 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
ctx.add_class("source-view")
|
||||
ctx.add_class(f"px{self._px_value}")
|
||||
|
||||
self.set_vexpand(True)
|
||||
|
||||
self.set_show_line_marks(True)
|
||||
self.set_show_line_numbers(True)
|
||||
|
@ -67,12 +68,13 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
self.set_show_right_margin(True)
|
||||
self.set_right_margin_position(80)
|
||||
self.set_background_pattern(0) # 0 = None, 1 = Grid
|
||||
# NOTE: Add back once we move to Gtk 4 and use GtkSource 5
|
||||
# self.set_indenter( AutoIndenter() )
|
||||
|
||||
self._create_default_tag()
|
||||
self.set_buffer_language()
|
||||
self.set_buffer_style()
|
||||
|
||||
self.set_vexpand(True)
|
||||
buffer = self.get_buffer()
|
||||
self._create_default_tag(buffer)
|
||||
self.set_buffer_language(buffer)
|
||||
self.set_buffer_style(buffer)
|
||||
|
||||
|
||||
def _setup_signals(self):
|
||||
|
@ -84,10 +86,12 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
self.connect("button-press-event", self._button_press_event)
|
||||
self.connect("scroll-event", self._scroll_event)
|
||||
|
||||
self._buffer.connect('changed', self._is_modified)
|
||||
self._buffer.connect("mark-set", self._on_cursor_move)
|
||||
self._buffer.connect('insert-text', self._insert_text)
|
||||
self._buffer.connect('modified-changed', self._buffer_modified_changed)
|
||||
buffer = self.get_buffer()
|
||||
buffer.connect('changed', self._is_modified)
|
||||
buffer.connect("mark-set", self._on_cursor_move)
|
||||
buffer.connect('insert-text', self._insert_text)
|
||||
buffer.connect('modified-changed', self._buffer_modified_changed)
|
||||
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
...
|
||||
|
@ -96,14 +100,15 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
...
|
||||
|
||||
|
||||
def _document_loaded(self):
|
||||
def _document_loaded(self, line: int = 0):
|
||||
for provider in self._completion.get_providers():
|
||||
self._completion.remove_provider(provider)
|
||||
|
||||
# TODO: actually load a meaningful provider based on file type...
|
||||
file = self._current_file.get_path()
|
||||
file = self._current_file.get_path()
|
||||
buffer = self.get_buffer()
|
||||
word_completion = GtkSource.CompletionWords.new("word_completion")
|
||||
word_completion.register(self._buffer)
|
||||
word_completion.register(buffer)
|
||||
self._completion.add_provider(word_completion)
|
||||
|
||||
# example_completion_provider = ExampleCompletionProvider()
|
||||
|
@ -111,27 +116,30 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
|
||||
# py_completion_provider = PythonCompletionProvider(file)
|
||||
# self._completion.add_provider(py_completion_provider)
|
||||
self.got_to_line(buffer, line)
|
||||
|
||||
|
||||
def _create_default_tag(self):
|
||||
general_style_tag = self._buffer.create_tag('general_style')
|
||||
def _create_default_tag(self, buffer):
|
||||
general_style_tag = buffer.create_tag('general_style')
|
||||
general_style_tag.set_property('size', 100)
|
||||
general_style_tag.set_property('scale', 100)
|
||||
|
||||
def _is_modified(self, *args):
|
||||
buffer = self.get_buffer()
|
||||
|
||||
if not self._loading_file:
|
||||
event_system.emit("buffer_changed", (self._buffer, ))
|
||||
event_system.emit("buffer_changed", (buffer, ))
|
||||
else:
|
||||
event_system.emit("buffer_changed_first_load", (self._buffer, ))
|
||||
event_system.emit("buffer_changed_first_load", (buffer, ))
|
||||
|
||||
self.update_cursor_position()
|
||||
self.update_cursor_position(buffer)
|
||||
|
||||
def _insert_text(self, text_buffer, location_itr, text_str, len_int):
|
||||
def _insert_text(self, buffer, location_itr, text_str, len_int):
|
||||
if self.freeze_multi_line_insert: return
|
||||
|
||||
self.begin_user_action()
|
||||
with self._buffer.freeze_notify():
|
||||
GLib.idle_add(self._update_multi_line_markers, *(text_str,))
|
||||
self.begin_user_action(buffer)
|
||||
with buffer.freeze_notify():
|
||||
GLib.idle_add(self._update_multi_line_markers, *(buffer, text_str,))
|
||||
|
||||
def _buffer_modified_changed(self, buffer):
|
||||
tab_widget = self.get_parent().get_tab_widget()
|
||||
|
@ -144,6 +152,7 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
|
||||
is_control = True if modifiers & Gdk.ModifierType.CONTROL_MASK else False
|
||||
is_shift = True if modifiers & Gdk.ModifierType.SHIFT_MASK else False
|
||||
buffer = self.get_buffer()
|
||||
|
||||
try:
|
||||
is_alt = True if modifiers & Gdk.ModifierType.ALT_MASK else False
|
||||
|
@ -156,7 +165,7 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
|
||||
if is_shift:
|
||||
if keyname in [ "z", "Up", "Down", "Left", "Right" ]:
|
||||
# NOTE: For now do like so for completion sake above.
|
||||
# NOTE: For now do like so for completion sake above.
|
||||
if keyname in ["Left", "Right"]:
|
||||
return False
|
||||
|
||||
|
@ -168,9 +177,9 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
|
||||
if keyname == "BackSpace":
|
||||
if len(self._multi_insert_marks) > 0:
|
||||
self.begin_user_action()
|
||||
with self._buffer.freeze_notify():
|
||||
GLib.idle_add(self._delete_on_multi_line_markers)
|
||||
self.begin_user_action(buffer)
|
||||
with buffer.freeze_notify():
|
||||
GLib.idle_add(self._delete_on_multi_line_markers, *(buffer,))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -190,12 +199,13 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
|
||||
def _scroll_event(self, widget, eve):
|
||||
accel_mask = Gtk.accelerator_get_default_mod_mask()
|
||||
x, y, z = eve.get_scroll_deltas()
|
||||
x, y, z = eve.get_scroll_deltas()
|
||||
if eve.state & accel_mask == Gdk.ModifierType.CONTROL_MASK:
|
||||
buffer = self.get_buffer()
|
||||
if z > 0:
|
||||
self.scale_down_text()
|
||||
self.scale_down_text(buffer)
|
||||
else:
|
||||
self.scale_up_text()
|
||||
self.scale_up_text(buffer)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -226,10 +236,10 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
|
||||
return False
|
||||
|
||||
def _on_cursor_move(self, buf, cursor_iter, mark, user_data = None):
|
||||
if mark != buf.get_insert(): return
|
||||
def _on_cursor_move(self, buffer, cursor_iter, mark, user_data = None):
|
||||
if mark != buffer.get_insert(): return
|
||||
|
||||
self.update_cursor_position()
|
||||
self.update_cursor_position(buffer)
|
||||
|
||||
# NOTE: Not sure but this might not be efficient if the map reloads the same view...
|
||||
event_system.emit(f"set_source_view", (self,))
|
||||
|
@ -246,12 +256,13 @@ class SourceView(SourceViewEventsMixin, GtkSource.View):
|
|||
if info == 70: return
|
||||
|
||||
if info == 80:
|
||||
uris = data.get_uris()
|
||||
buffer = self.get_buffer()
|
||||
uris = data.get_uris()
|
||||
|
||||
if len(uris) == 0:
|
||||
uris = data.get_text().split("\n")
|
||||
|
||||
if not self._current_file and not self._buffer.get_modified():
|
||||
if not self._current_file and not buffer.get_modified():
|
||||
gfile = Gio.File.new_for_uri(uris[0])
|
||||
self.open_file(gfile)
|
||||
uris.pop(0)
|
||||
|
|
|
@ -13,16 +13,16 @@ from .source_marks_events_mixin import MarkEventsMixin
|
|||
|
||||
class SourceViewEventsMixin(MarkEventsMixin, FileEventsMixin):
|
||||
|
||||
def set_buffer_language(self, language = "python3"):
|
||||
self._buffer.set_language( self._language_manager.get_language(language) )
|
||||
def set_buffer_language(self, buffer, language = "python3"):
|
||||
buffer.set_language( self._language_manager.get_language(language) )
|
||||
|
||||
def set_buffer_style(self, style = settings.theming.syntax_theme):
|
||||
self._buffer.set_style_scheme( self._style_scheme_manager.get_scheme(style) )
|
||||
def set_buffer_style(self, buffer, style = settings.theming.syntax_theme):
|
||||
buffer.set_style_scheme( self._style_scheme_manager.get_scheme(style) )
|
||||
|
||||
def toggle_highlight_line(self, widget = None, eve = None):
|
||||
self.set_highlight_current_line( not self.get_highlight_current_line() )
|
||||
|
||||
def scale_up_text(self, scale_step = 10):
|
||||
def scale_up_text(self, buffer, scale_step = 10):
|
||||
ctx = self.get_style_context()
|
||||
|
||||
if self._px_value < 99:
|
||||
|
@ -30,15 +30,15 @@ class SourceViewEventsMixin(MarkEventsMixin, FileEventsMixin):
|
|||
ctx.add_class(f"px{self._px_value}")
|
||||
|
||||
# NOTE: Hope to bring this or similar back after we decouple scaling issues coupled with the miniview.
|
||||
# tag_table = self._buffer.get_tag_table()
|
||||
# start_itr = self._buffer.get_start_iter()
|
||||
# end_itr = self._buffer.get_end_iter()
|
||||
# tag_table = buffer.get_tag_table()
|
||||
# start_itr = buffer.get_start_iter()
|
||||
# end_itr = buffer.get_end_iter()
|
||||
# tag = tag_table.lookup('general_style')
|
||||
#
|
||||
# tag.set_property('scale', tag.get_property('scale') + scale_step)
|
||||
# self._buffer.apply_tag(tag, start_itr, end_itr)
|
||||
# buffer.apply_tag(tag, start_itr, end_itr)
|
||||
|
||||
def scale_down_text(self, scale_step = 10):
|
||||
def scale_down_text(self, buffer, scale_step = 10):
|
||||
ctx = self.get_style_context()
|
||||
|
||||
if self._px_value > 1:
|
||||
|
@ -47,52 +47,61 @@ class SourceViewEventsMixin(MarkEventsMixin, FileEventsMixin):
|
|||
ctx.add_class(f"px{self._px_value}")
|
||||
|
||||
# NOTE: Hope to bring this or similar back after we decouple scaling issues coupled with the miniview.
|
||||
# tag_table = self._buffer.get_tag_table()
|
||||
# start_itr = self._buffer.get_start_iter()
|
||||
# end_itr = self._buffer.get_end_iter()
|
||||
# tag_table = buffer.get_tag_table()
|
||||
# start_itr = buffer.get_start_iter()
|
||||
# end_itr = buffer.get_end_iter()
|
||||
# tag = tag_table.lookup('general_style')
|
||||
#
|
||||
# tag.set_property('scale', tag.get_property('scale') - scale_step)
|
||||
# self._buffer.apply_tag(tag, start_itr, end_itr)
|
||||
# buffer.apply_tag(tag, start_itr, end_itr)
|
||||
|
||||
def update_cursor_position(self):
|
||||
iter = self._buffer.get_iter_at_mark( self._buffer.get_insert() )
|
||||
chars = iter.get_offset()
|
||||
row = iter.get_line() + 1
|
||||
col = self.get_visual_column(iter) + 1
|
||||
def update_cursor_position(self, buffer = None):
|
||||
buffer = self.get_buffer() if not buffer else buffer
|
||||
iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
chars = iter.get_offset()
|
||||
row = iter.get_line() + 1
|
||||
col = self.get_visual_column(iter) + 1
|
||||
|
||||
event_system.emit("set_line_char_label", (f"{row}:{col}",))
|
||||
|
||||
def got_to_line(self, line: int = 0):
|
||||
index = line
|
||||
buffer = self.get_buffer()
|
||||
line_itr = buffer.get_iter_at_line(index)
|
||||
char_iter = buffer.get_iter_at_line_offset(index, line_itr.get_bytes_in_line())
|
||||
def got_to_line(self, buffer = None, line: int = 0):
|
||||
buffer = self.get_buffer() if not buffer else buffer
|
||||
line_itr = buffer.get_iter_at_line(line)
|
||||
char_iter = buffer.get_iter_at_line_offset(line, line_itr.get_bytes_in_line())
|
||||
|
||||
buffer.place_cursor(char_iter)
|
||||
if not buffer.get_mark("starting_cursor"):
|
||||
buffer.create_mark("starting_cursor", char_iter, True)
|
||||
self.scroll_to_mark( buffer.get_mark("starting_cursor"), 0.0, True, 0.0, 0.0 )
|
||||
|
||||
|
||||
# https://github.com/ptomato/inform7-ide/blob/main/src/actions.c
|
||||
def action_uncomment_selection(self):
|
||||
...
|
||||
|
||||
def action_comment_out_selection(self):
|
||||
...
|
||||
|
||||
def keyboard_undo(self):
|
||||
self._buffer.undo()
|
||||
buffer = self.get_buffer()
|
||||
buffer.undo()
|
||||
|
||||
def keyboard_redo(self):
|
||||
self._buffer.redo()
|
||||
buffer = self.get_buffer()
|
||||
buffer.redo()
|
||||
|
||||
def keyboard_move_lines_up(self):
|
||||
buffer = self.get_buffer()
|
||||
|
||||
self.begin_user_action(buffer)
|
||||
|
||||
self.emit("move-lines", *(False,))
|
||||
# unindent_lines
|
||||
# self.emit("move-words", *(self, 4,))
|
||||
|
||||
self.end_user_action(buffer)
|
||||
|
||||
def keyboard_move_lines_down(self):
|
||||
buffer = self.get_buffer()
|
||||
|
||||
self.begin_user_action(buffer)
|
||||
|
||||
self.emit("move-lines", *(True,))
|
||||
# self.emit("move-words", *(self, -4,))
|
||||
|
||||
self.end_user_action(buffer)
|
||||
|
||||
def update_labels(self, gfile = None):
|
||||
if not gfile: return
|
||||
|
|
|
@ -82,4 +82,4 @@ class ThemeButton(Gtk.Button):
|
|||
|
||||
|
||||
def _show_popover(self, widget, eve = None):
|
||||
event_system.emit("show_theme_popup")
|
||||
event_system.emit("show_theme_popup")
|
|
@ -37,4 +37,4 @@ class MiniViewWidget(Map):
|
|||
...
|
||||
|
||||
def set_source_view(self, source_view):
|
||||
self.set_view(source_view)
|
||||
self.set_view(source_view)
|
Loading…
Reference in New Issue