635 lines
26 KiB
Rust
635 lines
26 KiB
Rust
use adw::prelude::*;
|
|
use chrono::Datelike;
|
|
use gtk::{gdk, glib};
|
|
use outlay_core::db::Database;
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
|
|
use crate::budgets_view::BudgetsView;
|
|
use crate::icon_theme;
|
|
use crate::charts_view::ChartsView;
|
|
use crate::credit_cards_view::CreditCardsView;
|
|
use crate::forecast_view::ForecastView;
|
|
use crate::insights_view::InsightsView;
|
|
use crate::goals_view::GoalsView;
|
|
use crate::history_view::HistoryView;
|
|
use crate::log_view::LogView;
|
|
use crate::recurring_view::RecurringView;
|
|
use crate::settings_view::SettingsView;
|
|
use crate::subscriptions_view::SubscriptionsView;
|
|
use crate::wishlist_view::WishlistView;
|
|
|
|
pub struct MainWindow {
|
|
pub window: adw::ApplicationWindow,
|
|
pub log_view: Rc<LogView>,
|
|
pub history_view: Rc<HistoryView>,
|
|
pub charts_view: Rc<ChartsView>,
|
|
pub budgets_view: Rc<BudgetsView>,
|
|
pub insights_view: Rc<InsightsView>,
|
|
sidebar_list: gtk::ListBox,
|
|
content_stack: gtk::Stack,
|
|
content_page: adw::NavigationPage,
|
|
}
|
|
|
|
struct SidebarItem {
|
|
id: &'static str,
|
|
label: &'static str,
|
|
icon: &'static str,
|
|
color: &'static str, // CSS color visible in both light and dark modes
|
|
}
|
|
|
|
struct SidebarSection {
|
|
label: &'static str,
|
|
items: &'static [SidebarItem],
|
|
}
|
|
|
|
const SIDEBAR_SECTIONS: &[SidebarSection] = &[
|
|
SidebarSection {
|
|
label: "TRACKING",
|
|
items: &[
|
|
SidebarItem { id: "log", label: "Log", icon: "outlay-log", color: "#4dabf7" },
|
|
SidebarItem { id: "history", label: "History", icon: "outlay-history", color: "#9775fa" },
|
|
SidebarItem { id: "charts", label: "Charts", icon: "outlay-charts", color: "#ff8787" },
|
|
],
|
|
},
|
|
SidebarSection {
|
|
label: "PLANNING",
|
|
items: &[
|
|
SidebarItem { id: "budgets", label: "Budgets", icon: "outlay-budgets", color: "#69db7c" },
|
|
SidebarItem { id: "goals", label: "Goals", icon: "outlay-goals", color: "#ffd43b" },
|
|
SidebarItem { id: "forecast", label: "Forecast", icon: "outlay-forecast", color: "#74c0fc" },
|
|
],
|
|
},
|
|
SidebarSection {
|
|
label: "MANAGEMENT",
|
|
items: &[
|
|
SidebarItem { id: "recurring", label: "Recurring", icon: "outlay-recurring", color: "#38d9a9" },
|
|
SidebarItem { id: "subscriptions", label: "Subscriptions", icon: "outlay-subscriptions", color: "#e599f7" },
|
|
SidebarItem { id: "wishlist", label: "Wishlist", icon: "outlay-wishlist", color: "#ffa94d" },
|
|
SidebarItem { id: "creditcards", label: "Credit Cards", icon: "outlay-creditcards", color: "#a9e34b" },
|
|
SidebarItem { id: "insights", label: "Insights", icon: "outlay-insights", color: "#f783ac" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const SETTINGS_ITEM: SidebarItem = SidebarItem { id: "settings", label: "Settings", icon: "outlay-settings", color: "#adb5bd" };
|
|
|
|
fn all_sidebar_items() -> Vec<&'static SidebarItem> {
|
|
SIDEBAR_SECTIONS.iter().flat_map(|s| s.items.iter()).collect()
|
|
}
|
|
|
|
impl MainWindow {
|
|
pub fn new(app: &adw::Application, db: Rc<Database>) -> Self {
|
|
let content_stack = gtk::Stack::new();
|
|
content_stack.set_transition_type(gtk::StackTransitionType::Crossfade);
|
|
|
|
let log_view = Rc::new(LogView::new(db.clone(), app));
|
|
let log_scroll = gtk::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
.child(&log_view.container)
|
|
.build();
|
|
content_stack.add_named(&log_scroll, Some("log"));
|
|
|
|
let history_view = HistoryView::new(db.clone());
|
|
let history_scroll = gtk::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
.child(&history_view.container)
|
|
.build();
|
|
content_stack.add_named(&history_scroll, Some("history"));
|
|
|
|
let charts_view = ChartsView::new(db.clone());
|
|
content_stack.add_named(&charts_view.container, Some("charts"));
|
|
|
|
let budgets_view = BudgetsView::new(db.clone());
|
|
content_stack.add_named(&budgets_view.container, Some("budgets"));
|
|
|
|
let goals_view = GoalsView::new(db.clone());
|
|
content_stack.add_named(&goals_view.container, Some("goals"));
|
|
|
|
let recurring_view = Rc::new(RecurringView::new(db.clone()));
|
|
content_stack.add_named(&recurring_view.container, Some("recurring"));
|
|
|
|
let subscriptions_view = Rc::new(SubscriptionsView::new(db.clone()));
|
|
content_stack.add_named(&subscriptions_view.container, Some("subscriptions"));
|
|
|
|
// Cross-view refresh: changes in subscriptions refresh recurring and vice versa
|
|
{
|
|
let rec_ref = recurring_view.clone();
|
|
subscriptions_view.set_on_change(move || {
|
|
rec_ref.refresh();
|
|
});
|
|
}
|
|
{
|
|
let sub_ref = subscriptions_view.clone();
|
|
recurring_view.set_on_change(move || {
|
|
sub_ref.refresh();
|
|
});
|
|
}
|
|
|
|
let wishlist_view = WishlistView::new(db.clone());
|
|
content_stack.add_named(&wishlist_view.container, Some("wishlist"));
|
|
|
|
let forecast_view = ForecastView::new(db.clone());
|
|
content_stack.add_named(&forecast_view.container, Some("forecast"));
|
|
|
|
let insights_view = InsightsView::new(db.clone());
|
|
content_stack.add_named(&insights_view.container, Some("insights"));
|
|
|
|
let credit_cards_view = CreditCardsView::new(db.clone());
|
|
content_stack.add_named(&credit_cards_view.container, Some("creditcards"));
|
|
|
|
let settings_view = SettingsView::new(db.clone(), app);
|
|
content_stack.add_named(&settings_view.container, Some("settings"));
|
|
|
|
// Main sidebar items (top) - grouped by section with headers
|
|
let sidebar_list = gtk::ListBox::new();
|
|
sidebar_list.set_selection_mode(gtk::SelectionMode::Single);
|
|
sidebar_list.add_css_class("navigation-sidebar");
|
|
|
|
let all_items = all_sidebar_items();
|
|
for item in &all_items {
|
|
let row = Self::make_sidebar_row(item);
|
|
sidebar_list.append(&row);
|
|
}
|
|
|
|
// Compute section boundary indices: the row index where each section starts
|
|
// TRACKING: 0, PLANNING: 3, MANAGEMENT: 6
|
|
let mut section_starts: Vec<(i32, &'static str)> = Vec::new();
|
|
let mut idx = 0i32;
|
|
for section in SIDEBAR_SECTIONS {
|
|
section_starts.push((idx, section.label));
|
|
idx += section.items.len() as i32;
|
|
}
|
|
|
|
sidebar_list.set_header_func(move |row, _before| {
|
|
let ri = row.index();
|
|
for &(start, label) in §ion_starts {
|
|
if ri == start {
|
|
let header_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
|
|
// Add separator before all sections except the first
|
|
if start > 0 {
|
|
let sep = gtk::Separator::new(gtk::Orientation::Horizontal);
|
|
sep.set_margin_top(6);
|
|
sep.set_margin_bottom(2);
|
|
sep.set_margin_start(12);
|
|
sep.set_margin_end(12);
|
|
header_box.append(&sep);
|
|
}
|
|
|
|
let section_label = gtk::Label::new(Some(label));
|
|
section_label.add_css_class("sidebar-section-label");
|
|
section_label.set_halign(gtk::Align::Start);
|
|
header_box.append(§ion_label);
|
|
|
|
row.set_header(Some(&header_box));
|
|
return;
|
|
}
|
|
}
|
|
row.set_header(gtk::Widget::NONE);
|
|
});
|
|
|
|
// Settings item (bottom)
|
|
let settings_list = gtk::ListBox::new();
|
|
settings_list.set_selection_mode(gtk::SelectionMode::Single);
|
|
settings_list.add_css_class("navigation-sidebar");
|
|
settings_list.append(&Self::make_sidebar_row(&SETTINGS_ITEM));
|
|
|
|
let sidebar_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
sidebar_list.set_vexpand(true);
|
|
sidebar_box.append(&sidebar_list);
|
|
sidebar_box.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
|
sidebar_box.append(&settings_list);
|
|
|
|
let sidebar_toolbar = adw::ToolbarView::new();
|
|
sidebar_toolbar.add_top_bar(&adw::HeaderBar::new());
|
|
sidebar_toolbar.set_content(Some(&sidebar_box));
|
|
|
|
let sidebar_page = adw::NavigationPage::builder()
|
|
.title("Outlay")
|
|
.child(&sidebar_toolbar)
|
|
.build();
|
|
|
|
let content_toolbar = adw::ToolbarView::new();
|
|
content_toolbar.add_top_bar(&adw::HeaderBar::new());
|
|
content_toolbar.set_content(Some(&content_stack));
|
|
|
|
let content_page = adw::NavigationPage::builder()
|
|
.title("Log")
|
|
.child(&content_toolbar)
|
|
.build();
|
|
|
|
// Wire sidebar selection to switch content, update title, and refresh views
|
|
let history_view_ref = Rc::new(history_view);
|
|
let charts_view_ref = Rc::new(charts_view);
|
|
let budgets_view_ref = Rc::new(budgets_view);
|
|
let insights_view_ref = Rc::new(insights_view);
|
|
|
|
// Wire up data reset callback to refresh all views
|
|
{
|
|
let log_ref = log_view.clone();
|
|
let hist_ref = history_view_ref.clone();
|
|
let chart_ref = charts_view_ref.clone();
|
|
let budget_ref = budgets_view_ref.clone();
|
|
let insights_ref = insights_view_ref.clone();
|
|
settings_view.set_on_data_reset(move || {
|
|
log_ref.refresh_categories();
|
|
hist_ref.refresh();
|
|
chart_ref.refresh();
|
|
let today = chrono::Local::now().date_naive();
|
|
budget_ref.set_month(today.year(), today.month());
|
|
insights_ref.refresh();
|
|
});
|
|
}
|
|
|
|
// Shared month state for syncing across views
|
|
let shared_month: Rc<Cell<(i32, u32)>> = {
|
|
let today = chrono::Local::now().date_naive();
|
|
Rc::new(Cell::new((today.year(), today.month())))
|
|
};
|
|
|
|
// Main list selection - deselect settings
|
|
{
|
|
let content_stack_ref = content_stack.clone();
|
|
let content_page_ref = content_page.clone();
|
|
let history_ref = history_view_ref.clone();
|
|
let charts_ref = charts_view_ref.clone();
|
|
let budgets_ref = budgets_view_ref.clone();
|
|
let log_ref = log_view.clone();
|
|
let insights_ref = insights_view_ref.clone();
|
|
let settings_list_ref = settings_list.clone();
|
|
let shared = shared_month.clone();
|
|
sidebar_list.connect_row_selected(move |_, row| {
|
|
if let Some(row) = row {
|
|
settings_list_ref.unselect_all();
|
|
let idx = row.index() as usize;
|
|
if idx < all_sidebar_items().len() {
|
|
// Save month from the view we're leaving
|
|
let current_view = content_stack_ref.visible_child_name();
|
|
if let Some(ref name) = current_view {
|
|
let month = match name.as_str() {
|
|
"history" => Some(history_ref.get_month()),
|
|
"charts" => Some(charts_ref.get_month()),
|
|
"budgets" => Some(budgets_ref.get_month()),
|
|
_ => None,
|
|
};
|
|
if let Some(m) = month {
|
|
shared.set(m);
|
|
}
|
|
}
|
|
|
|
content_stack_ref.set_visible_child_name(all_sidebar_items()[idx].id);
|
|
content_page_ref.set_title(all_sidebar_items()[idx].label);
|
|
|
|
// Sync month to the view we're entering and refresh
|
|
let (sy, sm) = shared.get();
|
|
match all_sidebar_items()[idx].id {
|
|
"log" => log_ref.refresh_categories(),
|
|
"history" => {
|
|
history_ref.set_month(sy, sm);
|
|
}
|
|
"charts" => {
|
|
charts_ref.set_month(sy, sm);
|
|
}
|
|
"budgets" => {
|
|
budgets_ref.set_month(sy, sm);
|
|
}
|
|
"insights" => {
|
|
insights_ref.refresh();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Settings list selection - deselect main list
|
|
{
|
|
let content_stack_ref = content_stack.clone();
|
|
let content_page_ref = content_page.clone();
|
|
let sidebar_list_ref = sidebar_list.clone();
|
|
settings_list.connect_row_selected(move |_, row| {
|
|
if row.is_some() {
|
|
sidebar_list_ref.unselect_all();
|
|
content_stack_ref.set_visible_child_name(SETTINGS_ITEM.id);
|
|
content_page_ref.set_title(SETTINGS_ITEM.label);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Select the first row by default
|
|
if let Some(first_row) = sidebar_list.row_at_index(0) {
|
|
sidebar_list.select_row(Some(&first_row));
|
|
}
|
|
|
|
let split_view = adw::NavigationSplitView::new();
|
|
split_view.set_sidebar(Some(&sidebar_page));
|
|
split_view.set_content(Some(&content_page));
|
|
|
|
// Restore window size from settings
|
|
let saved_width: i32 = db
|
|
.get_setting("window_width")
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(900);
|
|
let saved_height: i32 = db
|
|
.get_setting("window_height")
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(600);
|
|
let saved_maximized: bool = db
|
|
.get_setting("window_maximized")
|
|
.ok()
|
|
.flatten()
|
|
.map(|s| s == "true")
|
|
.unwrap_or(false);
|
|
|
|
let window = adw::ApplicationWindow::builder()
|
|
.application(app)
|
|
.title("Outlay")
|
|
.default_width(saved_width)
|
|
.default_height(saved_height)
|
|
.content(&split_view)
|
|
.build();
|
|
|
|
if saved_maximized {
|
|
window.maximize();
|
|
}
|
|
|
|
// Hide window on close instead of quitting
|
|
{
|
|
let db_ref = db.clone();
|
|
window.connect_close_request(move |win| {
|
|
let (width, height) = win.default_size();
|
|
db_ref.set_setting("window_width", &width.to_string()).ok();
|
|
db_ref.set_setting("window_height", &height.to_string()).ok();
|
|
db_ref
|
|
.set_setting("window_maximized", if win.is_maximized() { "true" } else { "false" })
|
|
.ok();
|
|
win.set_visible(false);
|
|
glib::Propagation::Stop
|
|
});
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
{
|
|
let key_ctrl = gtk::EventControllerKey::new();
|
|
let sidebar_ref = sidebar_list.clone();
|
|
let settings_list_ref = settings_list.clone();
|
|
let content_ref = content_stack.clone();
|
|
let page_ref = content_page.clone();
|
|
let log_ref = log_view.clone();
|
|
let hist_ref = history_view_ref.clone();
|
|
let chart_ref = charts_view_ref.clone();
|
|
let window_ref = window.clone();
|
|
key_ctrl.connect_key_pressed(move |_, key, _, modifier| {
|
|
if !modifier.contains(gdk::ModifierType::CONTROL_MASK) {
|
|
return glib::Propagation::Proceed;
|
|
}
|
|
match key {
|
|
gdk::Key::_1 | gdk::Key::_2 | gdk::Key::_3 | gdk::Key::_4 | gdk::Key::_5 | gdk::Key::_6 | gdk::Key::_7 | gdk::Key::_8 | gdk::Key::_9 | gdk::Key::_0 => {
|
|
let idx = match key {
|
|
gdk::Key::_1 => 0,
|
|
gdk::Key::_2 => 1,
|
|
gdk::Key::_3 => 2,
|
|
gdk::Key::_4 => 3,
|
|
gdk::Key::_5 => 4,
|
|
gdk::Key::_6 => 5,
|
|
gdk::Key::_7 => 6,
|
|
gdk::Key::_8 => 7,
|
|
gdk::Key::_9 => 8,
|
|
gdk::Key::_0 => 9,
|
|
_ => 0,
|
|
};
|
|
if let Some(row) = sidebar_ref.row_at_index(idx) {
|
|
settings_list_ref.unselect_all();
|
|
sidebar_ref.select_row(Some(&row));
|
|
content_ref.set_visible_child_name(all_sidebar_items()[idx as usize].id);
|
|
page_ref.set_title(all_sidebar_items()[idx as usize].label);
|
|
if idx == 0 { log_ref.refresh_categories(); }
|
|
if idx == 1 { hist_ref.refresh(); }
|
|
if idx == 2 { chart_ref.refresh(); }
|
|
}
|
|
}
|
|
gdk::Key::comma => {
|
|
sidebar_ref.unselect_all();
|
|
settings_list_ref.select_row(settings_list_ref.row_at_index(0).as_ref());
|
|
content_ref.set_visible_child_name(SETTINGS_ITEM.id);
|
|
page_ref.set_title(SETTINGS_ITEM.label);
|
|
}
|
|
gdk::Key::e => {
|
|
if let Some(row) = sidebar_ref.row_at_index(0) {
|
|
settings_list_ref.unselect_all();
|
|
sidebar_ref.select_row(Some(&row));
|
|
}
|
|
content_ref.set_visible_child_name("log");
|
|
page_ref.set_title("Log");
|
|
log_ref.refresh_categories();
|
|
log_ref.set_income_mode(false);
|
|
log_ref.focus_amount();
|
|
}
|
|
gdk::Key::i => {
|
|
if let Some(row) = sidebar_ref.row_at_index(0) {
|
|
settings_list_ref.unselect_all();
|
|
sidebar_ref.select_row(Some(&row));
|
|
}
|
|
content_ref.set_visible_child_name("log");
|
|
page_ref.set_title("Log");
|
|
log_ref.refresh_categories();
|
|
log_ref.set_income_mode(true);
|
|
log_ref.focus_amount();
|
|
}
|
|
gdk::Key::question => {
|
|
Self::show_shortcuts_window(&window_ref);
|
|
}
|
|
_ => return glib::Propagation::Proceed,
|
|
}
|
|
glib::Propagation::Stop
|
|
});
|
|
window.add_controller(key_ctrl);
|
|
}
|
|
|
|
MainWindow {
|
|
window,
|
|
log_view,
|
|
history_view: history_view_ref,
|
|
charts_view: charts_view_ref,
|
|
budgets_view: budgets_view_ref,
|
|
insights_view: insights_view_ref,
|
|
sidebar_list,
|
|
content_stack,
|
|
content_page,
|
|
}
|
|
}
|
|
|
|
pub fn show(&self) {
|
|
self.window.set_visible(true);
|
|
self.window.present();
|
|
}
|
|
|
|
pub fn switch_to_log(&self, income: bool) {
|
|
if let Some(row) = self.sidebar_list.row_at_index(0) {
|
|
self.sidebar_list.select_row(Some(&row));
|
|
}
|
|
self.content_stack.set_visible_child_name("log");
|
|
self.content_page.set_title("Log");
|
|
self.log_view.refresh_categories();
|
|
self.log_view.set_income_mode(income);
|
|
self.log_view.focus_amount();
|
|
}
|
|
|
|
pub fn switch_to_history_filtered(&self, category_id: i64) {
|
|
if let Some(row) = self.sidebar_list.row_at_index(1) {
|
|
self.sidebar_list.select_row(Some(&row));
|
|
}
|
|
self.content_stack.set_visible_child_name("history");
|
|
self.content_page.set_title("History");
|
|
self.history_view.refresh();
|
|
self.history_view.set_category_filter(category_id);
|
|
}
|
|
|
|
pub fn switch_to_insights(&self) {
|
|
// Find the sidebar index for "insights"
|
|
let idx = all_sidebar_items().iter().position(|item| item.id == "insights");
|
|
if let Some(i) = idx {
|
|
if let Some(row) = self.sidebar_list.row_at_index(i as i32) {
|
|
self.sidebar_list.select_row(Some(&row));
|
|
}
|
|
}
|
|
self.content_stack.set_visible_child_name("insights");
|
|
self.content_page.set_title("Insights");
|
|
self.insights_view.refresh();
|
|
}
|
|
|
|
pub fn save_window_state(&self, db: &Database) {
|
|
let (width, height) = self.window.default_size();
|
|
db.set_setting("window_width", &width.to_string()).ok();
|
|
db.set_setting("window_height", &height.to_string()).ok();
|
|
db.set_setting(
|
|
"window_maximized",
|
|
if self.window.is_maximized() { "true" } else { "false" },
|
|
)
|
|
.ok();
|
|
}
|
|
|
|
fn show_shortcuts_window(parent: &adw::ApplicationWindow) {
|
|
let ui = r#"
|
|
<interface>
|
|
<object class="GtkShortcutsWindow" id="shortcuts_window">
|
|
<property name="modal">true</property>
|
|
<child>
|
|
<object class="GtkShortcutsSection">
|
|
<property name="title">Outlay</property>
|
|
<property name="max-height">12</property>
|
|
<child>
|
|
<object class="GtkShortcutsGroup">
|
|
<property name="title">Navigation</property>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>1</property>
|
|
<property name="title">Log</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>2</property>
|
|
<property name="title">History</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>3</property>
|
|
<property name="title">Charts</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>4</property>
|
|
<property name="title">Budgets</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>5</property>
|
|
<property name="title">Goals</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>6</property>
|
|
<property name="title">Recurring</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>7</property>
|
|
<property name="title">Subscriptions</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>8</property>
|
|
<property name="title">Wishlist</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>9</property>
|
|
<property name="title">Forecast</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>0</property>
|
|
<property name="title">Insights</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>comma</property>
|
|
<property name="title">Settings</property>
|
|
</object></child>
|
|
</object>
|
|
</child>
|
|
<child>
|
|
<object class="GtkShortcutsGroup">
|
|
<property name="title">Transaction Entry</property>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>e</property>
|
|
<property name="title">New expense</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>i</property>
|
|
<property name="title">New income</property>
|
|
</object></child>
|
|
</object>
|
|
</child>
|
|
<child>
|
|
<object class="GtkShortcutsGroup">
|
|
<property name="title">General</property>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>question</property>
|
|
<property name="title">Keyboard shortcuts</property>
|
|
</object></child>
|
|
<child><object class="GtkShortcutsShortcut">
|
|
<property name="accelerator"><Ctrl>q</property>
|
|
<property name="title">Quit</property>
|
|
</object></child>
|
|
</object>
|
|
</child>
|
|
</object>
|
|
</child>
|
|
</object>
|
|
</interface>"#;
|
|
let builder = gtk::Builder::from_string(ui);
|
|
let win: gtk::ShortcutsWindow = builder.object("shortcuts_window").unwrap();
|
|
win.set_transient_for(Some(parent));
|
|
win.present();
|
|
}
|
|
|
|
fn make_sidebar_row(item: &SidebarItem) -> gtk::ListBoxRow {
|
|
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 14);
|
|
hbox.set_margin_top(12);
|
|
hbox.set_margin_bottom(12);
|
|
hbox.set_margin_start(16);
|
|
hbox.set_margin_end(12);
|
|
|
|
let tinted = icon_theme::get_tinted_icon_name(item.icon, item.color);
|
|
let icon = gtk::Image::from_icon_name(&tinted);
|
|
icon.set_pixel_size(24);
|
|
|
|
let label = gtk::Label::new(Some(item.label));
|
|
label.set_halign(gtk::Align::Start);
|
|
label.add_css_class("sidebar-label");
|
|
|
|
hbox.append(&icon);
|
|
hbox.append(&label);
|
|
|
|
let row = gtk::ListBoxRow::new();
|
|
row.set_child(Some(&hbox));
|
|
row
|
|
}
|
|
}
|