commit ebbdc24b1c735fcd4ae5fc2a1b2ca526e60b2c99
parent 2c1cac3b1c2825d0838a320b19b68f8ee4b42359
Author: Carlosokumu <carlosokumu254@gmail.com>
Date: Mon, 16 Mar 2026 07:39:04 +0300
handle on_attachment_removed
Diffstat:
1 file changed, 124 insertions(+), 139 deletions(-)
diff --git a/dummy/usawa/gui/views/create_entry_view.py b/dummy/usawa/gui/views/create_entry_view.py
@@ -1,5 +1,5 @@
import logging
-from gi.repository import Gtk, Adw,Gio,Pango
+from gi.repository import Gtk, Adw, Gio, Pango
from pathlib import Path
import mimetypes
@@ -8,83 +8,81 @@ logg = logging.getLogger("gui.create_entry_view")
def create_entry_page(nav_view, controller):
"""Create a new entry page"""
- page = Adw.NavigationPage(
- title="Create New Entry",
- tag="create-entry"
- )
+ page = Adw.NavigationPage(title="Create New Entry", tag="create-entry")
view = CreateEntryView(nav_view, controller)
page.set_child(view)
-
+
return page
class CreateEntryView(Gtk.Box):
"""Create entry view - UI ONLY"""
-
+
def __init__(self, nav_view, controller):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0)
-
+
self.nav_view = nav_view
self.controller = controller
self.attachment_paths: list[str] = []
-
+
self._build_ui(nav_view)
-
- def _build_ui(self,nav_view):
+
+ def _build_ui(self, nav_view):
"""Build the UI"""
header = self._create_header(nav_view=nav_view)
self.append(header)
-
+
scrolled = Gtk.ScrolledWindow()
scrolled.set_vexpand(True)
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
-
+
content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
content.set_margin_top(16)
content.set_margin_bottom(16)
content.set_margin_start(16)
content.set_margin_end(16)
-
+
# Sections
basic_section = self._create_basic_section()
content.append(basic_section)
-
+
transaction_section = self._create_transaction_section()
content.append(transaction_section)
-
+
attachments_section = self._create_attachments_section()
content.append(attachments_section)
-
+
warning = self._create_warning()
content.append(warning)
-
+
scrolled.set_child(content)
self.append(scrolled)
-
+
action_bar = self._create_action_bar()
self.append(action_bar)
-
- def _create_header(self,nav_view):
+ def _create_header(self, nav_view):
"""Create the header with back button and serial number"""
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
header_box.set_margin_top(12)
header_box.set_margin_bottom(12)
header_box.set_margin_start(12)
header_box.set_margin_end(12)
-
+
back_btn = Gtk.Button()
back_btn.set_icon_name("go-previous-symbolic")
back_btn.add_css_class("flat")
back_btn.connect("clicked", lambda b: nav_view.pop())
header_box.append(back_btn)
-
+
title = Gtk.Label(label="Create new Entry")
title.add_css_class("title-2")
header_box.append(title)
-
- serial_badge = Gtk.Label(label=f"Next Serial: #{self.controller.next_serial():04d}")
+
+ serial_badge = Gtk.Label(
+ label=f"Next Serial: #{self.controller.next_serial():04d}"
+ )
serial_badge.add_css_class("caption")
serial_badge.add_css_class("accent")
serial_badge.set_margin_start(8)
@@ -93,126 +91,118 @@ class CreateEntryView(Gtk.Box):
serial_badge.set_margin_bottom(4)
header_box.append(serial_badge)
return header_box
-
+
def _create_basic_section(self):
"""Create basic details section - UI ONLY"""
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
-
-
+
header = Gtk.Label(label="BASIC DETAILS")
header.set_halign(Gtk.Align.START)
header.add_css_class("heading")
section_box.append(header)
-
-
+
grid = Gtk.Grid()
grid.set_column_spacing(12)
grid.set_row_spacing(12)
-
+
ref_label = Gtk.Label(label="External reference (optional)")
ref_label.set_halign(Gtk.Align.START)
ref_label.add_css_class("dim-label")
grid.attach(ref_label, 0, 0, 2, 1)
-
+
self.ref_entry = Gtk.Entry()
self.ref_entry.set_hexpand(True)
grid.attach(self.ref_entry, 0, 1, 2, 1)
-
+
desc_label = Gtk.Label(label="Description (optional)")
desc_label.set_halign(Gtk.Align.START)
desc_label.add_css_class("dim-label")
grid.attach(desc_label, 0, 2, 2, 1)
-
+
self.desc_entry = Gtk.Entry()
self.desc_entry.set_placeholder_text("Brief description of the entry")
self.desc_entry.set_hexpand(True)
grid.attach(self.desc_entry, 0, 3, 2, 1)
-
+
section_box.append(grid)
return section_box
-
+
def _create_transaction_section(self):
"""Create transaction details section with per-side currencies"""
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
-
+
header = Gtk.Label(label="TRANSACTION DETAILS")
header.set_halign(Gtk.Align.START)
header.add_css_class("heading")
section_box.append(header)
-
+
amount_label = Gtk.Label(label="Amount")
amount_label.set_halign(Gtk.Align.START)
amount_label.add_css_class("dim-label")
section_box.append(amount_label)
-
+
self.amount_entry = Gtk.Entry()
self.amount_entry.set_placeholder_text("0.00")
section_box.append(self.amount_entry)
-
+
ledger_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16)
ledger_box.set_homogeneous(True)
-
source_card = self._create_ledger_side_card("Source", is_source=True)
ledger_box.append(source_card)
-
-
+
dest_card = self._create_ledger_side_card("Destination", is_source=False)
ledger_box.append(dest_card)
-
+
section_box.append(ledger_box)
-
+
return section_box
-
def _create_attachments_section(self):
"""Create the attachments section"""
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
-
-
+
header = Gtk.Label(label="ATTACHMENTS")
header.set_halign(Gtk.Align.START)
header.add_css_class("heading")
section_box.append(header)
-
+
attachments_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
attachments_card.add_css_class("card")
attachments_card.set_margin_top(8)
attachments_card.set_margin_bottom(8)
attachments_card.set_margin_start(8)
attachments_card.set_margin_end(8)
-
-
+
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
header_box.set_margin_top(8)
header_box.set_margin_start(8)
header_box.set_margin_end(8)
-
+
attach_label = Gtk.Label(label="Add supporting Documents")
attach_label.set_halign(Gtk.Align.START)
attach_label.set_hexpand(True)
header_box.append(attach_label)
-
+
add_btn = Gtk.Button()
add_btn.set_icon_name("list-add-symbolic")
add_btn.set_label("Add File")
add_btn.add_css_class("flat")
- add_btn.connect("clicked", self.on_add_attachment)
+ add_btn.connect("clicked", self.on_add_attachment)
header_box.append(add_btn)
-
+
attachments_card.append(header_box)
-
+
self.attachment_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
self.attachment_list.set_margin_start(8)
self.attachment_list.set_margin_end(8)
self.attachment_list.set_margin_bottom(8)
-
+
attachments_card.append(self.attachment_list)
-
+
section_box.append(attachments_card)
-
+
return section_box
-
def _create_attachment_row(self, filename: str, file_path: str, metadata: str):
"""Create a single attachment row"""
@@ -222,39 +212,37 @@ class CreateEntryView(Gtk.Box):
row.set_margin_bottom(4)
row.set_margin_start(4)
row.set_margin_end(4)
-
+
row.file_path = file_path
-
+
icon = self._get_icon_for_file(filename, metadata)
row.append(icon)
-
+
info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
info_box.set_hexpand(True)
-
+
name_label = Gtk.Label(label=filename)
name_label.set_halign(Gtk.Align.START)
name_label.add_css_class("caption")
name_label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
info_box.append(name_label)
-
+
meta_label = Gtk.Label(label=metadata)
meta_label.set_halign(Gtk.Align.START)
meta_label.add_css_class("dim-label")
meta_label.add_css_class("caption")
info_box.append(meta_label)
-
+
row.append(info_box)
-
+
remove_btn = Gtk.Button(label="Remove")
remove_btn.add_css_class("destructive-action")
remove_btn.connect("clicked", lambda b: self._on_remove_attachment(row))
row.append(remove_btn)
-
- return row
-
+ return row
- def _create_ledger_side_card( self,title, is_source=True):
+ def _create_ledger_side_card(self, title, is_source=True):
"""Create a card for source or destination with its own unit"""
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
card.add_css_class("card")
@@ -262,38 +250,36 @@ class CreateEntryView(Gtk.Box):
card.set_margin_bottom(8)
card.set_margin_start(8)
card.set_margin_end(8)
-
+
header = Gtk.Label(label=title)
header.set_halign(Gtk.Align.START)
header.add_css_class("heading")
header.set_margin_top(8)
header.set_margin_start(8)
card.append(header)
-
-
+
fields = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
fields.set_margin_start(8)
fields.set_margin_end(8)
fields.set_margin_bottom(8)
-
+
unit_label = Gtk.Label(label="Unit")
unit_label.set_halign(Gtk.Align.START)
unit_label.add_css_class("caption")
fields.append(unit_label)
-
+
units = Gtk.StringList()
units.append("Bitcoin(BTC)")
-
-
+
unit_dropdown = Gtk.DropDown(model=units)
- unit_dropdown.set_selected(0)
+ unit_dropdown.set_selected(0)
fields.append(unit_dropdown)
-
+
type_label = Gtk.Label(label="Account type")
type_label.set_halign(Gtk.Align.START)
type_label.add_css_class("caption")
fields.append(type_label)
-
+
types = Gtk.StringList()
if is_source:
types.append("Expense")
@@ -305,23 +291,21 @@ class CreateEntryView(Gtk.Box):
types.append("Expense")
types.append("Liability")
types.append("Income")
-
+
type_dropdown = Gtk.DropDown(model=types)
fields.append(type_dropdown)
-
-
+
path_label = Gtk.Label(label="Account path")
path_label.set_halign(Gtk.Align.START)
path_label.add_css_class("caption")
fields.append(path_label)
-
+
path_entry = Gtk.Entry()
path_entry.set_text("general")
fields.append(path_entry)
-
+
card.append(fields)
-
-
+
if is_source:
self.source_unit_dropdown = unit_dropdown
self.source_type_dropdown = type_dropdown
@@ -330,9 +314,8 @@ class CreateEntryView(Gtk.Box):
self.dest_unit_dropdown = unit_dropdown
self.dest_type_dropdown = type_dropdown
self.dest_path_entry = path_entry
-
+
return card
-
def _create_warning(self):
"""Create warning note"""
@@ -343,89 +326,88 @@ class CreateEntryView(Gtk.Box):
warning.set_margin_bottom(8)
warning.set_margin_start(8)
warning.set_margin_end(8)
-
+
icon = Gtk.Image.new_from_icon_name("dialog-warning-symbolic")
warning.append(icon)
-
+
text = Gtk.Label()
- text.set_markup("<b>Note:</b> Entries cannot be saved as drafts. You must either Finalize or Discard.")
+ text.set_markup(
+ "<b>Note:</b> Entries cannot be saved as drafts. You must either Finalize or Discard."
+ )
text.set_wrap(True)
text.set_hexpand(True)
text.set_halign(Gtk.Align.START)
warning.append(text)
-
+
return warning
-
+
def _create_action_bar(self):
"""Create action bar with buttons"""
action_bar = Gtk.ActionBar()
-
+
discard_btn = Gtk.Button(label="Discard")
discard_btn.add_css_class("destructive-action")
discard_btn.connect("clicked", self._on_discard)
action_bar.pack_start(discard_btn)
-
+
finalize_btn = Gtk.Button(label="Finalize Entry")
finalize_btn.add_css_class("suggested-action")
finalize_btn.connect("clicked", self._on_finalize)
action_bar.pack_end(finalize_btn)
-
+
return action_bar
-
+
def get_source_unit(self) -> str:
"""Get selected source unit"""
selected_idx = self.source_unit_dropdown.get_selected()
model = self.source_unit_dropdown.get_model()
return model.get_string(selected_idx)
-
+
def get_source_type(self) -> str:
"""Get selected source type"""
selected_idx = self.source_type_dropdown.get_selected()
model = self.source_type_dropdown.get_model()
return model.get_string(selected_idx)
-
+
def get_dest_unit(self) -> str:
"""Get selected dest unit"""
selected_idx = self.dest_unit_dropdown.get_selected()
model = self.dest_unit_dropdown.get_model()
return model.get_string(selected_idx)
-
+
def get_dest_type(self) -> str:
"""Get selected dest type"""
selected_idx = self.dest_type_dropdown.get_selected()
model = self.dest_type_dropdown.get_model()
return model.get_string(selected_idx)
-
+
def _on_discard(self, button):
"""Handle discard button"""
self.nav_view.pop()
-
def on_add_attachment(self, button):
"""Handle add attachment button - multiple files"""
logg.info("Add attachment clicked")
-
+
file_dialog = Gtk.FileDialog()
file_dialog.set_title("Select Attachments")
-
-
+
filters = Gio.ListStore.new(Gtk.FileFilter)
all_filter = Gtk.FileFilter()
all_filter.set_name("All Files")
all_filter.add_pattern("*")
filters.append(all_filter)
file_dialog.set_filters(filters)
-
+
file_dialog.open_multiple(
- parent=self.get_root(),
- callback=self._on_files_selected
+ parent=self.get_root(), callback=self._on_files_selected
)
def _on_files_selected(self, dialog, result):
"""Handle multiple file selection"""
try:
files = dialog.open_multiple_finish(result)
-
+
if files:
for i in range(files.get_n_items()):
file = files.get_item(i)
@@ -433,15 +415,14 @@ class CreateEntryView(Gtk.Box):
logg.info(f"File selected: {file_path}")
self.attachment_paths.append(file_path)
self._add_attachment_to_list(file_path)
-
+
except Exception as e:
logg.debug(f"File selection cancelled or failed: {e}")
-
def _get_icon_for_file(self, filename: str, metadata: str):
"""Get appropriate icon for file type"""
- icon_name = "text-x-generic-symbolic"
-
+ icon_name = "text-x-generic-symbolic"
+
if "pdf" in metadata.lower():
icon_name = "application-pdf-symbolic"
elif "image" in metadata.lower():
@@ -452,7 +433,7 @@ class CreateEntryView(Gtk.Box):
icon_name = "video-x-generic-symbolic"
elif "audio" in metadata.lower():
icon_name = "audio-x-generic-symbolic"
-
+
icon = Gtk.Image.new_from_icon_name(icon_name)
return icon
@@ -462,16 +443,14 @@ class CreateEntryView(Gtk.Box):
filename = path.name
file_size = path.stat().st_size
mime_type, _ = mimetypes.guess_type(file_path)
-
+
if mime_type is None:
mime_type = "application/octet-stream"
-
+
size_str = self._format_file_size(file_size)
-
+
attachment_row = self._create_attachment_row(
- filename=filename,
- file_path=file_path,
- metadata=f"{mime_type} • {size_str}"
+ filename=filename, file_path=file_path, metadata=f"{mime_type} • {size_str}"
)
self.attachment_list.append(attachment_row)
@@ -479,7 +458,7 @@ class CreateEntryView(Gtk.Box):
def _format_file_size(self, size_bytes: int) -> str:
"""Format file size in human-readable form"""
- for unit in ['B', 'KB', 'MB', 'GB']:
+ for unit in ["B", "KB", "MB", "GB"]:
if size_bytes < 1024.0:
return f"{size_bytes:.1f} {unit}"
size_bytes /= 1024.0
@@ -487,44 +466,51 @@ class CreateEntryView(Gtk.Box):
def _on_finalize(self, button):
entry = self.controller.collect_entry_data(self)
-
+
if entry is None:
self._show_error_dialog(
- "Invalid Input",
- "Please check your entries and try again."
+ "Invalid Input", "Please check your entries and try again."
)
return
-
+
if self.attachment_paths:
- entry.add_attachment(self.attachment_paths)
-
+ entry.add_attachment(self.attachment_paths)
+
if entry.attachments:
for attachment_path in entry.attachments:
if not Path(attachment_path).exists():
self._show_error_dialog(
"Attachment Missing",
- f"Attachment file not found: {Path(attachment_path).name}"
+ f"Attachment file not found: {Path(attachment_path).name}",
)
return
-
+
success, error_msg = self.controller.finalize_entry(entry)
-
+
if success:
self.controller.notify_entry_created()
logg.info("Entry saved successfully, returning to list")
self.nav_view.pop()
else:
self._show_error_dialog("Save Failed", error_msg)
-
def _show_error_dialog(self, title, message):
dialog = Adw.MessageDialog(
- transient_for=self.get_root(),
- heading=title,
- body=message
+ transient_for=self.get_root(), heading=title, body=message
)
dialog.add_response("ok", "OK")
dialog.set_response_appearance("ok", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.connect("response", lambda d, response: d.close())
-
- dialog.present()
-\ No newline at end of file
+
+ dialog.present()
+
+ def _on_remove_attachment(self, row):
+ file_path = row.file_path
+
+ if file_path in self.attachment_paths:
+ self.attachment_paths.remove(file_path)
+
+ parent = row.get_parent()
+ if parent is not None:
+ parent.remove(row)
+ logg.debug("Removed attachment: %s", file_path)