L'idea

Ci sono strumenti che vogliono fare tutto: sincronizzarsi con dieci servizi, prevedere le nostre abitudini, riempire lo schermo di funzioni. E poi ci sono strumenti che partono da una domanda più semplice: come posso vedere i miei appuntamenti, capirli in fretta e gestirli senza distrazioni?

CalenDaros nasce da questa seconda idea. È un'applicazione desktop Java per la gestione degli appuntamenti, pensata per essere immediata, leggibile e concreta. Non prova a sostituire un intero ecosistema di produttività: vuole essere un calendario personale chiaro, installabile, utilizzabile anche senza IDE e abbastanza flessibile da accompagnare sia una consultazione rapida sia una pianificazione più dettagliata.

Il cuore del progetto è una vista mensile completa, dove ogni giorno trova il suo spazio e gli appuntamenti diventano parte visibile del mese. Accanto a questa c'è una modalità compatta, pensata per quando serve tenere il calendario a portata di mano senza occupare troppo spazio sul desktop. L'interfaccia punta su una navigazione semplice, filtri per categoria, evidenziazione dei giorni importanti e supporto multilingua tramite risorse dedicate.

Caricamento immagineCalendario Java

La versione 1.0.1: rendere il progetto più facile da usare

Caricamento immagineLa versione 1.0.1

La versione 1.0.1, rilasciata il 7 aprile 2025, ha avuto un obiettivo molto pratico: abbassare la soglia d'ingresso.

Il progetto era già una piccola applicazione desktop funzionante, con visualizzazione del calendario, appuntamenti, vista estesa e modalità compatta. Con la 1.0.1 è arrivato un tassello importante per chi usa Windows: il file compila_e_avvia.bat. Una scelta semplice, ma significativa. Non tutti vogliono aprire Eclipse, configurare classpath o compilare manualmente da terminale. Avere uno script che compila i sorgenti, copia le risorse e avvia l'applicazione rende CalenDaros più accessibile, più vicino a un programma da provare subito.

Nella stessa direzione va anche il miglioramento del README, aggiornato con istruzioni più chiare per gli utenti Windows. La documentazione non è un accessorio: è il primo punto di contatto tra un progetto e chi prova a usarlo. Una guida più esplicita rende il software meno intimidatorio e più accogliente.

La 1.0.1 ha introdotto anche una riorganizzazione della struttura delle cartelle, con l'uso della cartella i18n per l'internazionalizzazione. È un dettaglio tecnico, ma racconta una direzione precisa: CalenDaros non vuole essere rigido. Vuole poter parlare più lingue, separando l'interfaccia dalla logica dell'applicazione e preparando il terreno per una crescita più ordinata.

La versione 1.0.2: dal calendario che mostra al calendario che accompagna

Caricamento immagineLa versione 1.0.2

Con la versione 1.0.2, rilasciata il 15 giugno 2026, CalenDaros cambia passo. Non si limita più a mostrare appuntamenti: comincia a comportarsi come uno strumento quotidiano, più fluido, più persistente, più vicino ai gesti naturali dell'utente.

La prima grande evoluzione è il salvataggio permanente degli appuntamenti su disco. È il passaggio che trasforma un prototipo utile in un'applicazione davvero utilizzabile. Gli appuntamenti non sono più dati temporanei o esempi da osservare: diventano informazioni dell'utente, conservate tra un avvio e l'altro. Per questo sono stati rimossi gli appuntamenti di esempio, lasciando spazio a un calendario personale, pulito, pronto a essere riempito con eventi reali.

Anche l'inserimento degli appuntamenti è diventato più naturale. Ora, nella vista estesa, basta cliccare su un'area vuota del giorno per aprire l'interfaccia di creazione. Il vecchio pulsante "Nuovo Appuntamento" non serve più, perché il gesto è già dentro al calendario. Si clicca dove si vuole aggiungere qualcosa, e il calendario risponde lì, con il contesto giusto.

La form è stata ripensata per essere più coerente: il campo Tipo è stato spostato dopo la descrizione, è stato aggiunto il tipo "Altro" come valore predefinito, i pulsanti Salva e Annulla sono stati organizzati in una colonna laterale e la visualizzazione dei dettagli usa la stessa logica della form. Anche il riquadro dei dettagli e quello di inserimento sono stati uniformati in altezza, riducendo salti visivi e rendendo l'interfaccia più stabile.

Più controllo sugli eventi

Caricamento immagineIl Drag-and-Drop

La 1.0.2 introduce un modo più diretto di gestire gli appuntamenti già presenti. Sono arrivati i pulsanti Modifica ed Elimina con icone, insieme ai pulsanti Salva e Annulla anch'essi resi più riconoscibili. L'icona del cestino e quella dell'annullamento sono rosse, una scelta piccola ma efficace: le azioni distruttive o di uscita diventano immediatamente distinguibili.

Gli eventi possono essere trascinati da un giorno all'altro. Durante il trascinamento compare un feedback visuale, così l'utente capisce sempre cosa sta spostando e dove. Con CTRL + trascinamento, invece, l'evento viene duplicato. È una funzione preziosa per chi ha appuntamenti simili in giorni diversi: non bisogna reinserire tutto, basta copiare e adattare.

Anche l'inserimento dell'orario è più comodo, grazie a una combobox che va da 00:00 a 23:45 con intervalli di 15 minuti. Meno digitazione libera, meno errori, più velocità.

Una sidebar più matura

Caricamento immagineLa Sidebar

Un'altra trasformazione importante riguarda la struttura dell'interfaccia. In modalità estesa, il mini calendario laterale non viene più mostrato: resta disponibile nella modalità compatta, dove ha più senso. La zona sinistra è diventata una vera sidebar, più ordinata e più funzionale.

L'ordine è stato ripensato: mese, pannello di navigazione, lingua, modalità compatta, sempre in primo piano e filtri. In basso trova spazio una sezione Help con i comandi disponibili, pensata per ricordare all'utente i gesti principali: clic per aggiungere, trascinamento per spostare, CTRL + trascinamento per duplicare.

La navigazione stessa è stata alleggerita: il pulsante "Oggi" è stato sostituito da un'icona circolare con un punto al centro. È un piccolo cambio di linguaggio visivo: meno testo, più immediatezza.

Lingue, temi e finestra sempre visibile

Caricamento immagineIl cambio della lingua

La versione 1.0.2 lavora molto anche sulla personalizzazione. Il cambio lingua è diventato più chiaro grazie all'uso delle bandiere italiana e inglese al posto dell'icona generica del mappamondo. È stato aggiunto un toggle per il tema chiaro/scuro.

È arrivata anche la modalità "Sempre in primo piano", utile per chi vuole tenere il calendario visibile sopra alle altre finestre. È una funzione piccola solo in apparenza: per un calendario desktop, poter restare presente mentre si lavora altrove è una qualità molto concreta.

La 1.0.2 aggiunge inoltre alcuni argomenti di avvio, pensati per chi vuole aprire CalenDaros già nello stato più adatto al proprio flusso di lavoro. Da linea di comando si può scegliere se mantenere la finestra sempre in primo piano con --always-on-top, dove posizionarla sullo schermo con --top-left, --top-right, --bottom-left, --bottom-right o --center, quale tema usare con --dark o --light, e quale vista aprire con --compact o --extended.

Per esempio, questo comando avvia il calendario in alto a destra, con tema scuro, in modalità compatta e sempre visibile:

compila_e_avvia.bat --always-on-top --top-right --dark --compact

Importa, Esporta e Tema sono stati trasformati in pulsanti a icona, ridimensionati e organizzati sulla stessa riga degli altri controlli. La sidebar diventa così più compatta, più pulita e più adatta a un uso frequente.

Importazione, esportazione e dialogo con Google Calendar

Caricamento immagineImportazione ed Esportazione

CalenDaros 1.0.2 apre anche una porta verso l'esterno. La sidebar include ora i comandi per importare ed esportare file .ics, il formato comunemente usato dai calendari digitali. L'importazione consente di selezionare dal computer un file compatibile, mentre l'esportazione permette di creare un file che può essere importato anche in Google Calendar.

Nota: per esportare gli eventi da Google Calendar, vai su https://calendar.google.com/ > menu Impostazioni > Impostazioni > Importazione ed esportazione > Esporta.

Questo non trasforma CalenDaros in un servizio cloud, e va bene così. Resta un'applicazione desktop semplice, ma diventa meno isolata. Può dialogare con altri strumenti quando serve, mantenendo però la sua identità locale e leggera.

Una griglia mensile più precisa

Caricamento immagineGriglia 7x6

Anche la vista del mese è stata resa più solida. La griglia 7x6 mostra sempre giorni del mese precedente e del mese successivo, in modo da riempire completamente lo spazio disponibile.

Sono dettagli che si notano soprattutto quando mancano. Una griglia piena, regolare, prevedibile rende il calendario più facile da leggere e più gradevole da usare.

In modalità compatta, i giorni con appuntamenti sono evidenziati con uno sfondo diverso, e il mini calendario mostra l'anteprima degli appuntamenti tramite tooltip. Anche nella vista estesa, passando il mouse sugli eventi, compare la descrizione. Il calendario comunica di più senza costringere l'utente ad aprire ogni dettaglio.

La versione 1.0.3: quando l'interfaccia comincia a respirare meglio

La versione 1.0.3, rilasciata il 19 giugno 2026, non aggiunge molte funzionalità. Il suo obiettivo è un altro, ma non meno importante: rendere CalenDaros più piacevole da guardare e più intuitivo da usare.

Dopo la 1.0.2 l'applicazione era già molto più completa. Si potevano salvare appuntamenti, importarli, esportarli, trascinarli, cambiare tema, lavorare in compatta o in estesa. A quel punto il problema non era più "cosa manca?", ma "quanto è comoda questa interfaccia quando la uso davvero?". La versione 1.0.3 non rivoluziona l'app: rifinisce quei dettagli che, pur passando quasi inosservati, migliorano concretamente la qualità dell'esperienza.

Il cambiamento più visibile riguarda le icone. I controlli principali sono passati a icone SVG in stile Material Symbols, gestite tramite FlatLaf Extras. Questo permette di avere simboli più puliti, scalabili e coerenti con un'interfaccia moderna. Non è stato però eliminato il lavoro precedente: le vecchie icone disegnate in Java restano come fallback. Se le librerie esterne non sono disponibili, l'applicazione continua comunque a funzionare.

Questa scelta racconta bene una piccola filosofia del progetto: migliorare l'aspetto senza rendere il programma fragile. Per questo lo script compila_e_avvia.bat ora scarica anche le dipendenze necessarie, cioè flatlaf-extras.jar e jsvg.jar, oltre a FlatLaf. L'utente non deve sapere quali librerie servono per renderizzare un'icona SVG: avvia lo script e trova l'applicazione pronta.

Icone, spazi e piccoli segnali visivi

La 1.0.3 lavora molto sulla sidebar. I nove pulsanti superiori sono stati uniformati per dimensione, allineamento e distanza dal bordo. Prima alcuni gruppi apparivano più compressi, altri più lontani, altri ancora meno regolari. Ora formano una piccola toolbar più ordinata: tre pulsanti per la navigazione del mese, tre per lingua, vista e primo piano, tre per importazione, esportazione e tema.

Anche i colori sono stati rivisti con più attenzione. Le icone devono restare leggibili sia in tema chiaro sia in tema scuro. Alcune azioni mantengono un colore espressivo, come il giallo per la modifica, il verde per il salvataggio e il rosso per eliminare o annullare. Altre, invece, restano neutre per non appesantire l'interfaccia: navigazione, importazione, esportazione e cambio modalità non devono gridare, devono solo essere riconoscibili.

La modalità "Sempre in primo piano" usa invece un codice colore più esplicito: verde quando indica l'attivazione, rosso quando indica la disattivazione. Qui il colore non è decorazione, è informazione.

Una vista estesa più stabile

La vista estesa ha ricevuto due miglioramenti importanti. Il primo riguarda l'intestazione dei giorni della settimana. Prima i nomi dei giorni vivevano dentro la stessa griglia delle celle del calendario, e quindi occupavano troppo spazio verticale. Ora l'intestazione è una riga separata, più bassa, pensata per contenere solo i nomi. La griglia dei giorni resta sotto, più libera e più equilibrata.

Il secondo miglioramento riguarda il ridimensionamento della finestra. Quando l'utente allarga o restringe il calendario esteso, CalenDaros ora ricorda quella dimensione. E non la ricorda solo finché l'app resta aperta: la salva in modo permanente in ~/.calenDaros/ui.properties. Questo significa che, anche dopo la chiusura, la modalità estesa torna alla dimensione scelta dall'utente.

È una funzione silenziosa, ma molto concreta. Un'app desktop vive nello spazio personale del monitor: ognuno ha il suo modo di disporre finestre, strumenti, browser, editor, terminali. Ricordare la dimensione scelta significa rispettare quella disposizione.

Anche i cursori sono stati resi più comunicativi. Passando sopra i pulsanti Modifica, Salva, Annulla ed Elimina, il puntatore cambia forma. Lo stesso accade sui riquadri dei giorni nella vista estesa, perché sono aree cliccabili. Non serve spiegare all'utente che può interagire: l'interfaccia glielo suggerisce.

Dentro il codice

Alcune scelte della versione 1.0.2 si vedono bene anche guardando direttamente il sorgente. La griglia mensile, per esempio, non dipende da celle vuote inserite a mano: viene calcolato il primo giorno visibile e poi vengono renderizzate sempre 42 celle, così il mese resta stabile in formato 7x6.

Prima ancora della griglia, però, vale la pena guardare il percorso dell'importazione .ics. Il pulsante nella sidebar non contiene logica complessa: apre il flusso dedicato e lascia al metodo importAppointmentsFromIcs() la responsabilità di selezionare il file, validarlo e integrare gli eventi nel calendario.

JButton importButton = createStyledButton("", getControlBackground());
importButton.setIcon(createImportIcon(getControlForeground()));
importButton.setToolTipText(Calendar_i18n.getString("button.import_tooltip"));
importButton.getAccessibleContext().setAccessibleName(Calendar_i18n.getString("button.import"));
importButton.addActionListener(e -> importAppointmentsFromIcs());

L'importazione parte da un JFileChooser con filtro sul formato iCalendar. Dopo la selezione, il codice controlla anche l'estensione del file: è una piccola difesa in più, utile perché il filtro "grafico" non basta sempre a garantire che l'utente abbia scelto davvero un .ics.

private void importAppointmentsFromIcs() {
    JFileChooser fileChooser = new JFileChooser();
    fileChooser.setDialogTitle(Calendar_i18n.getString("dialog.import_title"));
    fileChooser.setFileFilter(new FileNameExtensionFilter("iCalendar (*.ics)", "ics"));

    if (fileChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) {
        return;
    }

    Path selectedFile = fileChooser.getSelectedFile().toPath();
    if (!selectedFile.getFileName().toString().toLowerCase().endsWith(".ics")) {
        JOptionPane.showMessageDialog(this,
            Calendar_i18n.getString("error.import_extension"),
            Calendar_i18n.getString("dialog.import_title"),
            JOptionPane.WARNING_MESSAGE);
        return;
    }

    try {
        List<CalendarAppointment> importedAppointments = readAppointmentsFromIcs(selectedFile);
        // Gli appuntamenti letti verranno filtrati, salvati e mostrati nella UI.
    } catch (IOException e) {
        JOptionPane.showMessageDialog(this,
            MessageFormat.format(Calendar_i18n.getString("error.import"), e.getMessage()),
            Calendar_i18n.getString("dialog.import_title"),
            JOptionPane.WARNING_MESSAGE);
    }
}

La lettura del file segue la struttura tipica del formato iCalendar: ogni appuntamento è racchiuso tra BEGIN:VEVENT ed END:VEVENT. Il metodo accumula le proprietà dell'evento in una mappa, normalizza il nome dei campi e conserva anche la proprietà originale, utile per leggere informazioni come TZID o VALUE=DATE.

private List<CalendarAppointment> readAppointmentsFromIcs(Path icsFile) throws IOException {
    List<String> lines = unfoldIcsLines(Files.readAllLines(icsFile, StandardCharsets.UTF_8));
    List<CalendarAppointment> appointments = new ArrayList<>();
    Map<String, String> event = null;

    for (String line : lines) {
        if ("BEGIN:VEVENT".equals(line)) {
            event = new HashMap<>();
        } else if ("END:VEVENT".equals(line)) {
            CalendarAppointment appointment = createAppointmentFromIcsEvent(event);
            if (appointment != null) {
                appointments.add(appointment);
            }
            event = null;
        } else if (event != null) {
            int separatorIndex = line.indexOf(':');
            if (separatorIndex > 0) {
                String property = line.substring(0, separatorIndex);
                String value = line.substring(separatorIndex + 1);
                String key = property.split(";", 2)[0].toUpperCase();
                event.put(key, value);
                event.put(key + ".PROPERTY", property);
            }
        }
    }

    return appointments;
}

Il formato .ics permette anche righe spezzate: una riga che inizia con uno spazio o con una tabulazione continua quella precedente. Per questo, prima del parsing vero e proprio, CalenDaros ricompone le righe con unfoldIcsLines().

private List<String> unfoldIcsLines(List<String> lines) {
    List<String> unfoldedLines = new ArrayList<>();

    for (String line : lines) {
        if ((line.startsWith(" ") || line.startsWith("\t")) && !unfoldedLines.isEmpty()) {
            int lastIndex = unfoldedLines.size() - 1;
            unfoldedLines.set(lastIndex, unfoldedLines.get(lastIndex) + line.substring(1));
        } else {
            unfoldedLines.add(line);
        }
    }

    return unfoldedLines;
}

Una volta isolato un VEVENT, il codice lo trasforma in un CalendarAppointment. Gli eventi cancellati vengono ignorati, SUMMARY diventa il titolo, DESCRIPTION diventa la descrizione e, se manca, viene sostituita dal titolo. Il tipo assegnato agli eventi importati è "Altro", così l'appuntamento entra nel calendario senza inventare categorie non presenti nel file.

private CalendarAppointment createAppointmentFromIcsEvent(Map<String, String> event) {
    if (event == null || "CANCELLED".equalsIgnoreCase(event.get("STATUS"))) {
        return null;
    }

    String startValue = event.get("DTSTART");
    String summary = unescapeIcsText(event.getOrDefault("SUMMARY", "")).trim();
    if (startValue == null || summary.isEmpty()) {
        return null;
    }

    Calendar startDate = parseIcsStartDate(event.getOrDefault("DTSTART.PROPERTY", "DTSTART"), startValue);
    if (startDate == null) {
        return null;
    }

    String description = unescapeIcsText(event.getOrDefault("DESCRIPTION", "")).trim();
    if (description.isEmpty()) {
        description = summary;
    }

    return new CalendarAppointment(
        startDate.get(Calendar.YEAR),
        startDate.get(Calendar.MONTH),
        startDate.get(Calendar.DAY_OF_MONTH),
        String.format("%02d:%02d", startDate.get(Calendar.HOUR_OF_DAY), startDate.get(Calendar.MINUTE)),
        summary,
        description,
        Calendar_i18n.getString("appointment.other")
    );
}

La parte più delicata è la data di inizio. Un file .ics può contenere un evento di giornata intera (VALUE=DATE), un orario UTC con suffisso Z, oppure un orario associato a un fuso specifico tramite TZID. Il metodo parseIcsStartDate() copre questi tre casi e riconduce tutto al fuso locale dell'utente.

private Calendar parseIcsStartDate(String property, String value) {
    try {
        ZoneId localZone = ZoneId.systemDefault();
        ZonedDateTime dateTime;

        if (property.toUpperCase().contains("VALUE=DATE")) {
            LocalDate date = LocalDate.parse(value, DateTimeFormatter.BASIC_ISO_DATE);
            dateTime = date.atStartOfDay(localZone);
        } else if (value.endsWith("Z")) {
            LocalDateTime utcDateTime = LocalDateTime.parse(
                value.substring(0, value.length() - 1),
                DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")
            );
            dateTime = utcDateTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(localZone);
        } else {
            ZoneId eventZone = extractIcsZone(property, localZone);
            LocalDateTime localDateTime = LocalDateTime.parse(
                value,
                DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")
            );
            dateTime = localDateTime.atZone(eventZone).withZoneSameInstant(localZone);
        }

        Calendar parsedDate = Calendar.getInstance();
        parsedDate.setTimeInMillis(dateTime.toInstant().toEpochMilli());
        return parsedDate;
    } catch (RuntimeException e) {
        return null;
    }
}

Infine, gli eventi importati non vengono aggiunti alla cieca. Prima viene controllato se esiste già un appuntamento con stessa data, stesso orario, stesso titolo e stessa descrizione. In questo modo una seconda importazione dello stesso file non riempie il calendario di duplicati.

private boolean containsAppointment(CalendarAppointment importedAppointment) {
    for (CalendarAppointment appointment : customAppointments) {
        if (appointment.year == importedAppointment.year
                && appointment.month == importedAppointment.month
                && appointment.day == importedAppointment.day
                && appointment.time.equals(importedAppointment.time)
                && appointment.title.equals(importedAppointment.title)
                && appointment.description.equals(importedAppointment.description)) {
            return true;
        }
    }
    return false;
}

Questa parte del codice mostra bene la filosofia della funzione: importare da .ics non significa soltanto leggere un file, ma tradurre un formato esterno nel modello semplice di CalenDaros, rispettando date, fusi orari, testi escapati e possibili duplicati.

private void updateAppointmentPanel() {
    appointmentPanel.removeAll();
    appointmentPanel.setLayout(new GridLayout(0, 7, 1, 1));

    Calendar firstVisibleDay = (Calendar) calendar.clone();
    firstVisibleDay.set(Calendar.DAY_OF_MONTH, 1);
    int firstDayOffset = (firstVisibleDay.get(Calendar.DAY_OF_WEEK) + 5) % 7;
    if (firstDayOffset == 0) {
        firstDayOffset = 7;
    }
    firstVisibleDay.add(Calendar.DAY_OF_MONTH, -firstDayOffset);

    for (int cell = 0; cell < 42; cell++) {
        Calendar visibleDay = (Calendar) firstVisibleDay.clone();
        visibleDay.add(Calendar.DAY_OF_MONTH, cell);

        JPanel dayPanel = new JPanel();
        dayPanel.setLayout(new BoxLayout(dayPanel, BoxLayout.Y_AXIS));
        dayPanel.putClientProperty("calendarDay", visibleDay.get(Calendar.DAY_OF_MONTH));
        dayPanel.putClientProperty("calendarMonth", visibleDay.get(Calendar.MONTH));
        dayPanel.putClientProperty("calendarYear", visibleDay.get(Calendar.YEAR));
        appointmentPanel.add(dayPanel);
    }
}

Dentro questo metodo c'è un passaggio piccolo, ma molto interessante:

firstVisibleDay.set(Calendar.DAY_OF_MONTH, 1);
int firstDayOffset = (firstVisibleDay.get(Calendar.DAY_OF_WEEK) + 5) % 7;
if (firstDayOffset == 0) {
    firstDayOffset = 7;
}
firstVisibleDay.add(Calendar.DAY_OF_MONTH, -firstDayOffset);

Qui il calendario non si limita a chiedersi qual è il primo giorno del mese. Si chiede invece quale giorno deve comparire nella prima cella della griglia. È una differenza sottile, ma importante: in una vista mensile 7x6, il mese non inizia quasi mai davvero dalla prima casella. Prima del giorno 1 possono esserci alcuni giorni del mese precedente, necessari per mantenere la settimana allineata e la griglia sempre piena.

La prima riga porta firstVisibleDay al giorno 1 del mese corrente. Poi entra in gioco Calendar.DAY_OF_WEEK, che in Java usa una numerazione un po' particolare: domenica vale 1, lunedi 2, martedi 3 e così via fino a sabato 7. La formula (dayOfWeek + 5) % 7 riconverte questa logica in una settimana che parte da lunedi: lunedi diventa 0, martedi 1, mercoledi 2, fino a domenica che diventa 6.

Quel numero indica quanti giorni bisogna tornare indietro per arrivare al lunedi della settimana da mostrare. Se il mese comincia di mercoledi, per esempio, l'offset è 2: la griglia partirà dal lunedi precedente. Il caso firstDayOffset == 0 è una scelta esplicita: se il mese comincia già di lunedi, il codice non parte dal giorno 1, ma torna indietro di sette giorni e mostra anche la settimana precedente. In questo modo la vista mensile conserva sempre una riga iniziale di contesto.

È uno di quei dettagli che raccontano bene la differenza tra "stampare le date" e progettare davvero un calendario. Il codice sta prendendo una decisione di esperienza utente: offrire una griglia regolare, prevedibile, con abbastanza contesto prima e dopo il mese corrente.

Il drag & drop degli appuntamenti usa invece le proprietà salvate nel pannello del giorno: quando il mouse viene rilasciato, il codice risale alla cella di destinazione e passa anno, mese e giorno al listener. Nella stessa chiamata viene passato anche e.isControlDown(), cioè l'informazione sul fatto che il tasto CTRL sia premuto o meno: è questo valore booleano a decidere se la stessa interazione deve diventare uno spostamento o una duplicazione.

@Override
public void mouseDragged(MouseEvent e) {
    if (dragStart == null || dragListener == null) {
        return;
    }

    if (dragStart.distance(e.getPoint()) > 4) {
        dragging = true;
        appointmentPanel.setCursor(new Cursor(Cursor.MOVE_CURSOR));
        showDragPreview(e, time, title, color, e.isControlDown());
    }
}

@Override
public void mouseReleased(MouseEvent e) {
    hideDragPreview();

    if (!dragging || dragListener == null) {
        dragStart = null;
        dragging = false;
        return;
    }

    JPanel targetDayPanel = findTargetDayPanel(e);
    if (targetDayPanel != null && targetDayPanel != dayPanel) {
        Object dayValue = targetDayPanel.getClientProperty("calendarDay");
        Object monthValue = targetDayPanel.getClientProperty("calendarMonth");
        Object yearValue = targetDayPanel.getClientProperty("calendarYear");
        if (dayValue instanceof Integer && monthValue instanceof Integer && yearValue instanceof Integer) {
            dragListener.accept(new int[] {
                (Integer) yearValue, (Integer) monthValue, (Integer) dayValue
            }, e.isControlDown());
        }
    }
}

La gestione degli eventi resta volutamente lineare: un nuovo appuntamento viene validato, trasformato in CalendarAppointment, aggiunto alla lista, salvato su disco e subito mostrato nella vista aggiornata.

private void saveNewAppointment(int day, JTextField timeField, JTextField titleField,
        JTextArea descriptionArea, JComboBox<String> typeCombo) {
    String time = timeField.getText().trim();
    String title = titleField.getText().trim();
    String description = descriptionArea.getText().trim();
    String type = (String) typeCombo.getSelectedItem();

    if (time.isEmpty() || title.isEmpty()) {
        JOptionPane.showMessageDialog(this,
            Calendar_i18n.getString("form.required_fields"),
            Calendar_i18n.getString("info.title"),
            JOptionPane.WARNING_MESSAGE);
        return;
    }

    if (description.isEmpty()) {
        description = title;
    }

    CalendarAppointment appointment = new CalendarAppointment(
        calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH),
        day, time, title, description, type
    );
    customAppointments.add(appointment);
    saveAppointmentsToDisk();
    updateAppointmentPanel();
    showAppointmentDetails(appointment);
}

Meno rumore, più cura

La 1.0.2 porta con sé anche pulizia interna. Alcuni System.out.println di debug ancora presenti in produzione sono stati ricondotti alla classe Debug, già prevista dal progetto. È una scelta da manutentore: il codice non deve solo funzionare, deve anche restare leggibile e governabile.

public class Debug {
    private static final boolean ENABLED = false;

    public static void log(String message) {
        if (ENABLED) {
            System.out.println(message);
        }
    }
}

Questo piccolo snippet basta a spiegare l'idea: il debug esiste, ma passa da un punto solo. Quando ENABLED è false, i messaggi non finiscono in console; quando serve indagare un comportamento, si può riattivare il tracciamento senza andare a cercare stampe sparse nel codice.

Nel codice applicativo l'uso diventa molto più leggibile di una stampa diretta:

revalidate();
repaint();
Debug.log("Mini calendar panel revalidated and repainted");

Il messaggio resta vicino all'operazione che si vuole osservare, ma la decisione di mostrarlo o meno rimane nella classe Debug. È una separazione piccola, quasi invisibile, e proprio per questo utile: durante lo sviluppo aiuta a capire cosa succede nell'interfaccia, mentre nella versione normale non sporca l'output dell'applicazione.

In sintesi

Caricamento immagineModalità Compatta v1.0.1, v1.0.2 e v1.0.3

Versioni 1.0.1, 1.0.2 e 1.0.3

CalenDaros 1.0.1 ha reso il progetto più facile da avviare e più ordinato nella struttura. Ha migliorato l'esperienza iniziale, soprattutto per gli utenti Windows, e ha preparato il terreno per l'internazionalizzazione.

CalenDaros 1.0.2 ha invece lavorato sull'uso quotidiano: salvataggio persistente, creazione rapida degli appuntamenti, modifica più comoda, drag and drop, duplicazione, tema scuro, argomenti di avvio, import/export .ics, sidebar riorganizzata, tooltip, icone, griglia mensile più precisa e una generale pulizia dell'interfaccia.

CalenDaros 1.0.3 ha poi messo a fuoco l'aspetto: icone SVG Material Symbols, toolbar più regolare, colori più leggibili, intestazione del calendario esteso più compatta, cursori coerenti e dimensione della vista estesa ricordata in modo permanente.

Il risultato è un calendario desktop che cresce senza perdere la sua idea originale: aiutare a vedere il tempo, organizzare gli appuntamenti e farlo con uno strumento semplice, vicino, comprensibile. Un progetto piccolo nel formato, ma con una direzione chiara: diventare ad ogni nuova versione un po' più utile, un po' più curato, un po' più personale.

Caricamento immagineModalità Estesa v1.0.1

CalenDaros versione 1.0.1

Caricamento immagineModalità Estesa v1.0.2

CalenDaros versione 1.0.2

Caricamento immagineModalità Estesa v1.0.2

CalenDaros versione 1.0.3

Codice sorgente: Apri calenDaros su GitHub →