Add feature batch 2, subscription/recurring sync, smooth charts, and app icon
- Implement subscriptions view with bidirectional recurring transaction sync - Add cascade delete/pause/resume between subscriptions and recurring - Fix foreign key constraints when deleting recurring transactions - Add cross-view instant refresh via callback pattern - Replace Bezier chart smoothing with Fritsch-Carlson monotone Hermite interpolation - Smooth budget sparklines using shared monotone_subdivide function - Add vertical spacing to budget rows - Add app icon (receipt on GNOME blue) in all sizes for desktop, web, and AppImage - Add calendar, credit cards, forecast, goals, insights, and wishlist views - Add date picker, numpad, quick-add, category combo, and edit dialog components - Add import/export for CSV, JSON, OFX, QIF formats - Add NLP transaction parsing, OCR receipt scanning, expression evaluator - Add notification support, Sankey chart, tray icon - Add demo data seeder with full DB wipe - Expand database schema with subscriptions, goals, credit cards, and more
This commit is contained in:
@@ -1,44 +1,90 @@
|
||||
use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
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 split_view: adw::NavigationSplitView,
|
||||
pub content_stack: gtk::Stack,
|
||||
pub log_view: LogView,
|
||||
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
|
||||
}
|
||||
|
||||
const SIDEBAR_ITEMS: &[SidebarItem] = &[
|
||||
SidebarItem { id: "log", label: "Log", icon: "list-add-symbolic" },
|
||||
SidebarItem { id: "history", label: "History", icon: "document-open-recent-symbolic" },
|
||||
SidebarItem { id: "charts", label: "Charts", icon: "utilities-system-monitor-symbolic" },
|
||||
SidebarItem { id: "budgets", label: "Budgets", icon: "wallet2-symbolic" },
|
||||
SidebarItem { id: "recurring", label: "Recurring", icon: "view-refresh-symbolic" },
|
||||
SidebarItem { id: "settings", label: "Settings", icon: "emblem-system-symbolic" },
|
||||
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);
|
||||
|
||||
// Log view
|
||||
let log_view = LogView::new(db.clone(), app);
|
||||
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)
|
||||
@@ -61,47 +107,115 @@ impl MainWindow {
|
||||
let budgets_view = BudgetsView::new(db.clone());
|
||||
content_stack.add_named(&budgets_view.container, Some("budgets"));
|
||||
|
||||
// Goals view
|
||||
let goals_view = GoalsView::new(db.clone());
|
||||
content_stack.add_named(&goals_view.container, Some("goals"));
|
||||
|
||||
// Recurring view
|
||||
let recurring_view = RecurringView::new(db.clone());
|
||||
let recurring_view = Rc::new(RecurringView::new(db.clone()));
|
||||
content_stack.add_named(&recurring_view.container, Some("recurring"));
|
||||
|
||||
// Subscriptions view
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// Wishlist view
|
||||
let wishlist_view = WishlistView::new(db.clone());
|
||||
content_stack.add_named(&wishlist_view.container, Some("wishlist"));
|
||||
|
||||
// Forecast view
|
||||
let forecast_view = ForecastView::new(db.clone());
|
||||
content_stack.add_named(&forecast_view.container, Some("forecast"));
|
||||
|
||||
// Insights view
|
||||
let insights_view = InsightsView::new(db.clone());
|
||||
content_stack.add_named(&insights_view.container, Some("insights"));
|
||||
|
||||
// Credit Cards view
|
||||
let credit_cards_view = CreditCardsView::new(db.clone());
|
||||
content_stack.add_named(&credit_cards_view.container, Some("creditcards"));
|
||||
|
||||
// Settings view
|
||||
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");
|
||||
|
||||
for item in SIDEBAR_ITEMS {
|
||||
let all_items = all_sidebar_items();
|
||||
for item in &all_items {
|
||||
let row = Self::make_sidebar_row(item);
|
||||
sidebar_list.append(&row);
|
||||
}
|
||||
|
||||
let content_stack_ref = content_stack.clone();
|
||||
sidebar_list.connect_row_selected(move |_, row| {
|
||||
if let Some(row) = row {
|
||||
let idx = row.index() as usize;
|
||||
if idx < SIDEBAR_ITEMS.len() {
|
||||
content_stack_ref.set_visible_child_name(SIDEBAR_ITEMS[idx].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Select the first row by default
|
||||
if let Some(first_row) = sidebar_list.row_at_index(0) {
|
||||
sidebar_list.select_row(Some(&first_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;
|
||||
}
|
||||
|
||||
let sidebar_scroll = gtk::ScrolledWindow::builder()
|
||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||
.vexpand(true)
|
||||
.child(&sidebar_list)
|
||||
.build();
|
||||
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_scroll));
|
||||
sidebar_toolbar.set_content(Some(&sidebar_box));
|
||||
|
||||
let sidebar_page = adw::NavigationPage::builder()
|
||||
.title("Outlay")
|
||||
@@ -113,10 +227,114 @@ impl MainWindow {
|
||||
content_toolbar.set_content(Some(&content_stack));
|
||||
|
||||
let content_page = adw::NavigationPage::builder()
|
||||
.title("Outlay")
|
||||
.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));
|
||||
@@ -153,7 +371,7 @@ impl MainWindow {
|
||||
window.maximize();
|
||||
}
|
||||
|
||||
// Save window size on close
|
||||
// Hide window on close instead of quitting
|
||||
{
|
||||
let db_ref = db.clone();
|
||||
window.connect_close_request(move |win| {
|
||||
@@ -163,28 +381,260 @@ impl MainWindow {
|
||||
db_ref
|
||||
.set_setting("window_maximized", if win.is_maximized() { "true" } else { "false" })
|
||||
.ok();
|
||||
glib::Propagation::Proceed
|
||||
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,
|
||||
split_view,
|
||||
content_stack,
|
||||
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, 12);
|
||||
hbox.set_margin_top(8);
|
||||
hbox.set_margin_bottom(8);
|
||||
hbox.set_margin_start(12);
|
||||
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 icon = gtk::Image::from_icon_name(item.icon);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user