Fix UI for libadwaita
This commit is contained in:
parent
cc7db3b85f
commit
e35df88d4a
|
@ -154,6 +154,7 @@ public interface ConversationItemWidgetInterface: Object {
|
|||
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
||||
public class MessageAction : Object {
|
||||
public string icon_name;
|
||||
public string? tooltip;
|
||||
public Object? popover;
|
||||
public MessageActionEvoked? callback;
|
||||
}
|
||||
|
|
|
@ -112,6 +112,9 @@ set(MAIN_DEFINITIONS)
|
|||
if(GTK4_VERSION VERSION_GREATER_EQUAL "4.6")
|
||||
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_6)
|
||||
endif()
|
||||
if(GTK4_VERSION VERSION_GREATER_EQUAL "4.8")
|
||||
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_8)
|
||||
endif()
|
||||
if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.2")
|
||||
set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} Adw_1_2)
|
||||
endif()
|
||||
|
@ -204,7 +207,9 @@ SOURCES
|
|||
src/ui/util/label_hybrid.vala
|
||||
src/ui/util/sizing_bin.vala
|
||||
src/ui/util/size_request_box.vala
|
||||
src/ui/util/scaling_image.vala
|
||||
|
||||
src/ui/widgets/fixed_ratio_picture.vala
|
||||
src/ui/widgets/natural_size_increase.vala
|
||||
CUSTOM_VAPIS
|
||||
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||
|
|
|
@ -54,9 +54,19 @@
|
|||
<object class="GtkBox" id="right_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="child">
|
||||
<object class="AdwFlap" id="search_flap">
|
||||
<property name="flap-position">end</property>
|
||||
<property name="modal">true</property>
|
||||
<property name="locked">true</property>
|
||||
<property name="swipe-to-open">false</property>
|
||||
<property name="fold-threshold-policy">natural</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child type="content">
|
||||
<object class="DinoUiNaturalSizeIncrease">
|
||||
<property name="min-natural-width">600</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="right_stack">
|
||||
<property name="hexpand">false</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">content</property>
|
||||
|
@ -79,19 +89,25 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<child type="overlay">
|
||||
<object class="GtkRevealer" id="search_revealer">
|
||||
<property name="halign">end</property>
|
||||
<property name="transition-type">slide-left</property>
|
||||
<style>
|
||||
<class name="dino-sidebar"/>
|
||||
</style>
|
||||
<property name="child">
|
||||
<object class="GtkFrame" id="search_frame">
|
||||
<property name="width-request">400</property>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</child>
|
||||
<child type="separator">
|
||||
<object class="GtkSeparator" />
|
||||
</child>
|
||||
<child type="flap">
|
||||
<object class="AdwClamp">
|
||||
<property name="hexpand">false</property>
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="tightening-threshold">400</property>
|
||||
<child>
|
||||
<object class="AdwBin" id="search_frame">
|
||||
<property name="hexpand">true</property>
|
||||
<style>
|
||||
<class name="background"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -37,7 +37,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
|||
private uint time_update_timeout = 0;
|
||||
private ulong updated_roster_handler_id = 0;
|
||||
|
||||
public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item, bool initial_item) {
|
||||
public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
this.conversation = conversation;
|
||||
this.item = item;
|
||||
|
|
|
@ -24,7 +24,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||
private Gee.TreeSet<ContentMetaItem> content_items = new Gee.TreeSet<ContentMetaItem>(compare_content_meta_items);
|
||||
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
|
||||
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
|
||||
|
@ -37,7 +37,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
private double? was_page_size;
|
||||
|
||||
private Mutex reloading_mutex = Mutex();
|
||||
private bool animate = false;
|
||||
private bool firstLoad = true;
|
||||
private bool at_current_content = true;
|
||||
private bool reload_messages = true;
|
||||
|
@ -82,6 +81,15 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
main.add_controller(main_motion_events);
|
||||
main_motion_events.motion.connect(update_highlight);
|
||||
|
||||
// Process touch events and capture phase to allow highlighting a message without cursor
|
||||
GestureClick click_controller = new GestureClick();
|
||||
click_controller.touch_only = true;
|
||||
click_controller.propagation_phase = Gtk.PropagationPhase.CAPTURE;
|
||||
main_wrap_box.add_controller(click_controller);
|
||||
click_controller.pressed.connect_after((n, x, y) => {
|
||||
update_highlight(x, y);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -200,6 +208,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
MenuButton button = new MenuButton();
|
||||
button.icon_name = message_actions[i].icon_name;
|
||||
button.set_popover(message_actions[i].popover as Popover);
|
||||
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
|
||||
action_buttons.add(button);
|
||||
}
|
||||
|
||||
|
@ -210,6 +219,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
button.clicked.connect(() => {
|
||||
message_action.callback(button, current_meta_item, currently_highlighted);
|
||||
});
|
||||
button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip);
|
||||
action_buttons.add(button);
|
||||
}
|
||||
}
|
||||
|
@ -232,12 +242,71 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
});
|
||||
firstLoad = false;
|
||||
}
|
||||
if (conversation == this.conversation && at_current_content) {
|
||||
// Just make sure we are scrolled down
|
||||
if (scrolled.vadjustment.value != scrolled.vadjustment.upper) {
|
||||
scroll_animation(scrolled.vadjustment.upper).play();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clear();
|
||||
initialize_for_conversation_(conversation);
|
||||
display_latest();
|
||||
at_current_content = true;
|
||||
// Scroll to end
|
||||
scrolled.vadjustment.value = scrolled.vadjustment.upper;
|
||||
}
|
||||
|
||||
private void scroll_and_highlight_item(Plugins.MetaConversationItem target, uint duration = 500) {
|
||||
Widget widget = null;
|
||||
int h = 0;
|
||||
foreach (Plugins.MetaConversationItem item in meta_items) {
|
||||
widget = widgets[item];
|
||||
if (target == item) {
|
||||
break;
|
||||
}
|
||||
h += widget.get_allocated_height();
|
||||
}
|
||||
if (widget != widgets[target]) {
|
||||
warning("Target item widget not reached");
|
||||
return;
|
||||
}
|
||||
double target_height = h - scrolled.vadjustment.page_size * 1/3;
|
||||
Adw.Animation animation = scroll_animation(target_height);
|
||||
animation.done.connect(() => {
|
||||
widget.remove_css_class("highlight-once");
|
||||
widget.add_css_class("highlight-once");
|
||||
Timeout.add(5000, () => {
|
||||
widget.remove_css_class("highlight-once");
|
||||
return false;
|
||||
});
|
||||
});
|
||||
animation.play();
|
||||
}
|
||||
|
||||
private Adw.Animation scroll_animation(double target) {
|
||||
#if ADW_1_2
|
||||
return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500,
|
||||
new Adw.PropertyAnimationTarget(scrolled.vadjustment, "value")
|
||||
);
|
||||
#else
|
||||
return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500,
|
||||
new Adw.CallbackAnimationTarget(value => {
|
||||
scrolled.vadjustment.value = value;
|
||||
})
|
||||
);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public void initialize_around_message(Conversation conversation, ContentItem content_item) {
|
||||
if (conversation == this.conversation) {
|
||||
ContentMetaItem? matching_item = content_items.first_match(it => it.content_item.id == content_item.id);
|
||||
if (matching_item != null) {
|
||||
scroll_and_highlight_item(matching_item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
clear();
|
||||
initialize_for_conversation_(conversation);
|
||||
Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40);
|
||||
|
@ -245,7 +314,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
do_insert_item(item);
|
||||
}
|
||||
ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item);
|
||||
meta_item.can_merge = false;
|
||||
Widget w = insert_new(meta_item);
|
||||
content_items.add(meta_item);
|
||||
meta_items.add(meta_item);
|
||||
|
@ -261,23 +329,16 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
// Compute where to jump to for centered message, jump, highlight.
|
||||
reload_messages = false;
|
||||
Timeout.add(700, () => {
|
||||
int h = 0, i = 0;
|
||||
foreach (Plugins.MetaConversationItem item in meta_items) {
|
||||
Widget widget = widgets[item];
|
||||
if (widget == w) {
|
||||
break;
|
||||
}
|
||||
h += widget.get_allocated_height();
|
||||
i++;
|
||||
}
|
||||
scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3;
|
||||
w.add_css_class("highlight-once");
|
||||
scroll_and_highlight_item(meta_item, 300);
|
||||
reload_messages = true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void initialize_for_conversation_(Conversation? conversation) {
|
||||
if (this.conversation == conversation) {
|
||||
print("Re-initialized for %s\n", conversation.counterpart.bare_jid.to_string());
|
||||
}
|
||||
// Deinitialize old conversation
|
||||
Dino.Application app = Dino.Application.get_default();
|
||||
if (this.conversation != null) {
|
||||
|
@ -299,9 +360,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
}
|
||||
content_populator.init(this, conversation, Plugins.WidgetType.GTK4);
|
||||
subscription_notification.init(conversation, this);
|
||||
|
||||
animate = false;
|
||||
Timeout.add(20, () => { animate = true; return false; });
|
||||
}
|
||||
|
||||
private void display_latest() {
|
||||
|
@ -331,8 +389,8 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
public void do_insert_item(Plugins.MetaConversationItem item) {
|
||||
lock (meta_items) {
|
||||
insert_new(item);
|
||||
if (item as ContentMetaItem != null) {
|
||||
content_items.add(item);
|
||||
if (item is ContentMetaItem) {
|
||||
content_items.add((ContentMetaItem)item);
|
||||
}
|
||||
meta_items.add(item);
|
||||
}
|
||||
|
@ -348,7 +406,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
widget_order.remove(skeleton.get_widget());
|
||||
item_item_skeletons.unset(item);
|
||||
|
||||
content_items.remove(item);
|
||||
if (item is ContentMetaItem) {
|
||||
content_items.remove((ContentMetaItem)item);
|
||||
}
|
||||
meta_items.remove(item);
|
||||
}
|
||||
|
||||
|
@ -387,7 +447,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
|
||||
|
||||
// Fill datastructure
|
||||
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate);
|
||||
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item);
|
||||
item_item_skeletons[item] = item_skeleton;
|
||||
int index = lower_item != null ? widget_order.index_of(item_item_skeletons[lower_item].get_widget()) + 1 : 0;
|
||||
widget_order.insert(index, item_skeleton.get_widget());
|
||||
|
@ -503,6 +563,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
|||
}
|
||||
}
|
||||
|
||||
private static int compare_content_meta_items(ContentMetaItem a, ContentMetaItem b) {
|
||||
return compare_meta_items(a, b);
|
||||
}
|
||||
|
||||
private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) {
|
||||
int cmp1 = a.time.compare(b.time);
|
||||
if (cmp1 != 0) return cmp1;
|
||||
|
|
|
@ -10,9 +10,6 @@ namespace Dino.Ui {
|
|||
public class FileDefaultWidget : Box {
|
||||
|
||||
public signal void clicked();
|
||||
public signal void open_file();
|
||||
public signal void save_file_as();
|
||||
public signal void cancel_download();
|
||||
|
||||
[GtkChild] public unowned Stack image_stack;
|
||||
[GtkChild] public unowned Label name_label;
|
||||
|
@ -23,12 +20,6 @@ public class FileDefaultWidget : Box {
|
|||
|
||||
private FileTransfer.State state;
|
||||
|
||||
class construct {
|
||||
install_action("file.open", null, (widget, action_name) => { ((FileDefaultWidget) widget).open_file(); });
|
||||
install_action("file.save_as", null, (widget, action_name) => { ((FileDefaultWidget) widget).save_file_as(); });
|
||||
install_action("file.cancel", null, (widget, action_name) => { ((FileDefaultWidget) widget).cancel_download(); });
|
||||
}
|
||||
|
||||
public FileDefaultWidget() {
|
||||
EventControllerMotion this_motion_events = new EventControllerMotion();
|
||||
this.add_controller(this_motion_events);
|
||||
|
|
|
@ -8,41 +8,70 @@ namespace Dino.Ui {
|
|||
|
||||
public class FileImageWidget : Box {
|
||||
|
||||
FileDefaultWidget file_default_widget;
|
||||
FileDefaultWidgetController file_default_widget_controller;
|
||||
|
||||
public FileImageWidget() {
|
||||
this.halign = Align.START;
|
||||
|
||||
this.add_css_class("file-image-widget");
|
||||
this.set_cursor_from_name("zoom-in");
|
||||
}
|
||||
|
||||
public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error {
|
||||
Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.HORIZONTAL, 0) { halign=Gtk.Align.END, valign=Gtk.Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false };
|
||||
image_overlay_toolbar.add_css_class("card");
|
||||
image_overlay_toolbar.add_css_class("toolbar");
|
||||
image_overlay_toolbar.add_css_class("overlay-toolbar");
|
||||
image_overlay_toolbar.set_cursor_from_name("default");
|
||||
|
||||
FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=MAX_WIDTH, max_height=MAX_HEIGHT, file=file };
|
||||
GestureClick gesture_click_controller = new GestureClick();
|
||||
gesture_click_controller.button = 1; // listen for left clicks
|
||||
gesture_click_controller.released.connect((n_press, x, y) => {
|
||||
switch (gesture_click_controller.get_device().source) {
|
||||
case Gdk.InputSource.TOUCHSCREEN:
|
||||
case Gdk.InputSource.PEN:
|
||||
if (n_press == 1) {
|
||||
image_overlay_toolbar.visible = !image_overlay_toolbar.visible;
|
||||
} else if (n_press == 2) {
|
||||
this.activate_action("file.open", null);
|
||||
image_overlay_toolbar.visible = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.activate_action("file.open", null);
|
||||
image_overlay_toolbar.visible = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
image.add_controller(gesture_click_controller);
|
||||
|
||||
FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE);
|
||||
string? mime_type = file_info.get_content_type();
|
||||
|
||||
file_default_widget = new FileDefaultWidget() { valign=Align.END, vexpand=false, visible=false };
|
||||
file_default_widget.image_stack.visible = false;
|
||||
file_default_widget_controller = new FileDefaultWidgetController(file_default_widget);
|
||||
file_default_widget_controller.set_file(file, file_name, mime_type);
|
||||
MenuButton button = new MenuButton();
|
||||
button.icon_name = "open-menu";
|
||||
Menu menu_model = new Menu();
|
||||
menu_model.append(_("Open"), "file.open");
|
||||
menu_model.append(_("Save as…"), "file.save_as");
|
||||
Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model);
|
||||
button.popover = popover_menu;
|
||||
|
||||
image_overlay_toolbar.append(button);
|
||||
|
||||
Overlay overlay = new Overlay();
|
||||
overlay.set_child(image);
|
||||
overlay.add_overlay(file_default_widget);
|
||||
overlay.add_overlay(image_overlay_toolbar);
|
||||
overlay.set_measure_overlay(image, true);
|
||||
overlay.set_clip_overlay(file_default_widget, true);
|
||||
overlay.set_clip_overlay(image_overlay_toolbar, true);
|
||||
|
||||
EventControllerMotion this_motion_events = new EventControllerMotion();
|
||||
this.add_controller(this_motion_events);
|
||||
this_motion_events.enter.connect(() => {
|
||||
file_default_widget.visible = true;
|
||||
image_overlay_toolbar.visible = true;
|
||||
});
|
||||
this_motion_events.leave.connect(() => {
|
||||
if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return;
|
||||
if (button.popover != null && button.popover.visible) return;
|
||||
|
||||
file_default_widget.visible = false;
|
||||
image_overlay_toolbar.visible = false;
|
||||
});
|
||||
|
||||
this.append(overlay);
|
||||
|
|
|
@ -21,7 +21,9 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
|
|||
}
|
||||
|
||||
public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) {
|
||||
return new FileWidget(stream_interactor, file_transfer);
|
||||
FileWidget widget = new FileWidget(file_transfer);
|
||||
FileWidgetController widget_controller = new FileWidgetController(widget, file_transfer, stream_interactor);
|
||||
return widget;
|
||||
}
|
||||
|
||||
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
|
||||
|
@ -57,7 +59,6 @@ public class FileWidget : SizeRequestBox {
|
|||
DEFAULT
|
||||
}
|
||||
|
||||
private StreamInteractor stream_interactor;
|
||||
private FileTransfer file_transfer;
|
||||
public FileTransfer.State file_transfer_state { get; set; }
|
||||
public string file_transfer_mime_type { get; set; }
|
||||
|
@ -66,13 +67,24 @@ public class FileWidget : SizeRequestBox {
|
|||
private FileDefaultWidgetController default_widget_controller;
|
||||
private Widget? content = null;
|
||||
|
||||
public signal void open_file();
|
||||
public signal void save_file_as();
|
||||
public signal void start_download();
|
||||
public signal void cancel_download();
|
||||
|
||||
class construct {
|
||||
install_action("file.open", null, (widget, action_name) => { ((FileWidget) widget).open_file(); });
|
||||
install_action("file.save_as", null, (widget, action_name) => { ((FileWidget) widget).save_file_as(); });
|
||||
install_action("file.download", null, (widget, action_name) => { ((FileWidget) widget).start_download(); });
|
||||
install_action("file.cancel", null, (widget, action_name) => { ((FileWidget) widget).cancel_download(); });
|
||||
}
|
||||
|
||||
construct {
|
||||
margin_top = 4;
|
||||
size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
|
||||
}
|
||||
|
||||
public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) {
|
||||
this.stream_interactor = stream_interactor;
|
||||
public FileWidget(FileTransfer file_transfer) {
|
||||
this.file_transfer = file_transfer;
|
||||
|
||||
update_widget.begin();
|
||||
|
@ -113,7 +125,7 @@ public class FileWidget : SizeRequestBox {
|
|||
if (content != null) this.remove(content);
|
||||
FileDefaultWidget default_file_widget = new FileDefaultWidget();
|
||||
default_widget_controller = new FileDefaultWidgetController(default_file_widget);
|
||||
default_widget_controller.set_file_transfer(file_transfer, stream_interactor);
|
||||
default_widget_controller.set_file_transfer(file_transfer);
|
||||
content = default_file_widget;
|
||||
this.state = State.DEFAULT;
|
||||
this.append(content);
|
||||
|
@ -138,94 +150,104 @@ public class FileWidget : SizeRequestBox {
|
|||
}
|
||||
}
|
||||
|
||||
public class FileDefaultWidgetController : Object {
|
||||
|
||||
private FileDefaultWidget widget;
|
||||
private FileTransfer? file_transfer;
|
||||
public string file_transfer_path { get; set; }
|
||||
public string file_transfer_state { get; set; }
|
||||
public string file_transfer_mime_type { get; set; }
|
||||
public class FileWidgetController : Object {
|
||||
|
||||
private weak Widget widget;
|
||||
private FileTransfer file_transfer;
|
||||
private StreamInteractor? stream_interactor;
|
||||
private string file_uri;
|
||||
private string file_name;
|
||||
private FileTransfer.State state;
|
||||
|
||||
public FileDefaultWidgetController(FileDefaultWidget widget) {
|
||||
public FileWidgetController(FileWidget widget, FileTransfer file_transfer, StreamInteractor? stream_interactor = null) {
|
||||
this.widget = widget;
|
||||
|
||||
widget.clicked.connect(on_clicked);
|
||||
widget.open_file.connect(open_file);
|
||||
widget.save_file_as.connect(save_file);
|
||||
widget.cancel_download.connect(cancel_download);
|
||||
}
|
||||
|
||||
public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) {
|
||||
this.ref();
|
||||
this.widget.weak_ref(() => {
|
||||
this.widget = null;
|
||||
this.unref();
|
||||
});
|
||||
this.file_transfer = file_transfer;
|
||||
this.stream_interactor = stream_interactor;
|
||||
|
||||
widget.name_label.label = file_name = file_transfer.file_name;
|
||||
|
||||
file_transfer.bind_property("path", this, "file-transfer-path");
|
||||
file_transfer.bind_property("state", this, "file-transfer-state");
|
||||
file_transfer.bind_property("mime-type", this, "file-transfer-mime-type");
|
||||
|
||||
this.notify["file-transfer-path"].connect(update_file_info);
|
||||
this.notify["file-transfer-state"].connect(update_file_info);
|
||||
this.notify["file-transfer-mime-type"].connect(update_file_info);
|
||||
|
||||
update_file_info();
|
||||
}
|
||||
|
||||
public void set_file(File file, string file_name, string? mime_type) {
|
||||
file_uri = file.get_uri();
|
||||
state = FileTransfer.State.COMPLETE;
|
||||
widget.name_label.label = this.file_name = file_name;
|
||||
widget.update_file_info(mime_type, state, -1);
|
||||
}
|
||||
|
||||
private void update_file_info() {
|
||||
file_uri = file_transfer.get_file().get_uri();
|
||||
state = file_transfer.state;
|
||||
widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size);
|
||||
widget.open_file.connect(open_file);
|
||||
widget.save_file_as.connect(save_file);
|
||||
widget.start_download.connect(start_download);
|
||||
widget.cancel_download.connect(cancel_download);
|
||||
}
|
||||
|
||||
private void open_file() {
|
||||
try{
|
||||
AppInfo.launch_default_for_uri(file_uri, null);
|
||||
AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null);
|
||||
} catch (Error err) {
|
||||
warning("Failed to open %s - %s", file_uri, err.message);
|
||||
warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void save_file() {
|
||||
var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null);
|
||||
save_dialog.set_modal(true);
|
||||
save_dialog.set_current_name(file_name);
|
||||
save_dialog.set_current_name(file_transfer.file_name);
|
||||
|
||||
save_dialog.response.connect(() => {
|
||||
try{
|
||||
GLib.File.new_for_uri(file_uri).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null);
|
||||
GLib.File.new_for_uri(file_transfer.get_file().get_uri()).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null);
|
||||
} catch (Error err) {
|
||||
warning("Failed copy file %s - %s", file_uri, err.message);
|
||||
warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message);
|
||||
}
|
||||
});
|
||||
|
||||
save_dialog.show();
|
||||
}
|
||||
|
||||
private void start_download() {
|
||||
if (stream_interactor != null) {
|
||||
stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancel_download() {
|
||||
file_transfer.cancellable.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public class FileDefaultWidgetController : Object {
|
||||
|
||||
private FileDefaultWidget widget;
|
||||
private FileTransfer? file_transfer;
|
||||
public string file_transfer_state { get; set; }
|
||||
public string file_transfer_mime_type { get; set; }
|
||||
|
||||
private FileTransfer.State state;
|
||||
|
||||
public FileDefaultWidgetController(FileDefaultWidget widget) {
|
||||
this.widget = widget;
|
||||
|
||||
widget.clicked.connect(on_clicked);
|
||||
|
||||
this.notify["file-transfer-state"].connect(update_file_info);
|
||||
this.notify["file-transfer-mime-type"].connect(update_file_info);
|
||||
}
|
||||
|
||||
public void set_file_transfer(FileTransfer file_transfer) {
|
||||
this.file_transfer = file_transfer;
|
||||
|
||||
widget.name_label.label = file_transfer.file_name;
|
||||
|
||||
file_transfer.bind_property("state", this, "file-transfer-state");
|
||||
file_transfer.bind_property("mime-type", this, "file-transfer-mime-type");
|
||||
|
||||
update_file_info();
|
||||
}
|
||||
|
||||
private void update_file_info() {
|
||||
state = file_transfer.state;
|
||||
widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size);
|
||||
}
|
||||
|
||||
private void on_clicked() {
|
||||
switch (state) {
|
||||
case FileTransfer.State.COMPLETE:
|
||||
open_file();
|
||||
widget.activate_action("file.open", null);
|
||||
break;
|
||||
case FileTransfer.State.NOT_STARTED:
|
||||
assert(stream_interactor != null && file_transfer != null);
|
||||
stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer);
|
||||
widget.activate_action("file.download", null);
|
||||
break;
|
||||
default:
|
||||
// Clicking doesn't do anything in FAILED and IN_PROGRESS states
|
||||
|
|
|
@ -217,6 +217,7 @@ public class MessageMetaItem : ContentMetaItem {
|
|||
if (correction_allowed) {
|
||||
Plugins.MessageAction action1 = new Plugins.MessageAction();
|
||||
action1.icon_name = "document-edit-symbolic";
|
||||
action1.tooltip = _("Edit message");
|
||||
action1.callback = (button, content_meta_item_activated, widget) => {
|
||||
this.in_edit_mode = true;
|
||||
};
|
||||
|
@ -225,6 +226,7 @@ public class MessageMetaItem : ContentMetaItem {
|
|||
|
||||
Plugins.MessageAction reply_action = new Plugins.MessageAction();
|
||||
reply_action.icon_name = "mail-reply-sender-symbolic";
|
||||
reply_action.tooltip = _("Reply");
|
||||
reply_action.callback = (button, content_meta_item_activated, widget) => {
|
||||
GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) }));
|
||||
};
|
||||
|
@ -233,6 +235,7 @@ public class MessageMetaItem : ContentMetaItem {
|
|||
if (supports_reaction) {
|
||||
Plugins.MessageAction action2 = new Plugins.MessageAction();
|
||||
action2.icon_name = "dino-emoticon-add-symbolic";
|
||||
action2.tooltip = _("Add reaction");
|
||||
EmojiChooser chooser = new EmojiChooser();
|
||||
chooser.emoji_picked.connect((emoji) => {
|
||||
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji);
|
||||
|
|
|
@ -151,6 +151,8 @@ public class GlobalSearch {
|
|||
}
|
||||
|
||||
private void clear_search() {
|
||||
// Scroll to top
|
||||
results_scrolled.vadjustment.value = 0;
|
||||
foreach (Widget widget in results_box_children) {
|
||||
results_box.remove(widget);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public class MainWindow : Adw.Window {
|
|||
public Adw.Leaflet leaflet;
|
||||
public Box left_box;
|
||||
public Box right_box;
|
||||
public Revealer search_revealer;
|
||||
public Adw.Flap search_flap;
|
||||
public GlobalSearch global_search;
|
||||
private Stack stack = new Stack();
|
||||
private Stack left_stack;
|
||||
|
@ -35,7 +35,7 @@ public class MainWindow : Adw.Window {
|
|||
|
||||
class construct {
|
||||
var shortcut = new Shortcut(new KeyvalTrigger(Key.F, ModifierType.CONTROL_MASK), new CallbackAction((widget, args) => {
|
||||
((MainWindow) widget).search_revealer.reveal_child = true;
|
||||
((MainWindow) widget).search_flap.reveal_flap = true;
|
||||
return false;
|
||||
}));
|
||||
add_shortcut(shortcut);
|
||||
|
@ -67,11 +67,11 @@ public class MainWindow : Adw.Window {
|
|||
left_stack = (Stack) builder.get_object("left_stack");
|
||||
right_stack = (Stack) builder.get_object("right_stack");
|
||||
conversation_view = (ConversationView) builder.get_object("conversation_view");
|
||||
search_revealer = (Revealer) builder.get_object("search_revealer");
|
||||
search_flap = (Adw.Flap) builder.get_object("search_flap");
|
||||
conversation_selector = ((ConversationSelector) builder.get_object("conversation_list")).init(stream_interactor);
|
||||
conversation_selector.conversation_selected.connect_after(() => leaflet.navigate(Adw.NavigationDirection.FORWARD));
|
||||
|
||||
Frame search_frame = (Frame) builder.get_object("search_frame");
|
||||
Adw.Bin search_frame = (Adw.Bin) builder.get_object("search_frame");
|
||||
global_search = new GlobalSearch(stream_interactor);
|
||||
search_frame.set_child(global_search.get_widget());
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@ public class MainWindowController : Object {
|
|||
|
||||
this.conversation_view_controller = new ConversationViewController(window.conversation_view, window.conversation_titlebar, stream_interactor);
|
||||
|
||||
conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_revealer, "reveal_child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
|
||||
conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_flap, "reveal-flap", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
|
||||
|
||||
window.search_revealer.notify["child-revealed"].connect(() => {
|
||||
if (window.search_revealer.child_revealed) {
|
||||
window.search_flap.notify["reveal-flap"].connect(() => {
|
||||
if (window.search_flap.reveal_flap) {
|
||||
if (window.conversation_view.conversation_frame.conversation != null && window.global_search.search_entry.text == "") {
|
||||
reset_search_entry();
|
||||
}
|
||||
|
@ -59,7 +59,9 @@ public class MainWindowController : Object {
|
|||
window.global_search.selected_item.connect((item) => {
|
||||
select_conversation(item.conversation, false, false);
|
||||
window.conversation_view.conversation_frame.initialize_around_message(item.conversation, item);
|
||||
if (window.search_flap.folded) {
|
||||
close_search();
|
||||
}
|
||||
});
|
||||
|
||||
window.welcome_placeholder.primary_button.clicked.connect(() => {
|
||||
|
@ -91,16 +93,6 @@ public class MainWindowController : Object {
|
|||
|
||||
Widget window_widget = ((Widget) window);
|
||||
|
||||
GestureClick gesture_click_controller = new GestureClick();
|
||||
window_widget.add_controller(gesture_click_controller);
|
||||
gesture_click_controller.pressed.connect((n_press, click_x, click_y) => {
|
||||
double search_x, search_y;
|
||||
bool ret = window.search_revealer.translate_coordinates(window, 0, 0, out search_x, out search_y);
|
||||
if (ret && click_x < search_x) {
|
||||
close_search();
|
||||
}
|
||||
});
|
||||
|
||||
EventControllerKey key_event_controller = new EventControllerKey();
|
||||
window_widget.add_controller(key_event_controller);
|
||||
// TODO GTK4: Why doesn't this work with key_pressed signal
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
|
||||
namespace Dino.Ui {
|
||||
|
||||
class FixedRatioLayout : Gtk.LayoutManager {
|
||||
public int min_width { get; set; default = 0; }
|
||||
public int target_width { get; set; default = -1; }
|
||||
public int max_width { get; set; default = int.MAX; }
|
||||
public int min_height { get; set; default = 0; }
|
||||
public int target_height { get; set; default = -1; }
|
||||
public int max_height { get; set; default = int.MAX; }
|
||||
|
||||
public FixedRatioLayout() {
|
||||
this.notify.connect(layout_changed);
|
||||
}
|
||||
|
||||
private void measure_target_size(Gtk.Widget widget, out int width, out int height) {
|
||||
if (target_width != -1 && target_height != -1) {
|
||||
width = target_width;
|
||||
height = target_height;
|
||||
return;
|
||||
}
|
||||
Widget child;
|
||||
width = min_width;
|
||||
height = min_height;
|
||||
|
||||
child = widget.get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
int child_min = 0;
|
||||
int child_nat = 0;
|
||||
int child_min_baseline = -1;
|
||||
int child_nat_baseline = -1;
|
||||
child.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
width = int.max(child_nat, width);
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
width = int.min(width, max_width);
|
||||
|
||||
child = widget.get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
int child_min = 0;
|
||||
int child_nat = 0;
|
||||
int child_min_baseline = -1;
|
||||
int child_nat_baseline = -1;
|
||||
child.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
height = int.max(child_nat, height);
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
|
||||
if (height > max_height) {
|
||||
height = max_height;
|
||||
width = min_width;
|
||||
|
||||
child = widget.get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
int child_min = 0;
|
||||
int child_nat = 0;
|
||||
int child_min_baseline = -1;
|
||||
int child_nat_baseline = -1;
|
||||
child.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
width = int.max(child_nat, width);
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
width = int.min(width, max_width);
|
||||
}
|
||||
}
|
||||
|
||||
public override void measure(Gtk.Widget widget, Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
|
||||
minimum_baseline = -1;
|
||||
natural_baseline = -1;
|
||||
int width, height;
|
||||
measure_target_size(widget, out width, out height);
|
||||
if (orientation == Orientation.HORIZONTAL) {
|
||||
minimum = min_width;
|
||||
natural = width;
|
||||
} else if (for_size == -1) {
|
||||
minimum = min_height;
|
||||
natural = height;
|
||||
} else {
|
||||
minimum = natural = height * for_size / width;
|
||||
}
|
||||
}
|
||||
|
||||
public override void allocate(Gtk.Widget widget, int width, int height, int baseline) {
|
||||
Widget child = widget.get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
child.allocate(width, height, baseline, null);
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
public override SizeRequestMode get_request_mode(Gtk.Widget widget) {
|
||||
return SizeRequestMode.HEIGHT_FOR_WIDTH;
|
||||
}
|
||||
}
|
||||
|
||||
class FixedRatioPicture : Gtk.Widget {
|
||||
public int min_width { get { return layout.min_width; } set { layout.min_width = value; } }
|
||||
public int target_width { get { return layout.target_width; } set { layout.target_width = value; } }
|
||||
public int max_width { get { return layout.max_width; } set { layout.max_width = value; } }
|
||||
public int min_height { get { return layout.min_height; } set { layout.min_height = value; } }
|
||||
public int target_height { get { return layout.target_height; } set { layout.target_height = value; } }
|
||||
public int max_height { get { return layout.max_height; } set { layout.max_height = value; } }
|
||||
public File file { get { return inner.file; } set { inner.file = value; } }
|
||||
public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } }
|
||||
#if GTK_4_8 && VALA_0_58
|
||||
public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } }
|
||||
#endif
|
||||
private Gtk.Picture inner = new Gtk.Picture();
|
||||
private FixedRatioLayout layout = new FixedRatioLayout();
|
||||
|
||||
public FixedRatioPicture() {
|
||||
layout_manager = layout;
|
||||
inner.insert_after(this, null);
|
||||
}
|
||||
|
||||
public override void dispose() {
|
||||
inner.unparent();
|
||||
base.dispose();
|
||||
}
|
||||
}
|
||||
}
|
88
main/src/ui/widgets/fixed_ratio_picture.vala
Normal file
88
main/src/ui/widgets/fixed_ratio_picture.vala
Normal file
|
@ -0,0 +1,88 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
|
||||
class Dino.Ui.FixedRatioPicture : Gtk.Widget {
|
||||
public int min_width { get; set; default = -1; }
|
||||
public int max_width { get; set; default = int.MAX; }
|
||||
public int min_height { get; set; default = -1; }
|
||||
public int max_height { get; set; default = int.MAX; }
|
||||
public File file { get { return inner.file; } set { inner.file = value; } }
|
||||
public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } }
|
||||
#if GTK_4_8 && VALA_0_58
|
||||
public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } }
|
||||
#endif
|
||||
private Gtk.Picture inner = new Gtk.Picture();
|
||||
|
||||
construct {
|
||||
set_css_name("picture");
|
||||
add_css_class("fixed-ratio");
|
||||
inner.insert_after(this, null);
|
||||
this.notify.connect(queue_resize);
|
||||
}
|
||||
|
||||
private void measure_target_size(out int width, out int height) {
|
||||
if (width_request != -1 && height_request != -1) {
|
||||
width = width_request;
|
||||
height = height_request;
|
||||
return;
|
||||
}
|
||||
width = min_width;
|
||||
height = min_height;
|
||||
|
||||
if (inner.should_layout()) {
|
||||
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
|
||||
inner.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
width = int.max(child_nat, width);
|
||||
}
|
||||
width = int.min(width, max_width);
|
||||
|
||||
if (inner.should_layout()) {
|
||||
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
|
||||
inner.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
height = int.max(child_nat, height);
|
||||
}
|
||||
|
||||
if (height > max_height) {
|
||||
height = max_height;
|
||||
width = min_width;
|
||||
|
||||
if (inner.should_layout()) {
|
||||
int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1;
|
||||
inner.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
width = int.max(child_nat, width);
|
||||
}
|
||||
width = int.min(width, max_width);
|
||||
}
|
||||
}
|
||||
|
||||
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
|
||||
minimum_baseline = -1;
|
||||
natural_baseline = -1;
|
||||
int width, height;
|
||||
measure_target_size(out width, out height);
|
||||
if (orientation == Orientation.HORIZONTAL) {
|
||||
minimum = min_width;
|
||||
natural = width;
|
||||
} else if (for_size == -1) {
|
||||
minimum = min_height;
|
||||
natural = height;
|
||||
} else {
|
||||
minimum = natural = height * for_size / width;
|
||||
}
|
||||
}
|
||||
|
||||
public override void size_allocate(int width, int height, int baseline) {
|
||||
if (inner.should_layout()) {
|
||||
inner.allocate(width, height, baseline, null);
|
||||
}
|
||||
}
|
||||
|
||||
public override SizeRequestMode get_request_mode() {
|
||||
return SizeRequestMode.HEIGHT_FOR_WIDTH;
|
||||
}
|
||||
|
||||
public override void dispose() {
|
||||
inner.unparent();
|
||||
base.dispose();
|
||||
}
|
||||
}
|
59
main/src/ui/widgets/natural_size_increase.vala
Normal file
59
main/src/ui/widgets/natural_size_increase.vala
Normal file
|
@ -0,0 +1,59 @@
|
|||
using Gtk;
|
||||
|
||||
public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget {
|
||||
public int min_natural_height { get; set; default = -1; }
|
||||
public int min_natural_width { get; set; default = -1; }
|
||||
|
||||
construct {
|
||||
this.notify.connect(queue_resize);
|
||||
}
|
||||
|
||||
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
|
||||
minimum = 0;
|
||||
if (orientation == Orientation.HORIZONTAL) {
|
||||
natural = min_natural_width;
|
||||
} else {
|
||||
natural = min_natural_height;
|
||||
}
|
||||
natural = int.max(0, natural);
|
||||
minimum_baseline = -1;
|
||||
natural_baseline = -1;
|
||||
Widget child = get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
int child_min = 0;
|
||||
int child_nat = 0;
|
||||
int child_min_baseline = -1;
|
||||
int child_nat_baseline = -1;
|
||||
child.measure(orientation, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
|
||||
minimum = int.max(minimum, child_min);
|
||||
natural = int.max(natural, child_nat);
|
||||
if (child_min_baseline > 0) {
|
||||
minimum_baseline = int.max(minimum_baseline, child_min_baseline);
|
||||
}
|
||||
if (child_nat_baseline > 0) {
|
||||
natural_baseline = int.max(natural_baseline, child_nat_baseline);
|
||||
}
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
public override void size_allocate(int width, int height, int baseline) {
|
||||
Widget child = get_first_child();
|
||||
while (child != null) {
|
||||
if (child.should_layout()) {
|
||||
child.allocate(width, height, baseline, null);
|
||||
}
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
public override SizeRequestMode get_request_mode() {
|
||||
Widget child = get_first_child();
|
||||
if (child != null) {
|
||||
return child.get_request_mode();
|
||||
}
|
||||
return SizeRequestMode.CONSTANT_SIZE;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue