Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
# AppLaunchService (QProcess) — they are not linked into the desktop shell,
# so a crash or hang in an app cannot take down the desktop.
add_subdirectory(calculator)
add_subdirectory(noter)
add_subdirectory(system_state)
add_subdirectory(alarm_clock)
add_subdirectory(calendar)
27 changes: 27 additions & 0 deletions apps/alarm_clock/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Alarm Clock standalone application.
# Ported from CCIMXDesktop's AlarmyClock; UI uses QuarkWidgets MD3 widgets.
# CFDesktop launches this binary via AppLaunchService (QProcess), so it runs in
# its own process (isolated from the desktop shell).

# Standalone Alarm Clock executable (consumed by CFDesktop via QProcess launch).
qt_add_executable(alarm_clock
main.cpp
alarm_clock_panel.cpp
)

target_link_libraries(alarm_clock PRIVATE
QuarkWidgets::quarkwidgets
Qt6::Widgets
)

# App package is self-contained under <bin>/../apps/alarm_clock/ (executable +
# manifest in the same dir). AppDiscoverer scans <bin>/../apps/<id>/app.json,
# resolves exec to the co-located absolute path, and AppLaunchService launches
# it directly — no working-directory dependency, no PATH search needed.
set(ALARM_CLOCK_APP_DIR "${CMAKE_BINARY_DIR}/apps/alarm_clock")
set_target_properties(alarm_clock PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${ALARM_CLOCK_APP_DIR}"
)

# Deploy the manifest next to the executable (at configure time).
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/app.json" "${ALARM_CLOCK_APP_DIR}/app.json" COPYONLY)
223 changes: 223 additions & 0 deletions apps/alarm_clock/alarm_clock_panel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* @file apps/alarm_clock/alarm_clock_panel.cpp
* @brief Implementation of the Alarm Clock panel.
*
* Ported from CCIMXDesktop's AlarmyClock. The original AlarmyNotifier polling
* loop (1 s QTimer + per-second QTime match) is preserved verbatim in @ref
* onTick; the broadcaster/processor event bus is collapsed into the direct
* @ref fireAlarm call, and the QPainter analog dial + QMainWindow/.ui shell
* are replaced by a QuarkWidgets MD3 panel built in code.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-07-01
* @version 0.1
* @since 0.20
* @ingroup alarm_clock
*/

#include "alarm_clock_panel.h"

#include "ui/widget/material/widget/button/button.h"
#include "ui/widget/material/widget/label/label.h"

#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMessageBox>
#include <QPainter>
#include <QPainterPath>
#include <QSpinBox>
#include <QTextEdit>
#include <QTimer>
#include <QVBoxLayout>

namespace cf::desktop::desktop_component {

namespace {
using qw::widget::material::Button;
using qw::widget::material::Label;
using qw::widget::material::TypographyStyle;
using Variant = Button::ButtonVariant;

constexpr qreal kCornerRadius = 16.0; ///< Card corner radius (px).

/// @brief Qt item-data role key for the stored AlarmEntry.
constexpr int kAlarmRole = Qt::UserRole + 1;

/// @brief Renders an alarm entry as "hh:mm — note".
QString formatEntry(const AlarmEntry& e) {
return e.time.toString(QStringLiteral("hh:mm")) + QStringLiteral(" — ") +
(e.note.isEmpty() ? QStringLiteral("(no note)") : e.note);
}
} // namespace

AlarmClockPanel::AlarmClockPanel(QWidget* parent) : QWidget(parent) {
setupUi();

// Poll the wall clock once per second (mirrors AlarmyNotifier::check_time).
ticker_ = new QTimer(this);
ticker_->setInterval(1000);
connect(ticker_, &QTimer::timeout, this, &AlarmClockPanel::onTick);
ticker_->start();
onTick();
}

AlarmClockPanel::~AlarmClockPanel() = default;

void AlarmClockPanel::setupUi() {
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(16, 16, 16, 16);
layout->setSpacing(12);

// Live clock readout.
clock_label_ = new Label("--:--:--", TypographyStyle::DisplayLarge, this);
clock_label_->setAlignment(Qt::AlignCenter);
layout->addWidget(clock_label_);

// --- Time editor row: [- hour +] : [- minute +] ---
auto* editor_row = new QHBoxLayout;
editor_row->setSpacing(6);

auto make_spin = [this](int max) {
auto* spin = new QSpinBox(this);
spin->setRange(0, max);
spin->setFixedHeight(40);
spin->setButtonSymbols(QSpinBox::PlusMinus);
return spin;
};
hour_spin_ = make_spin(23);
hour_spin_->setValue(QTime::currentTime().hour());
minute_spin_ = make_spin(59);
minute_spin_->setValue(QTime::currentTime().minute());

auto make_step = [this](const QString& glyph, Variant v) {
auto* btn = new Button(glyph, v, this);
btn->setFixedSize(40, 40);
return btn;
};
auto* h_minus = make_step(QStringLiteral("-"), Variant::Tonal);
auto* h_plus = make_step(QStringLiteral("+"), Variant::Tonal);
auto* m_minus = make_step(QStringLiteral("-"), Variant::Tonal);
auto* m_plus = make_step(QStringLiteral("+"), Variant::Tonal);

connect(h_minus, &QPushButton::clicked, this,
[this] { hour_spin_->setValue(hour_spin_->value() - 1); });
connect(h_plus, &QPushButton::clicked, this,
[this] { hour_spin_->setValue(hour_spin_->value() + 1); });
connect(m_minus, &QPushButton::clicked, this,
[this] { minute_spin_->setValue(minute_spin_->value() - 1); });
connect(m_plus, &QPushButton::clicked, this,
[this] { minute_spin_->setValue(minute_spin_->value() + 1); });

editor_row->addWidget(h_minus);
editor_row->addWidget(hour_spin_);
editor_row->addWidget(h_plus);
auto* colon = new QLabel(QStringLiteral(":"), this);
colon->setAlignment(Qt::AlignCenter);
QFont colon_font = colon->font();
colon_font.setPointSize(20);
colon->setFont(colon_font);
editor_row->addWidget(colon);
editor_row->addWidget(m_minus);
editor_row->addWidget(minute_spin_);
editor_row->addWidget(m_plus);
editor_row->addStretch();
layout->addLayout(editor_row);

// Note editor.
note_edit_ = new QTextEdit(this);
note_edit_->setPlaceholderText(QStringLiteral("Reminder note (optional)"));
note_edit_->setFixedHeight(60);
layout->addWidget(note_edit_);

// Add / Remove control row.
auto* control_row = new QHBoxLayout;
control_row->setSpacing(8);
auto* add_btn = new Button(QStringLiteral("Add Alarm"), Variant::Filled, this);
auto* remove_btn = new Button(QStringLiteral("Remove Selected"), Variant::Outlined, this);
control_row->addWidget(add_btn);
control_row->addWidget(remove_btn);
control_row->addStretch();
layout->addLayout(control_row);

// Armed alarms list.
alarm_list_ = new QListWidget(this);
layout->addWidget(alarm_list_, /*stretch=*/1);

connect(add_btn, &QPushButton::clicked, this, &AlarmClockPanel::onAddAlarm);
connect(remove_btn, &QPushButton::clicked, this, &AlarmClockPanel::onRemoveAlarm);
}

void AlarmClockPanel::onTick() {
const QTime now = QTime::currentTime();
clock_label_->setText(now.toString(QStringLiteral("hh:mm:ss")));

// Match hour+minute+second against every armed alarm (the original
// AlarmyNotifier matches all three fields; entries are stored with second
// forced to 0, so an alarm fires in the first second of its minute).
for (int i = 0; i < alarm_list_->count(); ++i) {
auto* item = alarm_list_->item(i);
const auto entry = item->data(kAlarmRole).value<AlarmEntry>();
if (entry.time.hour() == now.hour() && entry.time.minute() == now.minute() &&
entry.time.second() == now.second()) {
fireAlarm(item);
}
}
}

void AlarmClockPanel::onAddAlarm() {
AlarmEntry entry;
entry.time = QTime(hour_spin_->value(), minute_spin_->value(), 0);
entry.note = note_edit_->toPlainText().trimmed();

// Reject duplicates at the same minute to avoid a double-fire.
for (int i = 0; i < alarm_list_->count(); ++i) {
const auto existing = alarm_list_->item(i)->data(kAlarmRole).value<AlarmEntry>();
if (existing.time.hour() == entry.time.hour() &&
existing.time.minute() == entry.time.minute()) {
return;
}
}

auto* item = new QListWidgetItem(formatEntry(entry), alarm_list_);
item->setData(kAlarmRole, QVariant::fromValue(entry));
alarm_list_->addItem(item);

note_edit_->clear();
}

void AlarmClockPanel::onRemoveAlarm() {
auto selected = alarm_list_->selectedItems();
for (auto* item : selected) {
delete item;
}
}

void AlarmClockPanel::fireAlarm(QListWidgetItem* item) {
const auto entry = item->data(kAlarmRole).value<AlarmEntry>();

// TODO(alarm_clock): add audible ringing. The CCIMX original had no audio
// asset either (it popped a QMessageBox). Wire QSoundEffect with a bundled
// .wav (requires Qt6::Multimedia + a qrc asset) or a platform system sound
// here. For now the visual alert mirrors the original DefaultProcessor.
QMessageBox::information(
this, QStringLiteral("Alarm"),
QStringLiteral("⏰ %1\n\n%2")
.arg(entry.time.toString(QStringLiteral("hh:mm")),
entry.note.isEmpty() ? QStringLiteral("Time's up!") : entry.note));

// One-shot: disarm after firing.
delete item;
}

void AlarmClockPanel::paintEvent(QPaintEvent* /*event*/) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
const QColor surface_color(0xF7, 0xF5, 0xF3);
QPainterPath card;
card.addRoundedRect(QRectF(rect()), kCornerRadius, kCornerRadius);
p.fillPath(card, surface_color);
}

} // namespace cf::desktop::desktop_component
135 changes: 135 additions & 0 deletions apps/alarm_clock/alarm_clock_panel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* @file apps/alarm_clock/alarm_clock_panel.h
* @brief Alarm Clock main panel (standalone app).
*
* Root widget of the standalone Alarm Clock executable. Provides a live
* digital clock readout, a time editor (hour/minute spinboxes with +/- MD3
* buttons), a note field, an add/remove control row, and a list of armed
* alarms. Each armed alarm is checked against the wall clock once per second;
* when its time arrives the alarm fires and a Material message box shows the
* note. Ported from CCIMXDesktop's AlarmyClock — the original QPainter analog
* dial, QMainWindow shell, .ui files, and broadcaster/processor event bus are
* replaced by a single QuarkWidgets MD3 panel built in code.
*
* @author Charliechen114514 (chengh1922@mails.jlu.edu.cn)
* @date 2026-07-01
* @version 0.1
* @since 0.20
* @ingroup alarm_clock
*/

#pragma once

#include <QMetaType>
#include <QTime>
#include <QWidget>

class QListWidget;
class QListWidgetItem;
class QPaintEvent;
class QSpinBox;
class QTextEdit;
class QTimer;

namespace qw::widget::material {
class Button;
class Label;
} // namespace qw::widget::material

namespace cf::desktop::desktop_component {

/**
* @brief Represents one armed alarm (target time + user note).
*
* @ingroup alarm_clock
*/
struct AlarmEntry {
QTime time; ///< Target wall-clock time (hh:mm, seconds ignored).
QString note; ///< Reminder text shown when the alarm fires.
};

/**
* @brief Root widget of the standalone Alarm Clock application.
*
* @ingroup alarm_clock
*/
class AlarmClockPanel final : public QWidget {
Q_OBJECT
public:
/**
* @brief Constructs the Alarm Clock panel.
*
* @param[in] parent Parent widget (nullptr for a top-level window).
*
* @throws None
* @since 0.20
* @ingroup alarm_clock
*/
explicit AlarmClockPanel(QWidget* parent = nullptr);

/**
* @brief Destructs the panel.
*
* @throws None
* @since 0.20
* @ingroup alarm_clock
*/
~AlarmClockPanel() override;

protected:
/**
* @brief Paints the rounded Material card background.
*
* @param[in] event The paint event descriptor.
*
* @throws None
* @since 0.20
* @ingroup alarm_clock
*/
void paintEvent(QPaintEvent* event) override;

private slots:
/// @brief Refreshes the live clock label from the wall clock.
void onTick();
/// @brief Arms a new alarm from the editor fields.
void onAddAlarm();
/// @brief Removes the currently selected armed alarm.
void onRemoveAlarm();

private:
/**
* @brief Builds the editor + armed-alarms layout.
*
* @throws None
* @since 0.20
* @ingroup alarm_clock
*/
void setupUi();

/**
* @brief Fires the alarm identified by @p item.
*
* Shows a Material message box with the alarm note and, if the alarm is
* non-repeating, disarms it. Audible ringing is a TODO (see @ref onTick).
*
* @param[in] item The list item whose alarm time has arrived.
*
* @throws None
* @since 0.20
* @ingroup alarm_clock
*/
void fireAlarm(QListWidgetItem* item);

/// @brief Live digital clock readout.
qw::widget::material::Label* clock_label_{nullptr};
QSpinBox* hour_spin_{nullptr}; ///< Hour editor (0-23).
QSpinBox* minute_spin_{nullptr}; ///< Minute editor (0-59).
QTextEdit* note_edit_{nullptr}; ///< Reminder note editor.
QListWidget* alarm_list_{nullptr}; ///< Armed alarms list.
QTimer* ticker_{nullptr}; ///< Per-second wall-clock poll.
};

} // namespace cf::desktop::desktop_component

/// @brief Enables storing AlarmEntry in a QVariant (QListWidgetItem data).
Q_DECLARE_METATYPE(cf::desktop::desktop_component::AlarmEntry)
Loading
Loading