Add ringtones/wallpapers plugins, AT console, dialer USSD/*#06#, and SIM icon fix
This commit is contained in:
@@ -0,0 +1,858 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
// Bananarchy Ringtone & Ringer Manager Plugin (ringtones.so / xringtones)
|
||||
|
||||
#include "phone_data.h"
|
||||
#include "../sxmo-dwm/dwm_plugin.h"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <pango/pangoxft.h>
|
||||
#include <dirent.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <clocale>
|
||||
|
||||
#ifdef None
|
||||
#undef None
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace bananarchy;
|
||||
|
||||
volatile sig_atomic_t gRunning = 1;
|
||||
|
||||
void handleSignal(int) {
|
||||
gRunning = 0;
|
||||
}
|
||||
|
||||
enum class TabId {
|
||||
Ringtones = 0,
|
||||
Settings = 1,
|
||||
};
|
||||
|
||||
struct RingtoneEntry {
|
||||
std::string path;
|
||||
std::string displayName;
|
||||
std::string formatTag;
|
||||
std::string sourceTag;
|
||||
off_t sizeBytes = 0;
|
||||
};
|
||||
|
||||
struct RingSettings {
|
||||
int ringerMode = 0; // 0 = Ring + Vibrate, 1 = Ring Only, 2 = Vibrate Only, 3 = Silent
|
||||
int ringTimeSec = 20;
|
||||
int ringLoops = 10;
|
||||
int vibrateStrength = 1;
|
||||
};
|
||||
|
||||
constexpr int kRingTimeSteps[] = {10, 15, 20, 25, 30, 45, 60};
|
||||
constexpr int kNumRingTimeSteps =
|
||||
static_cast<int>(sizeof(kRingTimeSteps) / sizeof(kRingTimeSteps[0]));
|
||||
|
||||
constexpr int kRingLoopSteps[] = {3, 5, 10, 15, 20, 50};
|
||||
constexpr int kNumRingLoopSteps =
|
||||
static_cast<int>(sizeof(kRingLoopSteps) / sizeof(kRingLoopSteps[0]));
|
||||
|
||||
std::string getConfigDir() {
|
||||
const char* xdg = std::getenv("XDG_CONFIG_HOME");
|
||||
if (xdg && *xdg) {
|
||||
return std::string(xdg) + "/sxmo";
|
||||
}
|
||||
return getHomeDirPath() + "/.config/sxmo";
|
||||
}
|
||||
|
||||
bool isAudioExtension(const std::string& name, std::string* extUpperOut = nullptr) {
|
||||
size_t dot = name.rfind('.');
|
||||
if (dot == std::string::npos || dot + 1 >= name.size()) return false;
|
||||
std::string ext = toLowerStr(name.substr(dot + 1));
|
||||
if (ext == "wav" || ext == "ogg" || ext == "mp3" ||
|
||||
ext == "flac" || ext == "opus" || ext == "m4a") {
|
||||
if (extUpperOut) {
|
||||
*extUpperOut = ext;
|
||||
for (char& c : *extUpperOut) {
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string prettifyFilename(const std::string& filename) {
|
||||
size_t dot = filename.rfind('.');
|
||||
std::string stem = (dot == std::string::npos) ? filename : filename.substr(0, dot);
|
||||
for (char& c : stem) {
|
||||
if (c == '_' || c == '-') c = ' ';
|
||||
}
|
||||
return trimStr(stem);
|
||||
}
|
||||
|
||||
void scanDirectoryForRingtones(const std::string& dirPath,
|
||||
const std::string& sourceTag,
|
||||
std::vector<RingtoneEntry>& out) {
|
||||
DIR* d = ::opendir(dirPath.c_str());
|
||||
if (!d) return;
|
||||
std::vector<std::string> names;
|
||||
while (struct dirent* ent = ::readdir(d)) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
names.emplace_back(ent->d_name);
|
||||
}
|
||||
::closedir(d);
|
||||
std::sort(names.begin(), names.end());
|
||||
|
||||
for (const auto& name : names) {
|
||||
std::string extTag;
|
||||
if (!isAudioExtension(name, &extTag)) continue;
|
||||
std::string fullPath = dirPath + "/" + name;
|
||||
struct stat st {};
|
||||
if (::stat(fullPath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
||||
RingtoneEntry e;
|
||||
e.path = fullPath;
|
||||
e.displayName = prettifyFilename(name);
|
||||
e.formatTag = extTag;
|
||||
e.sourceTag = sourceTag;
|
||||
e.sizeBytes = st.st_size;
|
||||
out.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RingtoneEntry> discoverRingtones() {
|
||||
std::vector<RingtoneEntry> list;
|
||||
scanDirectoryForRingtones("/usr/share/sxmo/ringtones", "System", list);
|
||||
|
||||
struct stat st {};
|
||||
if (::stat("/usr/share/sxmo/ring.ogg", &st) == 0 && S_ISREG(st.st_mode)) {
|
||||
RingtoneEntry defEntry;
|
||||
defEntry.path = "/usr/share/sxmo/ring.ogg";
|
||||
defEntry.displayName = "Sxmo Default Ring";
|
||||
defEntry.formatTag = "OGG";
|
||||
defEntry.sourceTag = "System";
|
||||
defEntry.sizeBytes = st.st_size;
|
||||
list.push_back(defEntry);
|
||||
}
|
||||
|
||||
std::string home = getHomeDirPath();
|
||||
scanDirectoryForRingtones(home + "/Ringtones", "User", list);
|
||||
scanDirectoryForRingtones(home + "/Music/Ringtones", "User", list);
|
||||
scanDirectoryForRingtones(home + "/Music", "Music", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
std::string loadActiveRingtonePath() {
|
||||
std::string cfgPath = getConfigDir() + "/ringtone";
|
||||
std::ifstream ifs(cfgPath);
|
||||
if (ifs) {
|
||||
std::string line;
|
||||
if (std::getline(ifs, line)) {
|
||||
line = trimStr(line);
|
||||
struct stat st {};
|
||||
if (!line.empty() && ::stat(line.c_str(), &st) == 0) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char* envRing = std::getenv("SXMO_RINGTONE");
|
||||
if (envRing && *envRing) return envRing;
|
||||
return "/usr/share/sxmo/ring.ogg";
|
||||
}
|
||||
|
||||
bool saveActiveRingtonePath(const std::string& path) {
|
||||
std::string cfgDir = getConfigDir();
|
||||
::mkdir(cfgDir.c_str(), 0755);
|
||||
std::ofstream ofs(cfgDir + "/ringtone", std::ios::trunc);
|
||||
if (!ofs) return false;
|
||||
ofs << path << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
RingSettings loadRingSettings() {
|
||||
RingSettings s;
|
||||
std::string cfgDir = getConfigDir();
|
||||
bool noRing = (::access((cfgDir + "/.noring").c_str(), F_OK) == 0);
|
||||
bool noVib = (::access((cfgDir + "/.novibrate").c_str(), F_OK) == 0);
|
||||
if (!noRing && !noVib) s.ringerMode = 0;
|
||||
else if (!noRing && noVib) s.ringerMode = 1;
|
||||
else if (noRing && !noVib) s.ringerMode = 2;
|
||||
else s.ringerMode = 3;
|
||||
|
||||
std::ifstream ifs(cfgDir + "/ringsettings");
|
||||
std::string line;
|
||||
while (std::getline(ifs, line)) {
|
||||
line = trimStr(line);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
std::string k = trimStr(line.substr(0, eq));
|
||||
std::string v = trimStr(line.substr(eq + 1));
|
||||
if (v.size() >= 2 && ((v.front() == '"' && v.back() == '"') ||
|
||||
(v.front() == '\'' && v.back() == '\''))) {
|
||||
v = v.substr(1, v.size() - 2);
|
||||
}
|
||||
int val = std::atoi(v.c_str());
|
||||
if (k == "SXMO_RINGTIME" && val >= 5 && val <= 120) {
|
||||
s.ringTimeSec = val;
|
||||
} else if (k == "SXMO_RINGNUMBER" && val >= 1 && val <= 100) {
|
||||
s.ringLoops = val;
|
||||
} else if (k == "SXMO_VIBRATE_STRENGTH" && val >= 1 && val <= 3) {
|
||||
s.vibrateStrength = val;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void saveRingSettings(const RingSettings& s) {
|
||||
std::string cfgDir = getConfigDir();
|
||||
::mkdir(cfgDir.c_str(), 0755);
|
||||
|
||||
std::string noRingPath = cfgDir + "/.noring";
|
||||
std::string noVibPath = cfgDir + "/.novibrate";
|
||||
if (s.ringerMode == 0) {
|
||||
::unlink(noRingPath.c_str());
|
||||
::unlink(noVibPath.c_str());
|
||||
} else if (s.ringerMode == 1) {
|
||||
::unlink(noRingPath.c_str());
|
||||
writeIntFile(noVibPath, 1);
|
||||
} else if (s.ringerMode == 2) {
|
||||
writeIntFile(noRingPath, 1);
|
||||
::unlink(noVibPath.c_str());
|
||||
} else {
|
||||
writeIntFile(noRingPath, 1);
|
||||
writeIntFile(noVibPath, 1);
|
||||
}
|
||||
|
||||
std::ofstream ofs(cfgDir + "/ringsettings", std::ios::trunc);
|
||||
if (ofs) {
|
||||
ofs << "SXMO_RINGTIME=" << s.ringTimeSec << "\n";
|
||||
ofs << "SXMO_RINGNUMBER=" << s.ringLoops << "\n";
|
||||
ofs << "SXMO_VIBRATE_STRENGTH=" << s.vibrateStrength << "\n";
|
||||
}
|
||||
runAsyncCmd("sxmo_hook_statusbar.sh audio >/dev/null 2>&1");
|
||||
}
|
||||
|
||||
std::string formatFileSize(off_t bytes) {
|
||||
if (bytes < 1024) return std::to_string(bytes) + " B";
|
||||
return std::to_string((bytes + 512) / 1024) + " KB";
|
||||
}
|
||||
|
||||
int runRingtonesApp(int argc, char** argv) {
|
||||
gRunning = 1;
|
||||
std::setlocale(LC_ALL, "");
|
||||
XSetLocaleModifiers("");
|
||||
|
||||
bool isPreload = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i] && std::strcmp(argv[i], "--preload") == 0) {
|
||||
isPreload = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string daemonPidPath = getPluginDaemonPidPath("ringtones");
|
||||
std::string activePidPath = getRuntimeFile("xringtones.pid");
|
||||
|
||||
if (isPluginDaemonAlive("ringtones")) {
|
||||
if (!isPreload) {
|
||||
sendPluginCommand("ringtones", "OPEN");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
||||
int fifoFd = setupPluginDaemonFifo("ringtones");
|
||||
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = handleSignal;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
|
||||
Display* dpy = XOpenDisplay(nullptr);
|
||||
if (!dpy) {
|
||||
if (fifoFd >= 0) ::close(fifoFd);
|
||||
::unlink(daemonPidPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int screen = DefaultScreen(dpy);
|
||||
Window root = RootWindow(dpy, screen);
|
||||
Visual* visual = DefaultVisual(dpy, screen);
|
||||
Colormap cmap = DefaultColormap(dpy, screen);
|
||||
|
||||
ThemeSpec initSpec;
|
||||
initSpec.loadActive(dpy, screen);
|
||||
|
||||
XSetWindowAttributes swa {};
|
||||
swa.override_redirect = True;
|
||||
swa.background_pixel = initSpec.bgMain;
|
||||
swa.border_pixel = initSpec.selBg;
|
||||
swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask |
|
||||
VisibilityChangeMask | StructureNotifyMask;
|
||||
|
||||
Window win = XCreateWindow(
|
||||
dpy, root, kWinX, kWinY, kWinWidth, kWinHeight, 0,
|
||||
DefaultDepth(dpy, screen), CopyFromParent, visual,
|
||||
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa);
|
||||
|
||||
XClassHint ch {};
|
||||
char resName[] = "xringtones";
|
||||
char resClass[] = "XRingtones";
|
||||
ch.res_name = resName;
|
||||
ch.res_class = resClass;
|
||||
XSetClassHint(dpy, win, &ch);
|
||||
XStoreName(dpy, win, "Ringtones");
|
||||
|
||||
Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight,
|
||||
DefaultDepth(dpy, screen));
|
||||
GC gc = XCreateGC(dpy, pix, 0, nullptr);
|
||||
XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap);
|
||||
|
||||
PluginTheme pt;
|
||||
pt.init(dpy, screen, visual, cmap, win);
|
||||
auto& layoutMain = pt.layoutMain;
|
||||
auto& layoutSmall = pt.layoutSmall;
|
||||
auto& colWhite = pt.colWhite;
|
||||
auto& colLight = pt.colLight;
|
||||
auto& colMuted = pt.colMuted;
|
||||
auto& colYellow = pt.colYellow;
|
||||
auto& colGreen = pt.colGreen;
|
||||
auto& colBlue = pt.colBlue;
|
||||
|
||||
TabId activeTab = TabId::Ringtones;
|
||||
std::vector<RingtoneEntry> tones;
|
||||
std::string activePath;
|
||||
RingSettings settings;
|
||||
int selIdx = 0;
|
||||
int settingsRow = 0;
|
||||
pid_t previewPid = -1;
|
||||
std::string previewPath;
|
||||
|
||||
std::string statusBanner;
|
||||
int64_t statusBannerUntilMs = 0;
|
||||
|
||||
bool visible = false;
|
||||
int64_t startupIgnoreUntilMs = 0;
|
||||
bool initialSoftkeysReleased = true;
|
||||
int softkeyPending = 0;
|
||||
int64_t softkeyDeadlineMs = 0;
|
||||
|
||||
auto setBanner = [&](const std::string& msg, int durationMs = 2200) {
|
||||
statusBanner = msg;
|
||||
statusBannerUntilMs = nowMs() + durationMs;
|
||||
};
|
||||
|
||||
auto stopPreview = [&]() {
|
||||
if (previewPid > 0) {
|
||||
::kill(previewPid, SIGTERM);
|
||||
::waitpid(previewPid, nullptr, WNOHANG);
|
||||
previewPid = -1;
|
||||
}
|
||||
previewPath.clear();
|
||||
};
|
||||
|
||||
auto checkPreviewAlive = [&]() {
|
||||
if (previewPid > 0) {
|
||||
int st = 0;
|
||||
pid_t r = ::waitpid(previewPid, &st, WNOHANG);
|
||||
if (r == previewPid || (r < 0 && errno == ECHILD)) {
|
||||
previewPid = -1;
|
||||
previewPath.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
auto togglePreview = [&](const std::string& path) {
|
||||
checkPreviewAlive();
|
||||
if (previewPid > 0 && previewPath == path) {
|
||||
stopPreview();
|
||||
setBanner("Preview stopped");
|
||||
return;
|
||||
}
|
||||
stopPreview();
|
||||
pid_t pid = ::fork();
|
||||
if (pid == 0) {
|
||||
int devNull = ::open("/dev/null", O_RDWR);
|
||||
if (devNull >= 0) {
|
||||
::dup2(devNull, STDIN_FILENO);
|
||||
::dup2(devNull, STDOUT_FILENO);
|
||||
::dup2(devNull, STDERR_FILENO);
|
||||
if (devNull > 2) ::close(devNull);
|
||||
}
|
||||
::execlp("mpv", "mpv", "--no-resume-playback", "--quiet",
|
||||
"--no-video", "--loop=2", path.c_str(), nullptr);
|
||||
::_exit(127);
|
||||
} else if (pid > 0) {
|
||||
previewPid = pid;
|
||||
previewPath = path;
|
||||
setBanner("Playing preview...");
|
||||
}
|
||||
};
|
||||
|
||||
auto refreshData = [&]() {
|
||||
tones = discoverRingtones();
|
||||
activePath = loadActiveRingtonePath();
|
||||
settings = loadRingSettings();
|
||||
if (selIdx >= static_cast<int>(tones.size())) {
|
||||
selIdx = std::max(0, static_cast<int>(tones.size()) - 1);
|
||||
}
|
||||
};
|
||||
|
||||
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y,
|
||||
int h, const std::string& text, int maxW = -1,
|
||||
int align = 0) {
|
||||
if (text.empty()) return;
|
||||
pango_layout_set_text(layout, text.c_str(), -1);
|
||||
if (maxW > 0) {
|
||||
pango_layout_set_width(layout, maxW * PANGO_SCALE);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
|
||||
} else {
|
||||
pango_layout_set_width(layout, -1);
|
||||
}
|
||||
int tw = 0, th = 0;
|
||||
pango_layout_get_pixel_size(layout, &tw, &th);
|
||||
int tx = x;
|
||||
if (align == 1 && maxW > 0) tx = x + (maxW - tw) / 2;
|
||||
else if (align == 2 && maxW > 0) tx = x + maxW - tw;
|
||||
int ty = y + (h - th) / 2;
|
||||
pango_xft_render_layout(xftDraw, color, layout, tx * PANGO_SCALE,
|
||||
ty * PANGO_SCALE);
|
||||
};
|
||||
|
||||
auto drawSoftkeyBar = [&](const std::string& leftLabel,
|
||||
const std::string& midLabel,
|
||||
const std::string& rightLabel) {
|
||||
int barY = kWinHeight - 24;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, barY, kWinWidth, 24);
|
||||
XSetForeground(dpy, gc, pt.spec.border);
|
||||
XDrawLine(dpy, pix, gc, 0, barY, kWinWidth, barY);
|
||||
|
||||
drawPango(layoutSmall, &colLight, 6, barY, 24, leftLabel, 76, 0);
|
||||
drawPango(layoutMain, &colYellow, 82, barY, 24, midLabel, 76, 1);
|
||||
drawPango(layoutSmall, &colLight, 158, barY, 24, rightLabel, 76, 2);
|
||||
};
|
||||
|
||||
auto render = [&]() {
|
||||
if (!visible) return;
|
||||
checkPreviewAlive();
|
||||
|
||||
XSetForeground(dpy, gc, pt.spec.bgMain);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||||
|
||||
// Top tab header (0..26)
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, 26);
|
||||
|
||||
const char* tabNames[2] = {"1:Ringtones", "2:Settings"};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
int tx = i * 120;
|
||||
bool isAct = (static_cast<int>(activeTab) == i);
|
||||
if (isAct) {
|
||||
XSetForeground(dpy, gc, pt.spec.selBg);
|
||||
XFillRectangle(dpy, pix, gc, tx, 0, 120, 26);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, tx, 24, 120, 2);
|
||||
}
|
||||
drawPango(layoutSmall, isAct ? &colWhite : &colMuted, tx + 4, 0, 24,
|
||||
tabNames[i], 112, 1);
|
||||
}
|
||||
|
||||
if (activeTab == TabId::Ringtones) {
|
||||
// Active ringtone summary banner (y=28..50)
|
||||
XSetForeground(dpy, gc, pt.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 4, 28, kWinWidth - 8, 22);
|
||||
std::string actName = prettifyFilename(
|
||||
activePath.substr(activePath.rfind('/') + 1));
|
||||
drawPango(layoutSmall, &colGreen, 8, 28, 22,
|
||||
std::string(kIconBell) + " Active: " + actName,
|
||||
kWinWidth - 16);
|
||||
|
||||
// List area (y=52..246, 5 rows of 38px)
|
||||
constexpr int kVisibleRows = 5;
|
||||
int total = static_cast<int>(tones.size());
|
||||
int startIdx = 0;
|
||||
if (total > kVisibleRows) {
|
||||
startIdx = std::clamp(selIdx - kVisibleRows / 2, 0,
|
||||
total - kVisibleRows);
|
||||
}
|
||||
|
||||
for (int i = 0; i < kVisibleRows; ++i) {
|
||||
int idx = startIdx + i;
|
||||
if (idx >= total) break;
|
||||
const auto& e = tones[idx];
|
||||
int ry = 54 + i * 38;
|
||||
bool isSel = (idx == selIdx);
|
||||
bool isActive = (e.path == activePath);
|
||||
bool isPlaying = (previewPid > 0 && previewPath == e.path);
|
||||
|
||||
XSetForeground(dpy, gc, isSel ? pt.spec.selBg
|
||||
: (i % 2 == 0 ? pt.spec.bgAltEven
|
||||
: pt.spec.bgAltOdd));
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 36);
|
||||
if (isActive) {
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, 3, 36);
|
||||
}
|
||||
|
||||
drawPango(layoutMain, isSel ? &colWhite : &colLight, 10, ry + 1, 19,
|
||||
e.displayName, 152);
|
||||
|
||||
std::string badge;
|
||||
XftColor* badgeCol = &colMuted;
|
||||
if (isPlaying) {
|
||||
badge = "[PLAYING]";
|
||||
badgeCol = &colYellow;
|
||||
} else if (isActive) {
|
||||
badge = "[ACTIVE]";
|
||||
badgeCol = &colGreen;
|
||||
}
|
||||
if (!badge.empty()) {
|
||||
drawPango(layoutSmall, badgeCol, 162, ry + 2, 18, badge, 70, 2);
|
||||
}
|
||||
|
||||
std::string sub = e.formatTag + " | " + formatFileSize(e.sizeBytes) +
|
||||
" | " + e.sourceTag + " ([*] Preview)";
|
||||
drawPango(layoutSmall, isSel ? &colLight : &colMuted, 10, ry + 19, 16,
|
||||
sub, kWinWidth - 20);
|
||||
}
|
||||
|
||||
bool curPlaying = (!tones.empty() && selIdx >= 0 &&
|
||||
selIdx < static_cast<int>(tones.size()) &&
|
||||
previewPid > 0 &&
|
||||
previewPath == tones[selIdx].path);
|
||||
drawSoftkeyBar(curPlaying ? "Stop" : "Play [*]", "Set Ring", "Close");
|
||||
} else {
|
||||
// Tab 2: Settings
|
||||
const char* modeLabels[4] = {
|
||||
"Ring + Vibrate",
|
||||
"Ring Only",
|
||||
"Vibrate Only",
|
||||
"Silent",
|
||||
};
|
||||
std::string rows[5][2] = {
|
||||
{"Ringer Mode",
|
||||
"< " + std::string(modeLabels[settings.ringerMode % 4]) + " >"},
|
||||
{"Ring Duration",
|
||||
"< " + std::to_string(settings.ringTimeSec) + " sec >"},
|
||||
{"Ring Loop Count",
|
||||
"< " + std::to_string(settings.ringLoops) + " loops >"},
|
||||
{"Vibrate Strength",
|
||||
"< Level " + std::to_string(settings.vibrateStrength) + " >"},
|
||||
{"Test Vibration", "Press OK to vibrate"},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
int ry = 32 + i * 42;
|
||||
bool isSel = (settingsRow == i);
|
||||
XSetForeground(dpy, gc, isSel ? pt.spec.selBg : pt.spec.bgAltEven);
|
||||
XFillRectangle(dpy, pix, gc, 4, ry, kWinWidth - 8, 38);
|
||||
if (isSel) {
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 4, ry, kWinWidth - 9, 37);
|
||||
}
|
||||
drawPango(layoutSmall, isSel ? &colWhite : &colMuted, 10, ry + 2, 17,
|
||||
rows[i][0], kWinWidth - 20);
|
||||
drawPango(layoutMain, isSel ? &colYellow : &colBlue, 10, ry + 18, 19,
|
||||
rows[i][1], kWinWidth - 20);
|
||||
}
|
||||
drawSoftkeyBar("Change", "Select", "Close");
|
||||
}
|
||||
|
||||
if (!statusBanner.empty()) {
|
||||
int by = kWinHeight - 46;
|
||||
XSetForeground(dpy, gc, pt.spec.bgHeader);
|
||||
XFillRectangle(dpy, pix, gc, 6, by, kWinWidth - 12, 20);
|
||||
XSetForeground(dpy, gc, pt.spec.accent);
|
||||
XDrawRectangle(dpy, pix, gc, 6, by, kWinWidth - 13, 19);
|
||||
drawPango(layoutSmall, &colYellow, 10, by, 20, statusBanner,
|
||||
kWinWidth - 20, 1);
|
||||
}
|
||||
|
||||
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto showWindow = [&]() {
|
||||
closeOtherPlugins("ringtones");
|
||||
refreshData();
|
||||
for (size_t i = 0; i < tones.size(); ++i) {
|
||||
if (tones[i].path == activePath) {
|
||||
selIdx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
||||
visible = true;
|
||||
startupIgnoreUntilMs = nowMs() + 180;
|
||||
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||||
softkeyPending = 0;
|
||||
XMapRaised(dpy, win);
|
||||
XSetInputFocus(dpy, win, RevertToPointerRoot, CurrentTime);
|
||||
render();
|
||||
};
|
||||
|
||||
auto hideWindow = [&]() {
|
||||
stopPreview();
|
||||
if (!visible) return;
|
||||
visible = false;
|
||||
softkeyPending = 0;
|
||||
XUnmapWindow(dpy, win);
|
||||
::unlink(activePidPath.c_str());
|
||||
XFlush(dpy);
|
||||
};
|
||||
|
||||
auto adjustSettingRow = [&](int delta) {
|
||||
if (settingsRow == 0) {
|
||||
settings.ringerMode = (settings.ringerMode + 4 + delta) % 4;
|
||||
saveRingSettings(settings);
|
||||
setBanner("Ringer mode updated");
|
||||
} else if (settingsRow == 1) {
|
||||
int curIdx = 2;
|
||||
for (int i = 0; i < kNumRingTimeSteps; ++i) {
|
||||
if (kRingTimeSteps[i] == settings.ringTimeSec) {
|
||||
curIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curIdx = (curIdx + kNumRingTimeSteps + delta) % kNumRingTimeSteps;
|
||||
settings.ringTimeSec = kRingTimeSteps[curIdx];
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 2) {
|
||||
int curIdx = 2;
|
||||
for (int i = 0; i < kNumRingLoopSteps; ++i) {
|
||||
if (kRingLoopSteps[i] == settings.ringLoops) {
|
||||
curIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
curIdx = (curIdx + kNumRingLoopSteps + delta) % kNumRingLoopSteps;
|
||||
settings.ringLoops = kRingLoopSteps[curIdx];
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 3) {
|
||||
settings.vibrateStrength =
|
||||
((settings.vibrateStrength - 1 + 3 + delta) % 3) + 1;
|
||||
saveRingSettings(settings);
|
||||
} else if (settingsRow == 4) {
|
||||
runAsyncCmd("sxmo_vibrate 600 " +
|
||||
std::to_string(settings.vibrateStrength) +
|
||||
" >/dev/null 2>&1");
|
||||
setBanner("Testing vibration...");
|
||||
}
|
||||
};
|
||||
|
||||
if (!isPreload) {
|
||||
showWindow();
|
||||
}
|
||||
|
||||
int xfd = ConnectionNumber(dpy);
|
||||
while (gRunning) {
|
||||
while (XPending(dpy) > 0) {
|
||||
XEvent ev;
|
||||
XNextEvent(dpy, &ev);
|
||||
if (ev.type == Expose && ev.xexpose.count == 0) {
|
||||
render();
|
||||
} else if (ev.type == VisibilityNotify && visible &&
|
||||
ev.xvisibility.state != VisibilityUnobscured) {
|
||||
XRaiseWindow(dpy, win);
|
||||
} else if (ev.type == KeyRelease) {
|
||||
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
|
||||
initialSoftkeysReleased = true;
|
||||
}
|
||||
} else if (ev.type == KeyPress && visible) {
|
||||
int64_t now = nowMs();
|
||||
if (now < startupIgnoreUntilMs) continue;
|
||||
|
||||
KeySym sym = XLookupKeysym(&ev.xkey, 0);
|
||||
unsigned int kc = ev.xkey.keycode;
|
||||
|
||||
bool isLSK = (kc == 135 || sym == XK_F1 || sym == XK_Menu ||
|
||||
sym == XF86XK_TaskPane);
|
||||
bool isRSK = (kc == 166 || sym == XK_F2 || sym == XF86XK_Back ||
|
||||
sym == XK_Escape ||
|
||||
sym == static_cast<KeySym>(XF86XK_KbdLcdMenu2));
|
||||
|
||||
if (isLSK || isRSK) {
|
||||
if (!initialSoftkeysReleased) continue;
|
||||
int mask = querySoftkeysDown(dpy);
|
||||
if (mask == 3 || (softkeyPending != 0 &&
|
||||
((isLSK && softkeyPending == 2) ||
|
||||
(isRSK && softkeyPending == 1)))) {
|
||||
softkeyPending = 0;
|
||||
writeSoftkeySuppress(now + 400);
|
||||
hideWindow();
|
||||
sendPluginCommand("xdrawer", "TOGGLE");
|
||||
continue;
|
||||
}
|
||||
softkeyPending = isLSK ? 1 : 2;
|
||||
softkeyDeadlineMs = now + 90;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sym == XF86XK_HangupPhone || sym == XK_Cancel) {
|
||||
hideWindow();
|
||||
continue;
|
||||
}
|
||||
|
||||
char dch = mapDialKey(sym, kc, ev.xkey.state);
|
||||
if (dch == '1') {
|
||||
activeTab = TabId::Ringtones;
|
||||
render();
|
||||
continue;
|
||||
} else if (dch == '2') {
|
||||
activeTab = TabId::Settings;
|
||||
render();
|
||||
continue;
|
||||
} else if (dch == '*' && activeTab == TabId::Ringtones &&
|
||||
!tones.empty()) {
|
||||
togglePreview(tones[selIdx].path);
|
||||
render();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sym == XK_Left || sym == XK_KP_Left) {
|
||||
if (activeTab == TabId::Settings && settingsRow < 4) {
|
||||
adjustSettingRow(-1);
|
||||
} else {
|
||||
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
||||
: TabId::Ringtones;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Right || sym == XK_KP_Right) {
|
||||
if (activeTab == TabId::Settings && settingsRow < 4) {
|
||||
adjustSettingRow(1);
|
||||
} else {
|
||||
activeTab = (activeTab == TabId::Ringtones) ? TabId::Settings
|
||||
: TabId::Ringtones;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Up || sym == XK_KP_Up) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
selIdx = (selIdx + static_cast<int>(tones.size()) - 1) %
|
||||
static_cast<int>(tones.size());
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
settingsRow = (settingsRow + 4) % 5;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Down || sym == XK_KP_Down) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
selIdx = (selIdx + 1) % static_cast<int>(tones.size());
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
settingsRow = (settingsRow + 1) % 5;
|
||||
}
|
||||
render();
|
||||
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
activePath = tones[selIdx].path;
|
||||
saveActiveRingtonePath(activePath);
|
||||
setBanner("Ringtone set: " + tones[selIdx].displayName);
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
adjustSettingRow(1);
|
||||
}
|
||||
render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int timeoutMs = -1;
|
||||
int64_t now = nowMs();
|
||||
if (visible) {
|
||||
if (checkPreviewAlive()) {
|
||||
render();
|
||||
}
|
||||
if (softkeyPending != 0) {
|
||||
int64_t rem = softkeyDeadlineMs - now;
|
||||
if (rem <= 0) {
|
||||
int pend = softkeyPending;
|
||||
softkeyPending = 0;
|
||||
if (querySoftkeysDown(dpy) == 3) {
|
||||
writeSoftkeySuppress(now + 400);
|
||||
hideWindow();
|
||||
sendPluginCommand("xdrawer", "TOGGLE");
|
||||
} else if (pend == 1) {
|
||||
if (activeTab == TabId::Ringtones && !tones.empty()) {
|
||||
togglePreview(tones[selIdx].path);
|
||||
} else if (activeTab == TabId::Settings) {
|
||||
adjustSettingRow(1);
|
||||
}
|
||||
render();
|
||||
} else if (pend == 2) {
|
||||
hideWindow();
|
||||
}
|
||||
} else {
|
||||
timeoutMs = static_cast<int>(rem);
|
||||
}
|
||||
}
|
||||
if (!statusBanner.empty()) {
|
||||
int64_t bRem = statusBannerUntilMs - now;
|
||||
if (bRem <= 0) {
|
||||
statusBanner.clear();
|
||||
render();
|
||||
} else if (timeoutMs < 0 || bRem < timeoutMs) {
|
||||
timeoutMs = static_cast<int>(bRem);
|
||||
}
|
||||
}
|
||||
if (previewPid > 0 && (timeoutMs < 0 || timeoutMs > 400)) {
|
||||
timeoutMs = 400;
|
||||
}
|
||||
}
|
||||
|
||||
struct pollfd pfds[2];
|
||||
pfds[0].fd = xfd;
|
||||
pfds[0].events = POLLIN;
|
||||
pfds[1].fd = fifoFd;
|
||||
pfds[1].events = POLLIN;
|
||||
|
||||
int pr = ::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs);
|
||||
if (pr > 0 && fifoFd >= 0 && (pfds[1].revents & POLLIN)) {
|
||||
for (const auto& cmd : readFifoLines(fifoFd)) {
|
||||
if (cmd == "CLOSE") {
|
||||
hideWindow();
|
||||
} else if (cmd == "RELOAD_THEME") {
|
||||
pt.reload();
|
||||
if (visible) render();
|
||||
} else if (cmd == "TOGGLE") {
|
||||
if (visible) hideWindow();
|
||||
else showWindow();
|
||||
} else if (cmd.rfind("OPEN", 0) == 0) {
|
||||
showWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
if (fifoFd >= 0) ::close(fifoFd);
|
||||
::unlink(daemonPidPath.c_str());
|
||||
if (xftDraw) XftDrawDestroy(xftDraw);
|
||||
XFreeGC(dpy, gc);
|
||||
XFreePixmap(dpy, pix);
|
||||
XDestroyWindow(dpy, win);
|
||||
XCloseDisplay(dpy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pluginInit(const DwmHostApi* host) {
|
||||
if (host && host->spawn_plugin) {
|
||||
host->spawn_plugin("ringtones", "--preload");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pluginCleanup(void) {}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||||
DWM_PLUGIN_API_VERSION,
|
||||
"ringtones",
|
||||
"Bananarchy Ringtones & Ringer Manager (240x320)",
|
||||
pluginInit,
|
||||
pluginCleanup,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
runRingtonesApp
|
||||
};
|
||||
Reference in New Issue
Block a user