diff --git a/data/gresource.xml b/data/gresource.xml
index f698731..39e18f9 100644
--- a/data/gresource.xml
+++ b/data/gresource.xml
@@ -14,6 +14,7 @@
ui/widgets/list_editor_item.ui
ui/widgets/attachment_slot.ui
ui/widgets/compose_attachment.ui
+ ui/widgets/adaptive_button.ui
ui/dialogs/new_account.ui
ui/dialogs/compose.ui
ui/dialogs/main.ui
diff --git a/data/ui/dialogs/compose.ui b/data/ui/dialogs/compose.ui
index 863167f..057e11c 100644
--- a/data/ui/dialogs/compose.ui
+++ b/data/ui/dialogs/compose.ui
@@ -3,85 +3,87 @@
-
+
False
True
500
- 250
+ 300
dialog
-
-
-
-
- False
- True
- 2
-
-
-
-
-
-
diff --git a/data/ui/dialogs/main.ui b/data/ui/dialogs/main.ui
index a18d2ef..3df029a 100644
--- a/data/ui/dialogs/main.ui
+++ b/data/ui/dialogs/main.ui
@@ -1,192 +1,21 @@
-
+
-
+
360
600
False
-
+
True
False
- vertical
+ slide
+ True
+ True
-
- True
- False
- slide-left-right
-
-
- True
- False
- slide-left-right
-
-
-
-
-
- 0
- 0
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
- timeline_stack
- False
-
-
- False
- True
- 1
-
-
-
-
-
-
diff --git a/data/ui/dialogs/new_account.ui b/data/ui/dialogs/new_account.ui
index 6dc7180..d299f94 100644
--- a/data/ui/dialogs/new_account.ui
+++ b/data/ui/dialogs/new_account.ui
@@ -3,169 +3,117 @@
-
+
False
True
700
500
-
-
-
+ dialog
-
+
True
False
- 18
- 18
- 18
- 18
- True
- True
- 400
+ vertical
-
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 18
+ 18
+ 18
+ 18
+ True
+ True
+ 400
+
+
+ True
+ False
+ center
+ 100
+ crossfade
+
+
+ True
+ False
+ vertical
+ 24
+
+
+ True
+ False
+ 128
+ network-server-symbolic
+ 0
@@ -177,11 +125,12 @@
True
- True
- <a href="https://joinmastodon.org/">Don't have one yet?</a>
- True
- False
- 1
+ False
+ What is your instance?
+
+
+
+
False
@@ -189,70 +138,97 @@
1
-
-
- False
- True
- 2
-
-
-
-
- instance
-
-
-
-
- True
- False
- center
- vertical
- 24
-
-
- True
- False
- 128
- dialog-password-symbolic
- 0
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- Enter Authorization Code
-
-
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 24
- vertical
- 12
-
+
True
- True
-
+ False
+ 24
+ vertical
+ 12
+
+
+ True
+ False
+
+
+ https://
+ True
+ False
+ True
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+
+
+
+ True
+ True
+ 1
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ <a href="https://joinmastodon.org/">Don't have one yet?</a>
+ True
+ False
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+ instance
+
+
+
+
+ True
+ False
+ center
+ vertical
+ 24
+
+
+ True
+ False
+ 128
+ dialog-password-symbolic
+ 0
+
False
@@ -261,14 +237,102 @@
-
+
True
- True
- <a href="tootle://manual_auth">Try manual authorization</a>
- True
- False
- 1
-
+ False
+ Enter Authorization Code
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ 24
+ vertical
+ 12
+
+
+ True
+ True
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ <a href="tootle://manual_auth">Try manual authorization</a>
+ True
+ False
+ 1
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+ code
+ 1
+
+
+
+
+ True
+ False
+ center
+ vertical
+ 24
+
+
+ True
+ False
+ 128
+ emblem-default-symbolic
+ 0
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Hello, @user@instance.com!
+
+
+
+
False
@@ -278,64 +342,18 @@
- False
- True
+ done
2
-
- code
- 1
-
-
-
-
- True
- False
- center
- vertical
- 24
-
-
- True
- False
- 128
- emblem-default-symbolic
- 0
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- Hello, @user@instance.com!
-
-
-
-
-
-
- False
- True
- 1
-
-
-
-
- done
- 2
-
+
+ False
+ True
+ 1
+
diff --git a/data/ui/menus.ui b/data/ui/menus.ui
index b03cbc5..37a1682 100644
--- a/data/ui/menus.ui
+++ b/data/ui/menus.ui
@@ -49,6 +49,10 @@
view.mention
direct
+ -
+ Copy Handle
+ view.copy_handle
+
diff --git a/data/ui/views/base.ui b/data/ui/views/base.ui
index c4c8a2c..a32a744 100644
--- a/data/ui/views/base.ui
+++ b/data/ui/views/base.ui
@@ -3,10 +3,38 @@
+
+ True
+ False
+ go-previous-symbolic
+
True
False
vertical
+
+
+
+
+ False
+ True
+ 0
+
+
True
@@ -129,7 +157,7 @@
-
+
True
False
vertical
diff --git a/data/ui/views/profile_header.ui b/data/ui/views/profile_header.ui
index 54af392..fda95ff 100644
--- a/data/ui/views/profile_header.ui
+++ b/data/ui/views/profile_header.ui
@@ -130,17 +130,4 @@
-
- True
- False
- True
- True
-
-
- True
- False
- Follow
-
-
-
diff --git a/data/ui/widgets/accounts_button.ui b/data/ui/widgets/accounts_button.ui
index 685a54c..3433b55 100644
--- a/data/ui/widgets/accounts_button.ui
+++ b/data/ui/widgets/accounts_button.ui
@@ -201,19 +201,6 @@
3
-
-
- True
- True
- True
- Search
-
-
- False
- True
- 4
-
-
True
@@ -224,7 +211,7 @@
False
True
- 5
+ 4
@@ -238,7 +225,7 @@
False
True
- 6
+ 5
diff --git a/data/ui/widgets/adaptive_button.ui b/data/ui/widgets/adaptive_button.ui
new file mode 100644
index 0000000..54d42d6
--- /dev/null
+++ b/data/ui/widgets/adaptive_button.ui
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ True
+ False
+ hjghjg
+
+
+ True
+ False
+ gtk-missing-image
+
+
+ True
+ False
+ end
+
+
+ True
+ False
+
+
+ Follow
+ True
+ True
+ False
+ image1
+ True
+
+
+
+
+
+ True
+ True
+ False
+ end
+ image2
+ True
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
diff --git a/data/ui/widgets/timeline_menu.ui b/data/ui/widgets/timeline_menu.ui
index 20f51f5..e3a9673 100644
--- a/data/ui/widgets/timeline_menu.ui
+++ b/data/ui/widgets/timeline_menu.ui
@@ -1,5 +1,5 @@
-
+
-
- False
-
-
-
-
diff --git a/meson.build b/meson.build
index 8ed67e8..a725809 100644
--- a/meson.build
+++ b/meson.build
@@ -95,6 +95,7 @@ executable(
'src/Widgets/Attachment/Box.vala',
'src/Widgets/Attachment/Slot.vala',
'src/Widgets/Attachment/Picture.vala',
+ 'src/Widgets/AdaptiveButton.vala',
'src/Dialogs/ISavedWindow.vala',
'src/Dialogs/NewAccount.vala',
'src/Dialogs/MainWindow.vala',
@@ -103,6 +104,8 @@ executable(
'src/Dialogs/Preferences.vala',
'src/Dialogs/ListEditor.vala',
'src/Views/Base.vala',
+ 'src/Views/TabbedBase.vala',
+ 'src/Views/Main.vala',
'src/Views/Timeline.vala',
'src/Views/Home.vala',
'src/Views/Local.vala',
@@ -110,7 +113,7 @@ executable(
'src/Views/Notifications.vala',
'src/Views/Conversations.vala',
'src/Views/Bookmarks.vala',
- 'src/Views/ExpandedStatus.vala',
+ 'src/Views/Thread.vala',
'src/Views/Profile.vala',
'src/Views/Favorites.vala',
'src/Views/Search.vala',
diff --git a/po/POTFILES b/po/POTFILES
index e8181d3..930826b 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -43,6 +43,8 @@ src/Services/Network.vala
src/Services/Streams.vala
src/Views/Base.vala
+src/Views/TabbedBase.vala
+src/Views/Main.vala
src/Views/Bookmarks.vala
src/Views/Conversations.vala
src/Views/ExpandedStatus.vala
@@ -57,6 +59,7 @@ src/Views/Notifications.vala
src/Views/Profile.vala
src/Views/Search.vala
src/Views/Timeline.vala
+src/Views/Thread.vala
src/Widgets/Attachment/Box.vala
src/Widgets/Attachment/Picture.vala
diff --git a/src/API/Conversation.vala b/src/API/Conversation.vala
index 713de5e..292b140 100644
--- a/src/API/Conversation.vala
+++ b/src/API/Conversation.vala
@@ -10,7 +10,7 @@ public class Tootle.API.Conversation : Entity, Widgetizable {
}
public override void open () {
- var view = new Views.ExpandedStatus (last_status.formal);
+ var view = new Views.Thread (last_status.formal);
window.open_view (view);
if (unread)
diff --git a/src/API/Status.vala b/src/API/Status.vala
index 78ac130..7332b4a 100644
--- a/src/API/Status.vala
+++ b/src/API/Status.vala
@@ -78,7 +78,7 @@ public class Tootle.API.Status : Entity, Widgetizable {
}
public override void open () {
- var view = new Views.ExpandedStatus (formal);
+ var view = new Views.Thread (formal);
window.open_view (view);
}
diff --git a/src/Application.vala b/src/Application.vala
index 5e9897e..3b2c9b9 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -48,6 +48,7 @@ namespace Tootle {
{ "compose", compose_activated },
{ "back", back_activated },
{ "refresh", refresh_activated },
+ { "search", search_activated },
{ "switch-timeline", switch_timeline_activated, "i" }
};
@@ -60,6 +61,7 @@ namespace Tootle {
public string[] ACCEL_NEW_POST = {"T"};
public string[] ACCEL_BACK = {"BackSpace", "Left"};
public string[] ACCEL_REFRESH = {"R", "F5"};
+ public string[] ACCEL_SEARCH = {"F"};
public string[] ACCEL_TIMELINE_0 = {"1"};
public string[] ACCEL_TIMELINE_1 = {"2"};
public string[] ACCEL_TIMELINE_2 = {"3"};
@@ -107,6 +109,7 @@ namespace Tootle {
set_accels_for_action ("app.compose", ACCEL_NEW_POST);
set_accels_for_action ("app.back", ACCEL_BACK);
set_accels_for_action ("app.refresh", ACCEL_REFRESH);
+ set_accels_for_action ("app.search", ACCEL_SEARCH);
set_accels_for_action ("app.switch-timeline(0)", ACCEL_TIMELINE_0);
set_accels_for_action ("app.switch-timeline(1)", ACCEL_TIMELINE_1);
set_accels_for_action ("app.switch-timeline(2)", ACCEL_TIMELINE_2);
@@ -162,6 +165,10 @@ namespace Tootle {
window.back ();
}
+ void search_activated () {
+ window.open_view (new Views.Search ());
+ }
+
void refresh_activated () {
refresh ();
}
diff --git a/src/Dialogs/Compose.vala b/src/Dialogs/Compose.vala
index 9df5028..69098fc 100644
--- a/src/Dialogs/Compose.vala
+++ b/src/Dialogs/Compose.vala
@@ -2,7 +2,7 @@ using Gtk;
using Gee;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/compose.ui")]
-public class Tootle.Dialogs.Compose : Window {
+public class Tootle.Dialogs.Compose : Hdy.Window {
public API.Status? status { get; construct set; }
public string style_class { get; construct set; }
diff --git a/src/Dialogs/ListEditor.vala b/src/Dialogs/ListEditor.vala
index 40bbd3f..f347e56 100644
--- a/src/Dialogs/ListEditor.vala
+++ b/src/Dialogs/ListEditor.vala
@@ -1,7 +1,7 @@
using Gtk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/list_editor.ui")]
-public class Tootle.Dialogs.ListEditor: Gtk.Window {
+public class Tootle.Dialogs.ListEditor: Hdy.Window {
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/list_editor_item.ui")]
class Item : ListBoxRow {
diff --git a/src/Dialogs/MainWindow.vala b/src/Dialogs/MainWindow.vala
index 0025739..3b1dcc1 100644
--- a/src/Dialogs/MainWindow.vala
+++ b/src/Dialogs/MainWindow.vala
@@ -2,182 +2,109 @@ using Gtk;
using Gdk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/main.ui")]
-public class Tootle.Dialogs.MainWindow: Gtk.Window, ISavedWindow {
+public class Tootle.Dialogs.MainWindow: Hdy.Window, ISavedWindow {
- public const string ZOOM_CLASS = "app-scalable";
+ public const string ZOOM_CLASS = "app-scalable";
- [GtkChild]
- protected Stack view_stack;
- [GtkChild]
- protected Stack timeline_stack;
+ [GtkChild]
+ Hdy.Deck deck;
- [GtkChild]
- public HeaderBar header;
- [GtkChild]
- protected Revealer view_navigation;
- [GtkChild]
- protected Revealer view_controls;
- [GtkChild]
- protected Button back_button;
- [GtkChild]
- protected Button compose_button;
- [GtkChild]
- protected Hdy.ViewSwitcherTitle timeline_switcher;
- [GtkChild]
- protected Hdy.ViewSwitcherBar switcher_navbar;
- [GtkChild]
- protected Widgets.AccountsButton accounts_button;
+ Views.Base? last_view = null;
- Views.Base? last_view = null;
+ construct {
+ settings.bind_property ("dark-theme", Gtk.Settings.get_default (), "gtk-application-prefer-dark-theme", BindingFlags.SYNC_CREATE);
+ settings.notify["post-text-size"].connect (() => on_zoom_level_changed ());
- construct {
- back_button.clicked.connect (() => back ());
- compose_button.clicked.connect (() => new Dialogs.Compose ());
+ on_zoom_level_changed ();
+ deck.notify["visible-child"].connect (on_view_changed);
+ button_press_event.connect (on_button_press);
+ restore_state ();
+ }
- timeline_switcher.stack = timeline_stack;
- timeline_switcher.valign = Align.FILL;
- timeline_stack.notify["visible-child"].connect (on_timeline_changed);
+ public MainWindow (Gtk.Application app) {
+ Object (
+ application: app,
+ icon_name: Build.DOMAIN,
+ title: Build.NAME,
+ resizable: true,
+ window_position: WindowPosition.CENTER
+ );
+ open_view (new Views.Main ());
+ }
- add_timeline_view (new Views.Home (), app.ACCEL_TIMELINE_0, 0);
- add_timeline_view (new Views.Notifications (), app.ACCEL_TIMELINE_1, 1);
- add_timeline_view (new Views.Local (), app.ACCEL_TIMELINE_2, 2);
- add_timeline_view (new Views.Federated (), app.ACCEL_TIMELINE_3, 3);
+ public Views.Base open_view (Views.Base view) {
+ deck.add (view);
+ deck.visible_child = view;
+ return view;
+ }
- settings.bind_property ("dark-theme", Gtk.Settings.get_default (), "gtk-application-prefer-dark-theme", BindingFlags.SYNC_CREATE);
- settings.notify["post-text-size"].connect (() => on_zoom_level_changed ());
+ public bool back () {
+ var children = deck.get_children ();
+ unowned var current = children.find (deck.visible_child);
+ if (current != null) {
+ unowned var prev = current.prev;
+ if (current.prev != null) {
+ deck.visible_child = prev.data;
+ (current.data as Views.Base).unused = true;
+ Timeout.add (deck.transition_duration, clean_unused_views);
+ }
+ }
+ return true;
+ }
- on_zoom_level_changed ();
-
- button_press_event.connect (on_button_press);
- update_header ();
- restore_state ();
- }
-
- public MainWindow (Gtk.Application app) {
- Object (
- application: app,
- icon_name: Build.DOMAIN,
- title: Build.NAME,
- resizable: true,
- window_position: WindowPosition.CENTER
- );
- }
-
- public int get_visible_id () {
- return int.parse (view_stack.get_visible_child_name ());
- }
-
- public bool open_view (Views.Base widget) {
- var curr = view_stack.visible_child as Views.Base;
- if (curr != null)
- curr.current = false;
-
- var i = get_visible_id ();
- i++;
- widget.stack_pos = i;
- widget.show ();
- view_stack.add_named (widget, i.to_string ());
- view_stack.set_visible_child_name (i.to_string ());
- update_header ();
- widget.current = true;
- return true;
- }
-
- public bool back () {
- var i = get_visible_id ();
- if (i == 0)
- return false;
-
- var child = view_stack.get_child_by_name (i.to_string ());
- view_stack.set_visible_child_name ((i-1).to_string ());
- (child as Views.Base).current = false;
- child.destroy ();
- update_header ();
-
- var curr = view_stack.visible_child as Views.Base;
- if (curr != null)
- curr.current = true;
- return true;
- }
-
- public void reopen_view (int view_id) {
- var i = get_visible_id ();
- while (i != view_id && view_id != 0) {
- back ();
- i = get_visible_id ();
- }
- }
+ bool clean_unused_views () {
+ deck.get_children ().foreach (c => {
+ var view = c as Views.Base;
+ if (view != null && view.unused)
+ view.destroy ();
+ });
+ return Source.REMOVE;
+ }
public override bool delete_event (Gdk.EventAny event) {
window = null;
return app.on_window_closed ();
}
- public void switch_timeline (int32 num) {
- timeline_stack.visible_child_name = num.to_string ();
- }
+ [Deprecated]
+ public void switch_timeline (int32 num) {
+ }
- public void set_header_controls (Widget w) {
- reset_header_controls ();
- view_controls.add (w);
- view_controls.reveal_child = true;
- }
- public void reset_header_controls () {
- view_controls.reveal_child = false;
- view_controls.get_children ().@foreach (w => {
- view_controls.remove (w);
- });
- }
+ bool on_button_press (EventButton ev) {
+ if (ev.button == 8)
+ return back ();
+ return false;
+ }
- bool on_button_press (EventButton ev) {
- if (ev.button == 8)
- return back ();
- return false;
- }
+ void on_zoom_level_changed () {
+ var css ="""
+ .%s label {
+ font-size: %i%;
+ }
+ """.printf (ZOOM_CLASS, settings.post_text_size);
- void add_timeline_view (Views.Base view, string[] accelerators, int32 num) {
- timeline_stack.add_titled (view, num.to_string (), view.label);
- timeline_stack.child_set_property (view, "icon-name", view.icon);
- view.notify["needs-attention"].connect (() => {
- timeline_stack.child_set_property (view, "needs-attention", view.needs_attention);
- });
- }
+ try {
+ app.zoom_css_provider.load_from_data (css);
+ }
+ catch (Error e) {
+ warning (@"Can't set zoom level: $(e.message)");
+ }
+ }
- void update_header () {
- bool primary_mode = get_visible_id () == 0;
- switcher_navbar.visible = timeline_switcher.sensitive = primary_mode;
- timeline_switcher.opacity = primary_mode ? 1 : 0; //Prevent HeaderBar height jitter
- view_navigation.reveal_child = !primary_mode;
+ void on_view_changed () {
+ var view = deck.visible_child as Views.Base;
- if (primary_mode)
- header.custom_title = timeline_switcher;
- }
+ if (last_view != null) {
+ last_view.current = false;
+ last_view.on_hidden ();
+ }
- void on_timeline_changed (ParamSpec spec) {
- var view = timeline_stack.visible_child as Views.Base;
+ if (view != null) {
+ view.current = true;
+ view.on_shown ();
+ }
- if (last_view != null)
- last_view.current = false;
-
- if (view != null) {
- view.current = true;
- last_view = view;
- }
- }
-
- void on_zoom_level_changed () {
- var css ="""
- .%s label {
- font-size: %i%;
- }
- """.printf (ZOOM_CLASS, settings.post_text_size);
-
- try {
- app.zoom_css_provider.load_from_data (css);
- }
- catch (Error e) {
- warning (@"Can't set zoom level: $(e.message)");
- }
- }
+ last_view = view;
+ }
}
diff --git a/src/Dialogs/NewAccount.vala b/src/Dialogs/NewAccount.vala
index 75917dd..9d6f270 100644
--- a/src/Dialogs/NewAccount.vala
+++ b/src/Dialogs/NewAccount.vala
@@ -1,7 +1,7 @@
using Gtk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/new_account.ui")]
-public class Tootle.Dialogs.NewAccount: Gtk.Window {
+public class Tootle.Dialogs.NewAccount: Hdy.Window {
const string scopes = "read%20write%20follow";
diff --git a/src/Request.vala b/src/Request.vala
index 2a2257e..744cb6c 100644
--- a/src/Request.vala
+++ b/src/Request.vala
@@ -120,7 +120,7 @@ public class Tootle.Request : Soup.Message {
if (error != null)
throw new Oopsie.INSTANCE (error);
else
- return this;
+ return this;
}
public static string array2string (Gee.ArrayList array, string key) {
diff --git a/src/Services/Network.vala b/src/Services/Network.vala
index dc14de2..0e7fd3b 100644
--- a/src/Services/Network.vala
+++ b/src/Services/Network.vala
@@ -47,7 +47,7 @@ public class Tootle.Network : GLib.Object {
requests_processing++;
started ();
- message (@"$(mess.method): $(mess.uri.to_string (false))");
+ // message (@"$(mess.method): $(mess.uri.to_string (false))");
try {
session.queue_message (mess, (sess, msg) => {
diff --git a/src/Views/Base.vala b/src/Views/Base.vala
index 671da45..754f304 100644
--- a/src/Views/Base.vala
+++ b/src/Views/Base.vala
@@ -3,117 +3,145 @@ using Gtk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/views/base.ui")]
public class Tootle.Views.Base : Box {
- public static string STATUS_EMPTY = _("Nothing to see here");
- public static string STATUS_LOADING = " ";
+ public static string STATUS_EMPTY = _("Nothing to see here");
+ public static string STATUS_LOADING = " ";
- public int stack_pos { get; set; default = -1; }
public string? icon { get; set; default = null; }
public string label { get; set; default = ""; }
public bool needs_attention { get; set; default = false; }
public bool current { get; set; default = false; }
+ public bool unused { get; set; default = false; }
+ public SimpleActionGroup? actions { get; set; }
- [GtkChild]
- protected ScrolledWindow scrolled;
- [GtkChild]
- protected Box view;
- [GtkChild]
- protected Hdy.Clamp clamp;
- [GtkChild]
- protected Box column_view;
- [GtkChild]
- protected Stack states;
- [GtkChild]
- protected Box content;
- [GtkChild]
- protected ListBox content_list;
- [GtkChild]
- protected Button status_button;
- [GtkChild]
- Stack status_stack;
- [GtkChild]
- Label status_message_label;
+ public Container content { get; set; }
- public string state { get; set; default = "status"; }
- public string status_message { get; set; default = STATUS_EMPTY; }
+ [GtkChild]
+ protected Hdy.HeaderBar header;
+ [GtkChild]
+ protected Button back_button;
- public bool empty {
- get {
- return content_list.get_children ().length () <= 0;
- }
- }
+ [GtkChild]
+ protected ScrolledWindow scrolled;
+ [GtkChild]
+ protected Box view;
+ [GtkChild]
+ protected Hdy.Clamp clamp;
+ [GtkChild]
+ protected Box column_view;
+ [GtkChild]
+ protected Stack states;
+ [GtkChild]
+ protected Box content_box;
+ [GtkChild]
+ protected ListBox content_list;
+ [GtkChild]
+ protected Button status_button;
+ [GtkChild]
+ Stack status_stack;
+ [GtkChild]
+ Label status_message_label;
- construct {
- status_button.label = _("Reload");
- bind_property ("state", states, "visible-child-name", BindingFlags.SYNC_CREATE);
- scrolled.edge_reached.connect (pos => {
- if (pos == PositionType.BOTTOM)
- on_bottom_reached ();
- });
- content.remove.connect (() => on_content_changed ());
- content_list.remove.connect (() => on_content_changed ());
- content_list.row_activated.connect (on_content_item_activated);
+ public string state { get; set; default = "status"; }
+ public string status_message { get; set; default = STATUS_EMPTY; }
- notify["status-message"].connect (() => {
- status_message_label.label = @"$status_message";
- status_stack.visible_child_name = status_message == STATUS_LOADING ? "spinner" : "message";
- });
+ public bool empty {
+ get {
+ return content.get_children ().length () <= 0;
+ }
+ }
- notify["current"].connect (() => {
- if (current)
- on_shown ();
- else
- on_hidden ();
- });
+ construct {
+ bind_property ("label", header, "title", BindingFlags.SYNC_CREATE);
- size_allocate.connect (on_resized);
- get_style_context ().add_class (Dialogs.MainWindow.ZOOM_CLASS);
- }
+ content = content_list;
- public virtual void clear (){
- content_list.forall (w => {
- w.destroy ();
- });
- state = "status";
- }
+ status_button.label = _("Reload");
+ bind_property ("state", states, "visible-child-name", BindingFlags.SYNC_CREATE);
+ scrolled.edge_reached.connect (pos => {
+ if (pos == PositionType.BOTTOM)
+ on_bottom_reached ();
+ });
+ content.remove.connect (() => on_content_changed ());
+ content_list.remove.connect (() => on_content_changed ());
+ content_list.row_activated.connect (on_content_item_activated);
- public virtual void on_bottom_reached () {}
- public virtual void on_shown () {}
- public virtual void on_hidden () {}
+ notify["status-message"].connect (() => {
+ status_message_label.label = @"$status_message";
+ status_stack.visible_child_name = status_message == STATUS_LOADING ? "spinner" : "message";
+ });
- public virtual void on_content_changed () {
- if (empty) {
- status_message = STATUS_EMPTY;
- state = "status";
- }
- else {
- state = "content";
- }
- check_resize ();
- }
+ notify["current"].connect (() => {
+ if (current)
+ on_shown ();
+ else
+ on_hidden ();
+ });
- public virtual void on_error (int32 code, string reason) {
- status_message = reason;
- status_button.visible = true;
- status_button.sensitive = true;
- state = "status";
- }
+ scrolled.get_style_context ().add_class (Dialogs.MainWindow.ZOOM_CLASS);
- protected void on_resized () {
- Allocation alloc;
- get_allocation (out alloc);
+ build_header ();
+ }
- var target_w = clamp.maximum_size;
- var view_w = alloc.width;
+ public virtual void build_header () {}
- var ctx = view.get_style_context ();
- if (view_w <= target_w && ctx.has_class ("padded"))
- ctx.remove_class ("padded");
- if (view_w > target_w && !ctx.has_class ("padded"))
- ctx.add_class ("padded");
- }
+ public virtual void clear (){
+ content.forall (w => {
+ w.destroy ();
+ });
+ state = "status";
+ }
- public virtual void on_content_item_activated (ListBoxRow row) {
- Signal.emit_by_name (row, "open");
+ public virtual void on_bottom_reached () {}
+
+ public virtual void on_shown () {
+ if (actions != null)
+ window.insert_action_group ("view", actions);
+ }
+ public virtual void on_hidden () {
+ if (actions != null)
+ window.insert_action_group ("view", null);
+ }
+
+ public virtual void on_content_changed () {
+ if (empty) {
+ status_message = STATUS_EMPTY;
+ state = "status";
+ }
+ else {
+ state = "content";
+ }
+ check_resize ();
+ }
+
+ public virtual void on_error (int32 code, string reason) {
+ status_message = reason;
+ status_button.visible = true;
+ status_button.sensitive = true;
+ state = "status";
+ }
+
+ [GtkCallback]
+ protected void on_resized () {
+ Allocation alloc;
+ get_allocation (out alloc);
+
+ var target_w = clamp.maximum_size;
+ var view_w = alloc.width;
+
+ var ctx = view.get_style_context ();
+ if (view_w <= target_w && ctx.has_class ("padded"))
+ ctx.remove_class ("padded");
+ if (view_w > target_w && !ctx.has_class ("padded"))
+ ctx.add_class ("padded");
+ }
+
+ public virtual void on_content_item_activated (ListBoxRow row) {
+ Signal.emit_by_name (row, "open");
+ }
+
+ [GtkCallback]
+ void on_close () {
+ window.back ();
}
}
diff --git a/src/Views/Main.vala b/src/Views/Main.vala
new file mode 100644
index 0000000..5c3610d
--- /dev/null
+++ b/src/Views/Main.vala
@@ -0,0 +1,37 @@
+using Gtk;
+
+public class Tootle.Views.Main : Views.TabbedBase {
+
+ public Widgets.AccountsButton account_button;
+ public Button compose_button;
+ public Button search_button;
+
+ public Main () {
+ add_tab (new Views.Home ());
+ add_tab (new Views.Notifications ());
+ add_tab (new Views.Local ());
+ add_tab (new Views.Federated ());
+ }
+
+ public override void build_header () {
+ base.build_header ();
+ back_button.visible = false;
+
+ account_button = new Widgets.AccountsButton ();
+ account_button.show ();
+ header.pack_start (account_button);
+
+ compose_button = new Button.from_icon_name ("document-edit-symbolic");
+ compose_button.tooltip_text = _("Compose");
+ compose_button.action_name = "app.compose";
+ compose_button.show ();
+ header.pack_start (compose_button);
+
+ search_button = new Button.from_icon_name ("edit-find-symbolic");
+ search_button.tooltip_text = _("Search");
+ search_button.action_name = "app.search";
+ search_button.show ();
+ header.pack_end (search_button);
+ }
+
+}
diff --git a/src/Views/Profile.vala b/src/Views/Profile.vala
index 1f71dd2..8f38559 100644
--- a/src/Views/Profile.vala
+++ b/src/Views/Profile.vala
@@ -8,7 +8,6 @@ public class Tootle.Views.Profile : Views.Timeline {
public bool only_media { get; set; default = false; }
public string source { get; set; default = "statuses"; }
- SimpleActionGroup actions;
SimpleAction media_action;
SimpleAction replies_action;
SimpleAction muting_action;
@@ -19,14 +18,14 @@ public class Tootle.Views.Profile : Views.Timeline {
ListBox profile_list;
Label relationship;
Widgets.TimelineMenu menu_button;
- Button rs_button;
- Label rs_button_label;
+
+ Widgets.AdaptiveButton rs_button;
+ SourceFunc? rs_button_action;
weak ListBoxRow note_row;
construct {
build_actions ();
- menu_button = new Widgets.TimelineMenu ("profile-menu");
var builder = new Builder.from_resource (@"$(Build.RESOURCES)ui/views/profile_header.ui");
profile_list = builder.get_object ("profile_list") as ListBox;
@@ -38,11 +37,15 @@ public class Tootle.Views.Profile : Views.Timeline {
var avatar = builder.get_object ("avatar") as Widgets.Avatar;
avatar.url = profile.avatar;
- profile.bind_property ("display-name", menu_button.title, "label", BindingFlags.SYNC_CREATE);
+ var domain = "@" + profile.domain;
+ menu_button.title.label = profile.handle.replace (domain, "");
+ menu_button.subtitle.label = domain;
+ if ("@" in profile.acct)
+ menu_button.subtitle.show ();
var handle = builder.get_object ("handle") as Widgets.RichLabel;
- profile.bind_property ("acct", handle, "text", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
- var text = "@" + (string) src;
+ profile.bind_property ("display-name", handle, "text", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
+ var text = (string) src;
target.set_string (@"$text");
return true;
});
@@ -57,9 +60,6 @@ public class Tootle.Views.Profile : Views.Timeline {
});
relationship = builder.get_object ("relationship") as Label;
- rs_button = builder.get_object ("rs_button") as Button;
- rs_button.clicked.connect (on_rs_button_clicked);
- rs_button_label = builder.get_object ("rs_button_label") as Label;
rs.notify["id"].connect (on_rs_updated);
rebuild_fields ();
@@ -77,6 +77,20 @@ public class Tootle.Views.Profile : Views.Timeline {
menu_button.destroy ();
}
+ public override void build_header () {
+ rs_button = new Widgets.AdaptiveButton ();
+ rs_button.clicked.connect (() => {
+ if (rs_button_action != null) {
+ rs_button.sensitive = false;
+ rs_button_action ();
+ }
+ });
+ header.custom_title = menu_button = new Widgets.TimelineMenu ("profile-menu");
+
+ if (profile.id != accounts.active.id)
+ header.pack_end (rs_button);
+ }
+
void build_actions () {
actions = new SimpleActionGroup ();
@@ -114,6 +128,12 @@ public class Tootle.Views.Profile : Views.Timeline {
});
actions.add_action (mention_action);
+ var copy_handle_action = new SimpleAction ("copy_handle", null);
+ copy_handle_action.activate.connect (v => {
+ Desktop.copy (profile.handle);
+ });
+ actions.add_action (copy_handle_action);
+
muting_action = new SimpleAction.stateful ("muting", null, false);
muting_action.change_state.connect (v => {
var state = v.get_boolean ();
@@ -181,24 +201,6 @@ public class Tootle.Views.Profile : Views.Timeline {
}
}
- public override void on_shown () {
- window.insert_action_group ("view", actions);
- window.header.custom_title = menu_button;
- menu_button.valign = Align.FILL;
- window.set_header_controls (rs_button);
- }
-
- public override void on_hidden () {
- window.insert_action_group ("view", null);
- window.header.custom_title = null;
- window.reset_header_controls ();
- }
-
- void on_rs_button_clicked () {
- rs_button.sensitive = false;
- rs.modify (rs.following ? "unfollow" : "follow");
- }
-
void on_rs_updated () {
var label = "";
if (rs_button.sensitive = rs != null) {
@@ -209,12 +211,13 @@ public class Tootle.Views.Profile : Views.Timeline {
else if (rs.followed_by)
label = _("Follows you");
- var ctx = rs_button.get_style_context ();
- ctx.remove_class (STYLE_CLASS_SUGGESTED_ACTION);
- ctx.remove_class (STYLE_CLASS_DESTRUCTIVE_ACTION);
- ctx.add_class (rs.following ? STYLE_CLASS_DESTRUCTIVE_ACTION : STYLE_CLASS_SUGGESTED_ACTION);
- rs_button_label.label = rs.following ? _("Unfollow") : _("Follow");
+ string action_icon = "";
+ string action_label = "";
+ get_rs_button_state (ref action_label, ref action_icon, ref rs_button_action);
+ rs_button.icon_name = action_icon;
+ rs_button.label = action_label;
+
}
relationship.label = label;
@@ -223,6 +226,40 @@ public class Tootle.Views.Profile : Views.Timeline {
invalidate_actions (false);
}
+ void get_rs_button_state (ref string label, ref string icon_name, ref SourceFunc? fn) {
+ if (rs == null) return;
+
+ if (rs.blocking) {
+ label = _("Unblock");
+ icon_name = "view-reveal-symbolic";
+ fn = () => {
+ blocking_action.change_state (false);
+ rs_button.sensitive = true;
+ return true;
+ };
+ return;
+ }
+ else if (rs.following || rs.requested) {
+ label = _("Unfollow");
+ icon_name = "list-remove-symbolic";
+ fn = () => {
+ rs.modify ("unfollow");
+ return true;
+ };
+ return;
+ }
+ else if (!rs.following) {
+ label = _("Follow");
+ icon_name = "list-add-symbolic";
+ fn = () => {
+ rs.modify ("follow");
+ return true;
+ };
+ return;
+ }
+
+ }
+
public override Request append_params (Request req) {
if (page_next == null && source == "statuses") {
req.with_param ("exclude_replies", @"$(!include_replies)");
diff --git a/src/Views/Search.vala b/src/Views/Search.vala
index 8a951eb..15d5c46 100644
--- a/src/Views/Search.vala
+++ b/src/Views/Search.vala
@@ -1,75 +1,82 @@
using Gtk;
-public class Tootle.Views.Search : Views.Base {
+public class Tootle.Views.Search : Views.TabbedBase {
- string query = "";
- SearchBar bar;
+ public string query { get; set; default = ""; }
+ Hdy.SearchBar bar;
+ Hdy.Clamp bar_clamp;
SearchEntry entry;
- construct {
- label = _("Search");
+ Views.Base accounts_tab;
+ Views.Base statuses_tab;
+ Views.Base hashtags_tab;
- bar = new SearchBar ();
+ public Search () {
+ Object (label: _("Search"));
+
+ bar = new Hdy.SearchBar ();
bar.search_mode_enabled = true;
bar.show ();
pack_start (bar, false, false, 0);
+ reorder_child (bar, 2);
entry = new SearchEntry ();
entry.width_chars = 25;
entry.text = query;
entry.show ();
- bar.add (entry);
+
+ bar_clamp = new Hdy.Clamp ();
+ bar_clamp.show ();
+ bar_clamp.add (entry);
+
+ bar.add (bar_clamp);
bar.connect_entry (entry);
entry.activate.connect (() => request ());
- entry.icon_press.connect (() => request ());
+ entry.icon_press.connect (() => {
+ entry.text = "";
+ request ();
+ });
entry.grab_focus_without_selecting ();
status_button.clicked.connect (request);
+ accounts_tab = add_list_tab (_("Accounts"), "system-users-symbolic");
+ statuses_tab = add_list_tab (_("Statuses"), "user-available-symbolic");
+ hashtags_tab = add_list_tab (_("Hashtags"), "emoji-flags-symbolic");
+
request ();
}
- bool append (owned Entity entity) {
+ bool append (Views.Base tab, owned Entity entity) {
var w = entity.to_widget ();
- content_list.insert (w, -1);
+ tab.content_list.insert (w, -1);
return true;
}
- void append_header (string name) {
- var w = new Label (@"$name");
- w.halign = Align.START;
- w.margin = 8;
- w.use_markup = true;
- w.show ();
- content_list.insert (w, -1);
- }
-
void request () {
query = entry.text.chug ().chomp ();
if (query == "") {
clear ();
+ state = "status";
+ status_message = _("Enter query");
return;
}
clear ();
+ state = "status";
status_message = STATUS_LOADING;
API.SearchResults.request.begin (query, accounts.active, (obj, res) => {
try {
var results = API.SearchResults.request.end (res);
if (!results.accounts.is_empty) {
- append_header (_("People"));
- results.accounts.@foreach (append);
+ results.accounts.@foreach (e => append (accounts_tab, e));
}
-
if (!results.statuses.is_empty) {
- append_header (_("Posts"));
- results.statuses.@foreach (append);
+ results.statuses.@foreach (e => append (statuses_tab, e));
}
-
if (!results.hashtags.is_empty) {
- append_header (_("Hashtags"));
- results.hashtags.@foreach (append);
+ results.hashtags.@foreach (e => append (hashtags_tab, e));
}
on_content_changed ();
diff --git a/src/Views/TabbedBase.vala b/src/Views/TabbedBase.vala
new file mode 100644
index 0000000..3be48f1
--- /dev/null
+++ b/src/Views/TabbedBase.vala
@@ -0,0 +1,116 @@
+using Gtk;
+
+public class Tootle.Views.TabbedBase : Views.Base {
+
+ static int ID_COUNTER = 0;
+
+ protected Hdy.ViewSwitcherTitle switcher_title;
+ protected Hdy.ViewSwitcherBar switcher_bar;
+ protected Stack stack;
+
+ Views.Base? last_view = null;
+
+ construct {
+ content = content_box;
+ content_list.destroy ();
+ state = "content";
+
+ states.get_parent ().remove (states);
+ view.get_style_context ().remove_class ("app-view");
+ scrolled.destroy ();
+ pack_start (states);
+
+ stack = new Stack ();
+ stack.transition_duration = 100;
+ stack.transition_type = StackTransitionType.CROSSFADE;
+ stack.notify["visible-child"].connect (on_view_switched);
+ stack.show ();
+ content_box.pack_start (stack);
+
+ switcher_bar.stack = switcher_title.stack = stack;
+ }
+
+ public override void build_header () {
+ switcher_title = new Hdy.ViewSwitcherTitle ();
+ switcher_title.show ();
+ header.bind_property ("title", switcher_title, "title", BindingFlags.SYNC_CREATE);
+ header.bind_property ("subtitle", switcher_title, "subtitle", BindingFlags.SYNC_CREATE);
+ header.custom_title = switcher_title;
+
+ switcher_bar = new Hdy.ViewSwitcherBar ();
+ switcher_bar.show ();
+ switcher_title.bind_property ("title-visible", switcher_bar, "reveal", BindingFlags.SYNC_CREATE);
+ pack_end (switcher_bar, false, false, 0);
+ }
+
+ public void add_tab (Views.Base view) {
+ ID_COUNTER++;
+ stack.add_titled (view, ID_COUNTER.to_string (), view.label);
+ stack.child_set_property (view, "icon-name", view.icon);
+ view.notify["needs-attention"].connect (() => {
+ stack.child_set_property (view, "needs-attention", view.needs_attention);
+ });
+ view.header.hide ();
+ }
+
+ public Views.Base add_list_tab (string label, string icon) {
+ var tab = new Views.Base ();
+ tab.label = label;
+ tab.icon = icon;
+
+ add_tab (tab);
+
+ return tab;
+ }
+
+ public delegate void TabCB (Views.Base tab);
+ public void foreach_tab (TabCB cb) {
+ stack.@foreach (child => {
+ var tab = child as Views.Base;
+ if (tab != null)
+ cb (tab);
+ });
+ }
+
+ public override void clear () {
+ foreach_tab (tab => tab.clear ());
+ on_content_changed ();
+ }
+
+ public override void on_content_changed () {
+ var empty = true;
+ foreach_tab (tab => {
+ tab.visible = !tab.empty;
+ if (tab.visible)
+ empty = false;
+
+ tab.on_content_changed ();
+ });
+
+ if (empty) {
+ state = "status";
+ status_message = STATUS_EMPTY;
+ }
+ else {
+ state = "content";
+ }
+ }
+
+ void on_view_switched () {
+ var view = stack.visible_child as Views.Base;
+
+ if (last_view != null) {
+ last_view.current = false;
+ last_view.on_hidden ();
+ }
+
+ if (view != null) {
+ header.title = view.label;
+ view.current = true;
+ view.on_shown ();
+ }
+
+ last_view = view;
+ }
+
+}
diff --git a/src/Views/ExpandedStatus.vala b/src/Views/Thread.vala
similarity index 89%
rename from src/Views/ExpandedStatus.vala
rename to src/Views/Thread.vala
index 17c5711..dadcfcc 100644
--- a/src/Views/ExpandedStatus.vala
+++ b/src/Views/Thread.vala
@@ -1,15 +1,16 @@
using Gtk;
-public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
+public class Tootle.Views.Thread : Views.Base, IAccountListener {
public API.Status root_status { get; construct set; }
protected InstanceAccount? account = null;
protected Widget root_widget;
- public ExpandedStatus (API.Status status) {
+ public Thread (API.Status status) {
Object (
root_status: status,
- status_message: STATUS_LOADING
+ status_message: STATUS_LOADING,
+ label: _("Conversation")
);
account_listener_init ();
}
@@ -61,7 +62,6 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
int x,y;
translate_coordinates (root_widget, 0, 0, out x, out y);
scrolled.vadjustment.value = (double)(y*-1);
- //content_list.select_row (root_widget);
})
.exec ();
}
@@ -77,7 +77,7 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
var node = statuses.get_element (0);
if (node != null){
var status = API.Status.from (node);
- window.open_view (new Views.ExpandedStatus (status));
+ window.open_view (new Views.Thread (status));
}
else
Desktop.open_uri (q);
diff --git a/src/Widgets/AccountsButton.vala b/src/Widgets/AccountsButton.vala
index 3bf0516..650e2e4 100644
--- a/src/Widgets/AccountsButton.vala
+++ b/src/Widgets/AccountsButton.vala
@@ -70,8 +70,6 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
[GtkChild]
ModelButton item_refresh;
[GtkChild]
- ModelButton item_search;
- [GtkChild]
Button item_favs;
[GtkChild]
Button item_conversations;
@@ -82,6 +80,7 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
construct {
account_listener_init ();
+ get_style_context ().add_class ("image-button");
item_refresh.clicked.connect (() => {
app.refresh ();
@@ -103,10 +102,6 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
window.open_view (new Views.Lists ());
popover.popdown ();
});
- item_search.clicked.connect (() => {
- window.open_view (new Views.Search ());
- popover.popdown ();
- });
item_prefs.clicked.connect (() => {
Dialogs.Preferences.open ();
popover.popdown ();
diff --git a/src/Widgets/AdaptiveButton.vala b/src/Widgets/AdaptiveButton.vala
new file mode 100644
index 0000000..861e72a
--- /dev/null
+++ b/src/Widgets/AdaptiveButton.vala
@@ -0,0 +1,33 @@
+using Hdy;
+using Gtk;
+
+[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/adaptive_button.ui")]
+public class Tootle.Widgets.AdaptiveButton : Box {
+
+ public string label { get; set; }
+ public string icon_name { get; set; default = "image-loading-symbolic"; }
+
+ public signal void clicked ();
+
+ [GtkChild]
+ Button full;
+ [GtkChild]
+ Button mini;
+ [GtkChild]
+ Image image1;
+ [GtkChild]
+ Image image2;
+
+ construct {
+ var butts = new Button[]{ full, mini };
+ bind_property ("label", full, "label", BindingFlags.SYNC_CREATE);
+ foreach (Button butt in butts) {
+ bind_property ("tooltip_text", butt, "tooltip_text", BindingFlags.SYNC_CREATE);
+ butt.clicked.connect (() => clicked ());
+ }
+ foreach (Image img in new Image[]{ image1, image2 }) {
+ bind_property ("icon_name", img, "icon_name", BindingFlags.SYNC_CREATE);
+ }
+ }
+
+}
diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala
index a010eb3..94bdd55 100644
--- a/src/Widgets/Status.vala
+++ b/src/Widgets/Status.vala
@@ -138,7 +138,7 @@ public class Tootle.Widgets.Status : ListBoxRow {
status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE);
bind_property ("avatar_url", avatar, "url", BindingFlags.SYNC_CREATE);
- status.bind_property ("has-spoiler", this, "reveal-spoiler", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
+ status.formal.bind_property ("has-spoiler", this, "reveal-spoiler", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
target.set_boolean (!src.get_boolean ());
return true;
});
diff --git a/src/Widgets/TimelineMenu.vala b/src/Widgets/TimelineMenu.vala
index c0f9c58..07bc204 100644
--- a/src/Widgets/TimelineMenu.vala
+++ b/src/Widgets/TimelineMenu.vala
@@ -5,6 +5,8 @@ public class Tootle.Widgets.TimelineMenu : MenuButton {
[GtkChild]
public Label title;
+ [GtkChild]
+ public Label subtitle;
public TimelineMenu (string id) {
var builder = new Builder.from_resource (@"$(Build.RESOURCES)ui/menus.ui");