Project IT Calendar 1.0.0
Advanced Calendar Applet for Cinnamon Desktop Environment
Loading...
Searching...
No Matches
EventListView.ts
Go to the documentation of this file.
1/**
2/**
3 * @file EventListView.ts
4 * @brief Event sidebar UI component
5 *
6 * @details Displays events in list format with scrollable container and navigation support.
7 *
8 * @author Arnold Schiller <calendar@projektit.de>
9 * @date 2023-2026
10 * @copyright GPL-3.0-or-later
11 */
12/*
13 * Project IT Calendar - Event List View Component
14 * ----------------------------------------------
15 * This component handles the rendering of the event list shown next to or
16 * below the calendar grid. It supports single-day views, range views, and
17 * full-month overviews with clickable event rows for navigation.
18 * * * * ARCHITECTURE OVERVIEW:
19 * 1. EventManager: Handles data fetching (ICS/Evolution/System).
20 * 2. CalendarLogic: Pure JS logic for date calculations and holiday parsing.
21 * 3. CalendarView: The complex St.Table based UI grid.
22 * 4. EventListView: Specialized view for displaying event details.
23 * * * SYSTEM INTEGRATION:
24 * - Uses 'Settings' for user-defined date formats and behavior.
25 * - Uses 'AppletPopupMenu' to host the calendar UI.
26 * - Uses 'KeybindingManager' for global hotkey support.
27 * * @author Arnold Schiller <calendar@projektit.de>
28 * @link https://github.com/ArnoldSchiller/calendar
29 * @link https://projektit.de/kalender
30 * @license GPL-3.0-or-later
31
32 */
33
34/* === GJS Imports - Shell Toolkit and Clutter for UI === */
35const { St, Clutter } = imports.gi;
36const Signals = imports.signals;
37
38/**
39 * Interface Merging for Signals
40 * This tells TypeScript that EventListView will have the methods
41 * from the Signals.Signals interface (connect, disconnect, emit).
42 */
43export interface EventListView extends Signals.Signals {}
44
45
46
47/**
48 * EventListView Class
49 * Manages the UI for the event sidebar/agenda.
50 */
51/**
52 * @class EventListView
53 * @brief Main eventlist view class
54 *
55 * @details For detailed documentation see the main class documentation.
56 */
57/**
58 * @class EventListView
59 * @brief Main eventlist view class
60 *
61 * @details For detailed documentation see the main class documentation.
62 */
63export class EventListView {
64 public actor: any; // The main container (St.BoxLayout)
65 private _eventsBox: any; // Container for the list of event rows
66 private _selectedDateLabel: any; // Header showing the current date or range
67
68 constructor() {
69 // Main layout: vertical box containing the header and the scrollable list
70 this.actor = new St.BoxLayout({
71 style_class: "calendar-events-main-box",
72 vertical: true,
73 x_expand: true
74 });
75
76 // Header Label: Shows "Monday, January 1, 2026" or "January 2026"
77 this._selectedDateLabel = new St.Label({
78 style_class: "calendar-events-date-label"
79 });
80 this.actor.add_actor(this._selectedDateLabel);
81
82 /**
83 * ScrollView: Essential for UI usability.
84 * Policy 1 (NEVER) for horizontal: We want text to wrap or clip, not scroll sideways.
85 * Policy 2 (AUTOMATIC) for vertical: Shows scrollbar only if content exceeds height.
86 */
87 let scrollBox = new St.ScrollView({
88 style_class: 'calendar-events-scrollbox vfade',
89 hscrollbar_policy: 1,
90 vscrollbar_policy: 2
91 });
92
93 // Internal box for the actual event entries
94 this._eventsBox = new St.BoxLayout({
95 style_class: 'calendar-events-event-container',
96 vertical: true
97 });
98
99 scrollBox.add_actor(this._eventsBox);
100 this.actor.add_actor(scrollBox);
101 }
102 /**
103 * Legacy/Generic Update Method
104 * This remains as a central entry point. By default, it treats
105 * the input as a single day update.
106 * @param date - The date to display in the header.
107 * @param events - Array of events.
108 */
109 public update(date: Date, events: any[]): void {
110 this.updateForDate(date, events);
111 }
112
113 /**
114 * Update the list for a specific day.
115 * @param date - The specific day to display.
116 * @param events - Array of events for this day.
117 */
118 public updateForDate(date: Date, events: any[] = []): void {
119 this._selectedDateLabel.set_text(date.toLocaleDateString(undefined, {
120 weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
121 }));
122
123 this._eventsBox.destroy_children();
124
125 if (!events || events.length === 0) {
126 this._showNoEvents();
127 return;
128 }
129
130 // Render rows without showing the date (redundant in single day view)
131 events.forEach((ev) => this._addEventRow(ev, false));
132 }
133
134 /**
135 * Update the list for a specific month overview.
136 * @param year - The year of the month.
137 * @param month - The month index (0-11).
138 * @param events - All events within this month.
139 */
140 public updateForMonth(year: number, month: number, events: any[]): void {
141 const date = new Date(year, month, 1);
142 this._selectedDateLabel.set_text(date.toLocaleDateString(undefined, {
143 month: 'long', year: 'numeric'
144 }));
145
146 this._eventsBox.destroy_children();
147
148 if (!events.length) {
149 this._showNoEvents();
150 return;
151 }
152
153 // Render rows with dates enabled so users can see which day the event belongs to
154 events.forEach(ev => this._addEventRow(ev, true));
155 }
156
157 /**
158 * Update the list for a specific date range.
159 * @param range - Object with from and to dates.
160 * @param events - Events within this range.
161 */
162 public updateForRange(range: DateRange, events: any[]): void {
163 this._selectedDateLabel.set_text(this._formatRangeLabel(range));
164 this._eventsBox.destroy_children();
165
166 if (!events.length) {
167 this._showNoEvents();
168 return;
169 }
170
171 // Range views usually benefit from seeing the date per row
172 events.forEach(ev => this._addEventRow(ev, true));
173 }
174
175 /**
176 * Helper to format a DateRange into a readable string.
177 */
178 private _formatRangeLabel(range: DateRange): string {
179 const opts: Intl.DateTimeFormatOptions = {
180 day: 'numeric',
181 month: 'short',
182 year: 'numeric'
183 };
184 return `${range.from.toLocaleDateString(undefined, opts)} – ${range.to.toLocaleDateString(undefined, opts)}`;
185 }
186
187 /**
188 * Renders a placeholder state when no events are present.
189 */
190 private _showNoEvents() {
191 let box = new St.BoxLayout({
192 style_class: "calendar-events-no-events-box",
193 vertical: true,
194 x_align: 2 // Clutter.ActorAlign.CENTER
195 });
196
197 box.add_actor(new St.Icon({
198 icon_name: 'office-calendar',
199 icon_size: 48
200 }));
201
202 box.add_actor(new St.Label({
203 text: "No Events",
204 style_class: "calendar-events-no-events-label"
205 }));
206
207 this._eventsBox.add_actor(box);
208 }
209
210 /**
211 * Creates a stylized row for a single event.
212 * @param ev - The event data object.
213 * @param showDate - Whether to display the day/month prefix.
214 */
215 private _addEventRow(ev: any, showDate: boolean = false) {
216 let row = new St.BoxLayout({
217 style_class: "calendar-event-button",
218 reactive: true,
219 can_focus: true,
220 track_hover: true
221 });
222
223 // Event listener to trigger navigation when a row is clicked
224 row.connect('button-press-event', () => {
225 if (ev.start) {
226 // Emit signal so CalendarView can jump to this specific date
227 this.emit('event-clicked', ev);
228 }
229 return Clutter.EVENT_STOP;
230 });
231
232 // Visual indicator: Color strip matching the source calendar color
233 let colorStrip = new St.Bin({
234 style_class: "calendar-event-color-strip",
235 style: `background-color: ${ev.color || '#3498db'}; width: 4px;`
236 });
237 row.add_actor(colorStrip);
238
239 let contentVBox = new St.BoxLayout({
240 style_class: "calendar-event-row-content",
241 vertical: true,
242 x_expand: true
243 });
244
245 // Date Label: Only shown in month/range overviews for orientation
246 if (showDate && ev.start) {
247 let dateStr = ev.start.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' });
248 contentVBox.add_actor(new St.Label({
249 text: dateStr,
250 style_class: "calendar-event-date-small"
251 }));
252 }
253
254 // Event Title
255 contentVBox.add_actor(new St.Label({
256 text: ev.summary || "Unnamed Event",
257 style_class: "calendar-event-summary"
258 }));
259
260 // Optional: Sub-text (e.g., location or description)
261 if (ev.description) {
262 contentVBox.add_actor(new St.Label({
263 text: ev.description || "",
264 style_class: "calendar-event-time-future"
265 }));
266 }
267
268 row.add_actor(contentVBox);
269 this._eventsBox.add_actor(row);
270 }
271}
272
273/**
274 * Add GJS Signal support to the prototype.
275 * This allows the view to emit the 'event-clicked' signal.
276 */
277Signals.addSignalMethods(EventListView.prototype);
278
279/**
280 * HYBRID EXPORT SYSTEM
281 */
282if (typeof exports !== 'undefined') {
283 exports.EventListView = EventListView;
284}
285(global as any).EventListView = EventListView;