1506 lines
49 KiB
C++
1506 lines
49 KiB
C++
#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 <poll.h>
|
||
|
|
#include <clocale>
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
using namespace bananarchy;
|
||
|
|
|
||
|
|
volatile sig_atomic_t gRunning = 1;
|
||
|
|
|
||
|
|
void handleSignal(int) {
|
||
|
|
gRunning = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
enum class ViewMode {
|
||
|
|
List = 0,
|
||
|
|
Detail = 1,
|
||
|
|
Options = 2,
|
||
|
|
Edit = 3,
|
||
|
|
PickRecent = 4,
|
||
|
|
ConfirmDelete = 5,
|
||
|
|
};
|
||
|
|
|
||
|
|
enum class TextCaseMode {
|
||
|
|
Title = 0, // Abc
|
||
|
|
Upper = 1, // ABC
|
||
|
|
Lower = 2, // abc
|
||
|
|
Num = 3, // 123
|
||
|
|
};
|
||
|
|
|
||
|
|
const char* kMultiTapLower[10] = {
|
||
|
|
" 0+", // 0
|
||
|
|
".,-@_1", // 1
|
||
|
|
"abc2", // 2
|
||
|
|
"def3", // 3
|
||
|
|
"ghi4", // 4
|
||
|
|
"jkl5", // 5
|
||
|
|
"mno6", // 6
|
||
|
|
"pqrs7", // 7
|
||
|
|
"tuv8", // 8
|
||
|
|
"wxyz9" // 9
|
||
|
|
};
|
||
|
|
|
||
|
|
int runContactsApp(int argc, char** argv) {
|
||
|
|
gRunning = 1;
|
||
|
|
std::setlocale(LC_ALL, "");
|
||
|
|
XSetLocaleModifiers("");
|
||
|
|
|
||
|
|
bool isPreload = false;
|
||
|
|
std::string initArg;
|
||
|
|
for (int i = 1; i < argc; ++i) {
|
||
|
|
if (!argv[i]) continue;
|
||
|
|
if (std::strcmp(argv[i], "--preload") == 0) {
|
||
|
|
isPreload = true;
|
||
|
|
} else if (std::strcmp(argv[i], "newcontact") == 0) {
|
||
|
|
if (i + 1 < argc && argv[i + 1]) {
|
||
|
|
initArg = std::string("new:") + argv[++i];
|
||
|
|
} else {
|
||
|
|
initArg = "new:";
|
||
|
|
}
|
||
|
|
} else if (initArg.empty()) {
|
||
|
|
initArg = argv[i];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string daemonPidPath = getPluginDaemonPidPath("contacts");
|
||
|
|
std::string activePidPath = getRuntimeFile("xcontacts.pid");
|
||
|
|
|
||
|
|
int existingDaemon = readIntFile(daemonPidPath, -1);
|
||
|
|
if (existingDaemon > 0 && existingDaemon != static_cast<int>(::getpid()) &&
|
||
|
|
::kill(existingDaemon, 0) == 0) {
|
||
|
|
if (!isPreload) {
|
||
|
|
sendPluginCommand("contacts",
|
||
|
|
initArg.empty() ? "OPEN" : ("OPEN " + initArg));
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
writeIntFile(daemonPidPath, static_cast<int>(::getpid()));
|
||
|
|
int fifoFd = setupPluginDaemonFifo("contacts");
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
XSetWindowAttributes swa{};
|
||
|
|
swa.override_redirect = True;
|
||
|
|
swa.background_pixel = 0x222222;
|
||
|
|
swa.border_pixel = 0x005577;
|
||
|
|
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[] = "xcontacts";
|
||
|
|
char resClass[] = "XContacts";
|
||
|
|
ch.res_name = resName;
|
||
|
|
ch.res_class = resClass;
|
||
|
|
XSetClassHint(dpy, win, &ch);
|
||
|
|
XStoreName(dpy, win, "Contacts");
|
||
|
|
|
||
|
|
Pixmap pix = XCreatePixmap(dpy, win, kWinWidth, kWinHeight,
|
||
|
|
DefaultDepth(dpy, screen));
|
||
|
|
GC gc = XCreateGC(dpy, pix, 0, nullptr);
|
||
|
|
XftDraw* xftDraw = XftDrawCreate(dpy, pix, visual, cmap);
|
||
|
|
|
||
|
|
PangoFontMap* fontMap = pango_xft_get_font_map(dpy, screen);
|
||
|
|
PangoContext* pangoCtx = pango_font_map_create_context(fontMap);
|
||
|
|
|
||
|
|
PangoLayout* layoutMain = pango_layout_new(pangoCtx);
|
||
|
|
PangoFontDescription* descMain =
|
||
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 15px");
|
||
|
|
pango_layout_set_font_description(layoutMain, descMain);
|
||
|
|
|
||
|
|
PangoLayout* layoutSmall = pango_layout_new(pangoCtx);
|
||
|
|
PangoFontDescription* descSmall =
|
||
|
|
pango_font_description_from_string("Fira Code, DejaVuSansM Nerd Font 12px");
|
||
|
|
pango_layout_set_font_description(layoutSmall, descSmall);
|
||
|
|
|
||
|
|
PangoLayout* layoutIcon = pango_layout_new(pangoCtx);
|
||
|
|
PangoFontDescription* descIcon =
|
||
|
|
pango_font_description_from_string("DejaVuSansM Nerd Font, Fira Code 16px");
|
||
|
|
pango_layout_set_font_description(layoutIcon, descIcon);
|
||
|
|
|
||
|
|
auto allocColor = [&](const char* hex) {
|
||
|
|
XftColor col{};
|
||
|
|
XftColorAllocName(dpy, visual, cmap, hex, &col);
|
||
|
|
return col;
|
||
|
|
};
|
||
|
|
|
||
|
|
XftColor colWhite = allocColor("#ffffff");
|
||
|
|
XftColor colLight = allocColor("#dddddd");
|
||
|
|
XftColor colMuted = allocColor("#999999");
|
||
|
|
XftColor colYellow = allocColor("#f4d03f");
|
||
|
|
XftColor colGreen = allocColor("#2ecc71");
|
||
|
|
XftColor colBlue = allocColor("#5dade2");
|
||
|
|
XftColor colRed = allocColor("#e74c3c");
|
||
|
|
|
||
|
|
ContactBook book;
|
||
|
|
std::vector<ContactEntry> filtered;
|
||
|
|
std::vector<std::string> recentUnknowns;
|
||
|
|
FileStamp contactsStamp;
|
||
|
|
FileStamp logStamp;
|
||
|
|
bool contactsLoadedOnce = false;
|
||
|
|
|
||
|
|
ViewMode mode = ViewMode::List;
|
||
|
|
ViewMode prevModeBeforeEdit = ViewMode::List;
|
||
|
|
std::string searchQuery;
|
||
|
|
int selected = 0;
|
||
|
|
int detailSelected = 0;
|
||
|
|
int optionSelected = 0;
|
||
|
|
int confirmSelected = 0;
|
||
|
|
int recentSelected = 0;
|
||
|
|
|
||
|
|
// Editor state
|
||
|
|
bool editIsNew = true;
|
||
|
|
std::string editOrigName;
|
||
|
|
std::string editOrigNumber;
|
||
|
|
std::string editName;
|
||
|
|
std::string editNumber;
|
||
|
|
int editField = 0; // 0 = Name, 1 = Number, 2 = From Recent, 3 = Save
|
||
|
|
TextCaseMode caseMode = TextCaseMode::Title;
|
||
|
|
int lastTapDigit = -1;
|
||
|
|
int lastTapCycle = 0;
|
||
|
|
int64_t lastTapMs = 0;
|
||
|
|
bool zeroHeld = false;
|
||
|
|
int64_t zeroPressMs = 0;
|
||
|
|
|
||
|
|
bool visible = false;
|
||
|
|
int64_t openStartMs = 0;
|
||
|
|
int64_t startupIgnoreUntilMs = 0;
|
||
|
|
bool initialSoftkeysReleased = true;
|
||
|
|
int softkeyPending = 0;
|
||
|
|
int64_t softkeyDeadlineMs = 0;
|
||
|
|
|
||
|
|
auto updateFilter = [&]() {
|
||
|
|
filtered.clear();
|
||
|
|
const auto& all = book.savedContacts();
|
||
|
|
if (searchQuery.empty()) {
|
||
|
|
filtered = all;
|
||
|
|
} else {
|
||
|
|
std::string qNorm = normalizePhone(searchQuery);
|
||
|
|
std::string qLower = toLowerStr(searchQuery);
|
||
|
|
std::vector<ContactEntry> pfxMatches;
|
||
|
|
std::vector<ContactEntry> subMatches;
|
||
|
|
for (const auto& c : all) {
|
||
|
|
bool pfx = (!qLower.empty() && c.lowerName.rfind(qLower, 0) == 0) ||
|
||
|
|
(!qNorm.empty() && c.t9Name.rfind(qNorm, 0) == 0) ||
|
||
|
|
(!qNorm.empty() && c.normNumber.rfind(qNorm, 0) == 0);
|
||
|
|
bool sub = pfx ||
|
||
|
|
(!qLower.empty() && c.lowerName.find(qLower) != std::string::npos) ||
|
||
|
|
(!qNorm.empty() && c.t9Name.find(qNorm) != std::string::npos) ||
|
||
|
|
(!qNorm.empty() && c.normNumber.find(qNorm) != std::string::npos);
|
||
|
|
if (pfx) {
|
||
|
|
pfxMatches.push_back(c);
|
||
|
|
} else if (sub) {
|
||
|
|
subMatches.push_back(c);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
filtered.insert(filtered.end(), pfxMatches.begin(), pfxMatches.end());
|
||
|
|
filtered.insert(filtered.end(), subMatches.begin(), subMatches.end());
|
||
|
|
}
|
||
|
|
if (selected >= static_cast<int>(filtered.size())) {
|
||
|
|
selected = std::max(0, static_cast<int>(filtered.size()) - 1);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto reloadContactsIfNeeded = [&]() -> bool {
|
||
|
|
bool cChanged = contactsStamp.checkAndUpdate(getContactsFilePath());
|
||
|
|
bool lChanged = logStamp.checkAndUpdate(getModemLogFilePath());
|
||
|
|
if (cChanged || lChanged || !contactsLoadedOnce) {
|
||
|
|
contactsLoadedOnce = true;
|
||
|
|
book.load();
|
||
|
|
updateFilter();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
};
|
||
|
|
|
||
|
|
reloadContactsIfNeeded();
|
||
|
|
|
||
|
|
auto drawPango = [&](PangoLayout* layout, XftColor* color, int x, int y, int h,
|
||
|
|
const std::string& text, int maxW = -1) {
|
||
|
|
pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.size()));
|
||
|
|
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);
|
||
|
|
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
|
||
|
|
}
|
||
|
|
PangoRectangle ink{}, logical{};
|
||
|
|
pango_layout_get_pixel_extents(layout, &ink, &logical);
|
||
|
|
int ty = y + (h - logical.height) / 2;
|
||
|
|
pango_xft_render_layout(xftDraw, color, layout, x * PANGO_SCALE,
|
||
|
|
ty * PANGO_SCALE);
|
||
|
|
};
|
||
|
|
|
||
|
|
auto pangoWidth = [&](PangoLayout* layout, const std::string& text) -> int {
|
||
|
|
pango_layout_set_width(layout, -1);
|
||
|
|
pango_layout_set_text(layout, text.c_str(), static_cast<int>(text.size()));
|
||
|
|
PangoRectangle ink{}, logical{};
|
||
|
|
pango_layout_get_pixel_extents(layout, &ink, &logical);
|
||
|
|
return logical.width;
|
||
|
|
};
|
||
|
|
|
||
|
|
constexpr int kHeaderH = 38;
|
||
|
|
constexpr int kSoftbarH = 26;
|
||
|
|
constexpr int kListY = kHeaderH;
|
||
|
|
constexpr int kListH = kWinHeight - kHeaderH - kSoftbarH;
|
||
|
|
constexpr int kVisibleRows = 5;
|
||
|
|
|
||
|
|
auto drawSoftbar = [&](const std::string& leftLabel,
|
||
|
|
const std::string& centerLabel,
|
||
|
|
const std::string& rightLabel,
|
||
|
|
XftColor* centerCol = nullptr) {
|
||
|
|
int sbY = kWinHeight - kSoftbarH;
|
||
|
|
XSetForeground(dpy, gc, 0x181818);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, kSoftbarH);
|
||
|
|
XSetForeground(dpy, gc, 0x444444);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, sbY, kWinWidth, 1);
|
||
|
|
|
||
|
|
if (!leftLabel.empty()) {
|
||
|
|
drawPango(layoutMain, &colLight, 6, sbY + 1, kSoftbarH - 1, leftLabel);
|
||
|
|
}
|
||
|
|
if (!centerLabel.empty()) {
|
||
|
|
int cw = pangoWidth(layoutMain, centerLabel);
|
||
|
|
drawPango(layoutMain, centerCol ? centerCol : &colGreen,
|
||
|
|
(kWinWidth - cw) / 2, sbY + 1, kSoftbarH - 1, centerLabel);
|
||
|
|
}
|
||
|
|
if (!rightLabel.empty()) {
|
||
|
|
int rw = pangoWidth(layoutMain, rightLabel);
|
||
|
|
drawPango(layoutMain, &colLight, kWinWidth - rw - 6, sbY + 1,
|
||
|
|
kSoftbarH - 1, rightLabel);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto render = [&]() {
|
||
|
|
XSetForeground(dpy, gc, 0x222222);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kWinHeight);
|
||
|
|
|
||
|
|
if (mode == ViewMode::List || mode == ViewMode::Options) {
|
||
|
|
// Header bar with live search / count
|
||
|
|
XSetForeground(dpy, gc, 0x1c1c1c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
|
||
|
|
XSetForeground(dpy, gc, 0x444444);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
|
||
|
|
|
||
|
|
if (searchQuery.empty()) {
|
||
|
|
drawPango(layoutIcon, &colBlue, 8, 0, kHeaderH, kIconBook);
|
||
|
|
std::string title =
|
||
|
|
"Contacts (" + std::to_string(book.savedContacts().size()) + ")";
|
||
|
|
drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, title, 200);
|
||
|
|
} else {
|
||
|
|
drawPango(layoutIcon, &colYellow, 8, 0, kHeaderH, kIconSearch);
|
||
|
|
drawPango(layoutMain, &colYellow, 32, 0, kHeaderH,
|
||
|
|
"Find: " + searchQuery, 178);
|
||
|
|
drawPango(layoutIcon, &colMuted, 216, 0, kHeaderH, kIconBackspace);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (filtered.empty()) {
|
||
|
|
drawPango(layoutIcon, &colMuted, 112, kListY + 46, 24, kIconUser);
|
||
|
|
drawPango(layoutMain, &colMuted, 32, kListY + 76, 26,
|
||
|
|
searchQuery.empty() ? "Phonebook is empty"
|
||
|
|
: "No matching contacts",
|
||
|
|
190);
|
||
|
|
drawPango(layoutSmall, &colMuted, 20, kListY + 106, 22,
|
||
|
|
"Press Options or Enter to add", 204);
|
||
|
|
} else {
|
||
|
|
int total = static_cast<int>(filtered.size());
|
||
|
|
int scrollOffset = 0;
|
||
|
|
if (selected >= kVisibleRows) {
|
||
|
|
scrollOffset = selected - kVisibleRows + 1;
|
||
|
|
}
|
||
|
|
if (scrollOffset > std::max(0, total - kVisibleRows)) {
|
||
|
|
scrollOffset = std::max(0, total - kVisibleRows);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int i = 0; i < kVisibleRows; ++i) {
|
||
|
|
int idx = scrollOffset + i;
|
||
|
|
if (idx >= total) break;
|
||
|
|
int ry = kListY + (i * kListH) / kVisibleRows;
|
||
|
|
int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows;
|
||
|
|
int rowH = nextRy - ry;
|
||
|
|
bool isSel = (idx == selected);
|
||
|
|
|
||
|
|
XSetForeground(dpy, gc,
|
||
|
|
isSel ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e));
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH);
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
|
||
|
|
}
|
||
|
|
|
||
|
|
const ContactEntry& c = filtered[idx];
|
||
|
|
drawPango(layoutIcon, isSel ? &colYellow : &colBlue, 8, ry, rowH,
|
||
|
|
kIconUser);
|
||
|
|
drawPango(layoutMain, isSel ? &colWhite : &colLight, 30, ry + 3, 22,
|
||
|
|
c.name, 202);
|
||
|
|
drawPango(layoutSmall, isSel ? &colYellow : &colMuted, 30, ry + 24, 19,
|
||
|
|
c.number, 202);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (total > kVisibleRows) {
|
||
|
|
int sbH = std::max(16, (kListH * kVisibleRows) / total);
|
||
|
|
int sbY = kListY +
|
||
|
|
((kListH - sbH) * scrollOffset) / (total - kVisibleRows);
|
||
|
|
XSetForeground(dpy, gc, 0x666666);
|
||
|
|
XFillRectangle(dpy, pix, gc, kWinWidth - 3, sbY, 3, sbH);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mode == ViewMode::Options) {
|
||
|
|
// Popup options menu overlay
|
||
|
|
const char* optLabels[6] = {
|
||
|
|
"New Contact",
|
||
|
|
"Add from Recent",
|
||
|
|
"Call Contact",
|
||
|
|
"Send SMS",
|
||
|
|
"Edit Contact",
|
||
|
|
"Delete Contact"
|
||
|
|
};
|
||
|
|
const char* optIcons[6] = {
|
||
|
|
kIconPlus,
|
||
|
|
kIconClock,
|
||
|
|
kIconPhone,
|
||
|
|
kIconSms,
|
||
|
|
kIconEdit,
|
||
|
|
kIconTrash
|
||
|
|
};
|
||
|
|
int optCount = filtered.empty() ? 2 : 6;
|
||
|
|
int menuRowH = 34;
|
||
|
|
int menuH = optCount * menuRowH + 4;
|
||
|
|
int menuY = kWinHeight - kSoftbarH - menuH;
|
||
|
|
XSetForeground(dpy, gc, 0x181818);
|
||
|
|
XFillRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 16, menuH);
|
||
|
|
XSetForeground(dpy, gc, 0x005577);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 8, menuY, kWinWidth - 17, menuH - 1);
|
||
|
|
|
||
|
|
for (int i = 0; i < optCount; ++i) {
|
||
|
|
int ry = menuY + 2 + i * menuRowH;
|
||
|
|
bool isSel = (i == optionSelected);
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, 0x005577);
|
||
|
|
XFillRectangle(dpy, pix, gc, 10, ry, kWinWidth - 20, menuRowH);
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 10, ry, 4, menuRowH);
|
||
|
|
}
|
||
|
|
XftColor* icCol = (i == 5) ? &colRed : (isSel ? &colYellow : &colGreen);
|
||
|
|
drawPango(layoutIcon, icCol, 18, ry, menuRowH, optIcons[i]);
|
||
|
|
drawPango(layoutMain, isSel ? &colWhite : &colLight, 42, ry, menuRowH,
|
||
|
|
optLabels[i], 180);
|
||
|
|
}
|
||
|
|
drawSoftbar("Select", "OK", "Back", &colYellow);
|
||
|
|
} else {
|
||
|
|
drawSoftbar("Options", filtered.empty() ? "New" : "Open",
|
||
|
|
searchQuery.empty() ? "Close" : "Clear", &colGreen);
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Detail) {
|
||
|
|
ContactEntry c;
|
||
|
|
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
|
||
|
|
c = filtered[selected];
|
||
|
|
}
|
||
|
|
constexpr int kCardH = 56;
|
||
|
|
XSetForeground(dpy, gc, 0x1b3b4b);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kCardH);
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, 4, kCardH);
|
||
|
|
XSetForeground(dpy, gc, 0x005577);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, kCardH - 2, kWinWidth, 2);
|
||
|
|
|
||
|
|
drawPango(layoutIcon, &colYellow, 10, 6, 24, kIconUser);
|
||
|
|
drawPango(layoutMain, &colWhite, 34, 4, 26, c.name, 198);
|
||
|
|
drawPango(layoutSmall, &colYellow, 34, 30, 20, c.number, 198);
|
||
|
|
|
||
|
|
const char* actLabels[6] = {
|
||
|
|
"Call Contact",
|
||
|
|
"Send SMS",
|
||
|
|
"Read Messages",
|
||
|
|
"Open in Dialer",
|
||
|
|
"Edit Contact",
|
||
|
|
"Delete Contact"
|
||
|
|
};
|
||
|
|
const char* actIcons[6] = {
|
||
|
|
kIconPhone,
|
||
|
|
kIconSms,
|
||
|
|
kIconList,
|
||
|
|
kIconPhoneLog,
|
||
|
|
kIconEdit,
|
||
|
|
kIconTrash
|
||
|
|
};
|
||
|
|
constexpr int kActCount = 6;
|
||
|
|
constexpr int kActVis = 5;
|
||
|
|
int actListY = kCardH;
|
||
|
|
int actListH = kWinHeight - kCardH - kSoftbarH;
|
||
|
|
int scrollOffset = 0;
|
||
|
|
if (detailSelected >= kActVis) {
|
||
|
|
scrollOffset = detailSelected - kActVis + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int i = 0; i < kActVis; ++i) {
|
||
|
|
int idx = scrollOffset + i;
|
||
|
|
if (idx >= kActCount) break;
|
||
|
|
int ry = actListY + (i * actListH) / kActVis;
|
||
|
|
int nextRy = actListY + ((i + 1) * actListH) / kActVis;
|
||
|
|
int rowH = nextRy - ry;
|
||
|
|
bool isSel = (idx == detailSelected);
|
||
|
|
|
||
|
|
XSetForeground(dpy, gc,
|
||
|
|
isSel ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e));
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH);
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
|
||
|
|
}
|
||
|
|
XftColor* icCol =
|
||
|
|
(idx == 5) ? &colRed : (isSel ? &colYellow : &colGreen);
|
||
|
|
drawPango(layoutIcon, icCol, 10, ry, rowH, actIcons[idx]);
|
||
|
|
drawPango(layoutMain,
|
||
|
|
(idx == 5 && !isSel) ? &colRed : (isSel ? &colWhite : &colLight),
|
||
|
|
34, ry, rowH, actLabels[idx], 196);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawSoftbar("Edit", "Select", "Back", &colGreen);
|
||
|
|
} else if (mode == ViewMode::Edit) {
|
||
|
|
XSetForeground(dpy, gc, 0x1c1c1c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
|
||
|
|
XSetForeground(dpy, gc, 0x444444);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
|
||
|
|
|
||
|
|
drawPango(layoutIcon, &colGreen, 8, 0, kHeaderH,
|
||
|
|
editIsNew ? kIconPlus : kIconEdit);
|
||
|
|
drawPango(layoutMain, &colWhite, 30, 0, kHeaderH,
|
||
|
|
editIsNew ? "New Contact" : "Edit Contact", 145);
|
||
|
|
|
||
|
|
const char* modeNames[4] = {"[Abc]", "[ABC]", "[abc]", "[123]"};
|
||
|
|
std::string modeBadge =
|
||
|
|
(editField == 0) ? modeNames[static_cast<int>(caseMode)] : "[+0-9]";
|
||
|
|
int mw = pangoWidth(layoutSmall, modeBadge);
|
||
|
|
drawPango(layoutSmall, &colYellow, kWinWidth - mw - 6, 0, kHeaderH,
|
||
|
|
modeBadge);
|
||
|
|
|
||
|
|
// Field 0: Name box (y = 44..108)
|
||
|
|
auto drawFieldBox = [&](int y, int h, bool active, const char* label,
|
||
|
|
const std::string& val, const char* placeholder) {
|
||
|
|
XSetForeground(dpy, gc, active ? 0x1b3b4b : 0x2a2a2a);
|
||
|
|
XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h);
|
||
|
|
XSetForeground(dpy, gc, active ? 0xf4d03f : 0x444444);
|
||
|
|
XDrawRectangle(dpy, pix, gc, 6, y, kWinWidth - 13, h - 1);
|
||
|
|
drawPango(layoutSmall, active ? &colYellow : &colMuted, 12, y + 3, 18,
|
||
|
|
label);
|
||
|
|
std::string shown = val;
|
||
|
|
if (active) {
|
||
|
|
shown.push_back('|');
|
||
|
|
}
|
||
|
|
if (val.empty() && !active) {
|
||
|
|
drawPango(layoutMain, &colMuted, 12, y + 23, 28, placeholder, 214);
|
||
|
|
} else {
|
||
|
|
if (shown.size() > 21) {
|
||
|
|
shown = ".." + shown.substr(shown.size() - 19);
|
||
|
|
}
|
||
|
|
drawPango(layoutMain, &colWhite, 12, y + 23, 28, shown, 214);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
drawFieldBox(44, 56, editField == 0, "Name (# changes case):", editName,
|
||
|
|
"Type name on keypad...");
|
||
|
|
drawFieldBox(106, 56, editField == 1,
|
||
|
|
"Number (Hold 0 for +):", editNumber, "Enter phone number...");
|
||
|
|
|
||
|
|
// Button Row 2: Pick from Recent Numbers (y = 168..208)
|
||
|
|
auto drawBtnRow = [&](int y, int h, bool active, const char* icon,
|
||
|
|
const char* label, XftColor* icCol) {
|
||
|
|
XSetForeground(dpy, gc, active ? 0x005577 : 0x2c2c2c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 6, y, kWinWidth - 12, h);
|
||
|
|
if (active) {
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 6, y, 4, h);
|
||
|
|
}
|
||
|
|
drawPango(layoutIcon, active ? &colYellow : icCol, 14, y, h, icon);
|
||
|
|
drawPango(layoutMain, active ? &colWhite : &colLight, 38, y, h, label,
|
||
|
|
190);
|
||
|
|
};
|
||
|
|
|
||
|
|
drawBtnRow(168, 42, editField == 2, kIconClock, "Use Recent Number",
|
||
|
|
&colBlue);
|
||
|
|
drawBtnRow(216, 44, editField == 3, kIconCheck, "Save Contact",
|
||
|
|
&colGreen);
|
||
|
|
|
||
|
|
bool canClearChar =
|
||
|
|
(editField == 0 && !editName.empty()) ||
|
||
|
|
(editField == 1 && !editNumber.empty());
|
||
|
|
drawSoftbar("Save", (editField >= 2) ? "Select" : "Next",
|
||
|
|
canClearChar ? "Clear" : "Cancel", &colGreen);
|
||
|
|
} else if (mode == ViewMode::PickRecent) {
|
||
|
|
XSetForeground(dpy, gc, 0x1c1c1c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
|
||
|
|
XSetForeground(dpy, gc, 0x444444);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, kHeaderH - 1, kWinWidth, 1);
|
||
|
|
drawPango(layoutIcon, &colBlue, 8, 0, kHeaderH, kIconClock);
|
||
|
|
drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, "Recent Numbers", 200);
|
||
|
|
|
||
|
|
if (recentUnknowns.empty()) {
|
||
|
|
drawPango(layoutMain, &colMuted, 24, kListY + 70, 28,
|
||
|
|
"No recent numbers", 196);
|
||
|
|
} else {
|
||
|
|
int total = static_cast<int>(recentUnknowns.size());
|
||
|
|
int scrollOffset = 0;
|
||
|
|
if (recentSelected >= kVisibleRows) {
|
||
|
|
scrollOffset = recentSelected - kVisibleRows + 1;
|
||
|
|
}
|
||
|
|
for (int i = 0; i < kVisibleRows; ++i) {
|
||
|
|
int idx = scrollOffset + i;
|
||
|
|
if (idx >= total) break;
|
||
|
|
int ry = kListY + (i * kListH) / kVisibleRows;
|
||
|
|
int nextRy = kListY + ((i + 1) * kListH) / kVisibleRows;
|
||
|
|
int rowH = nextRy - ry;
|
||
|
|
bool isSel = (idx == recentSelected);
|
||
|
|
XSetForeground(dpy, gc,
|
||
|
|
isSel ? 0x005577 : ((i % 2 == 0) ? 0x282828 : 0x2e2e2e));
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, kWinWidth, rowH);
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, ry, 4, rowH);
|
||
|
|
}
|
||
|
|
drawPango(layoutIcon, isSel ? &colYellow : &colGreen, 10, ry, rowH,
|
||
|
|
kIconPhone);
|
||
|
|
drawPango(layoutMain, isSel ? &colWhite : &colLight, 34, ry, rowH,
|
||
|
|
recentUnknowns[idx], 198);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
drawSoftbar("Use", "Select", "Back", &colGreen);
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
XSetForeground(dpy, gc, 0x1c1c1c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 0, 0, kWinWidth, kHeaderH);
|
||
|
|
drawPango(layoutIcon, &colRed, 8, 0, kHeaderH, kIconTrash);
|
||
|
|
drawPango(layoutMain, &colWhite, 32, 0, kHeaderH, "Delete Contact?", 200);
|
||
|
|
|
||
|
|
ContactEntry c;
|
||
|
|
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
|
||
|
|
c = filtered[selected];
|
||
|
|
}
|
||
|
|
drawPango(layoutMain, &colWhite, 14, 54, 28, c.name, 212);
|
||
|
|
drawPango(layoutSmall, &colYellow, 14, 82, 22, c.number, 212);
|
||
|
|
|
||
|
|
const char* ynLabels[2] = {"No, keep contact", "Yes, delete contact"};
|
||
|
|
for (int i = 0; i < 2; ++i) {
|
||
|
|
int ry = 130 + i * 50;
|
||
|
|
bool isSel = (i == confirmSelected);
|
||
|
|
XSetForeground(dpy, gc, isSel ? 0x005577 : 0x2c2c2c);
|
||
|
|
XFillRectangle(dpy, pix, gc, 8, ry, kWinWidth - 16, 44);
|
||
|
|
if (isSel) {
|
||
|
|
XSetForeground(dpy, gc, 0xf4d03f);
|
||
|
|
XFillRectangle(dpy, pix, gc, 8, ry, 4, 44);
|
||
|
|
}
|
||
|
|
drawPango(layoutIcon, (i == 1) ? &colRed : &colGreen, 16, ry, 44,
|
||
|
|
(i == 1) ? kIconTrash : kIconCheck);
|
||
|
|
drawPango(layoutMain, isSel ? &colWhite : &colLight, 40, ry, 44,
|
||
|
|
ynLabels[i], 184);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawSoftbar("Delete", "Select", "Cancel", &colRed);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visible) {
|
||
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||
|
|
XFlush(dpy);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Pre-warm Pango/FreeType/Xrender glyph caches into offscreen Pixmap at startup
|
||
|
|
drawPango(layoutIcon, &colGreen, 0, 0, 20,
|
||
|
|
std::string(kIconBook) + kIconUser + kIconSearch + kIconPlus +
|
||
|
|
kIconEdit + kIconTrash + kIconSms + kIconList + kIconCheck +
|
||
|
|
kIconPhone + kIconClock);
|
||
|
|
render();
|
||
|
|
XSync(dpy, False);
|
||
|
|
|
||
|
|
auto beginNewContact = [&](const std::string& prefillNumber) {
|
||
|
|
prevModeBeforeEdit = mode;
|
||
|
|
editIsNew = true;
|
||
|
|
editOrigName.clear();
|
||
|
|
editOrigNumber.clear();
|
||
|
|
editName.clear();
|
||
|
|
editNumber = normalizePhone(prefillNumber);
|
||
|
|
editField = 0;
|
||
|
|
caseMode = TextCaseMode::Title;
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto beginEditContact = [&](const ContactEntry& c) {
|
||
|
|
prevModeBeforeEdit = mode;
|
||
|
|
editIsNew = false;
|
||
|
|
editOrigName = c.name;
|
||
|
|
editOrigNumber = c.number;
|
||
|
|
editName = c.name;
|
||
|
|
editNumber = c.number;
|
||
|
|
editField = 0;
|
||
|
|
caseMode = TextCaseMode::Title;
|
||
|
|
lastTapDigit = -1;
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto saveEditedContact = [&]() -> bool {
|
||
|
|
std::string nm = trimStr(editName);
|
||
|
|
std::string num = normalizePhone(editNumber);
|
||
|
|
if (nm.empty() || num.empty()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (!book.addOrUpdateContact(editIsNew ? "" : editOrigNumber,
|
||
|
|
editIsNew ? "" : editOrigName, num, nm)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
contactsStamp.checkAndUpdate(getContactsFilePath());
|
||
|
|
searchQuery.clear();
|
||
|
|
updateFilter();
|
||
|
|
std::string targetLower = toLowerStr(nm);
|
||
|
|
for (size_t i = 0; i < filtered.size(); ++i) {
|
||
|
|
if (filtered[i].lowerName == targetLower &&
|
||
|
|
filtered[i].normNumber == num) {
|
||
|
|
selected = static_cast<int>(i);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
mode = ViewMode::List;
|
||
|
|
return true;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto showWindow = [&](const std::string& arg) {
|
||
|
|
if (isPidFileAlive(getRuntimeFile("xdialer.pid"))) {
|
||
|
|
sendPluginCommand("dialer", "CLOSE");
|
||
|
|
}
|
||
|
|
if (isPidFileAlive(getRuntimeFile("xcalllog.pid"))) {
|
||
|
|
sendPluginCommand("calllog", "CLOSE");
|
||
|
|
}
|
||
|
|
if (isPidFileAlive(getRuntimeFile("xdrawer.pid"))) {
|
||
|
|
sendPluginCommand("xdrawer", "CLOSE");
|
||
|
|
}
|
||
|
|
bool openNewMode = (arg.rfind("new:", 0) == 0);
|
||
|
|
std::string prefill = openNewMode ? arg.substr(4) : "";
|
||
|
|
|
||
|
|
if (!visible) {
|
||
|
|
openStartMs = nowMs();
|
||
|
|
startupIgnoreUntilMs = openStartMs + 90;
|
||
|
|
initialSoftkeysReleased = (querySoftkeysDown(dpy) == 0);
|
||
|
|
softkeyPending = 0;
|
||
|
|
softkeyDeadlineMs = 0;
|
||
|
|
zeroHeld = false;
|
||
|
|
visible = true;
|
||
|
|
XMapRaised(dpy, win);
|
||
|
|
if (!openNewMode) {
|
||
|
|
XCopyArea(dpy, pix, win, gc, 0, 0, kWinWidth, kWinHeight, 0, 0);
|
||
|
|
}
|
||
|
|
for (int i = 0; i < 20; ++i) {
|
||
|
|
if (XGrabKeyboard(dpy, win, True, GrabModeAsync, GrabModeAsync, CurrentTime) ==
|
||
|
|
GrabSuccess) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
usleep(2000);
|
||
|
|
}
|
||
|
|
bool reloaded = reloadContactsIfNeeded();
|
||
|
|
if (openNewMode) {
|
||
|
|
beginNewContact(prefill);
|
||
|
|
render();
|
||
|
|
} else if (reloaded || mode != ViewMode::List || !searchQuery.empty() ||
|
||
|
|
selected != 0) {
|
||
|
|
mode = ViewMode::List;
|
||
|
|
searchQuery.clear();
|
||
|
|
selected = 0;
|
||
|
|
updateFilter();
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
XFlush(dpy);
|
||
|
|
writeIntFile(activePidPath, static_cast<int>(::getpid()));
|
||
|
|
} else {
|
||
|
|
reloadContactsIfNeeded();
|
||
|
|
if (openNewMode) {
|
||
|
|
beginNewContact(prefill);
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto hideWindow = [&](bool drainSoftkeys) {
|
||
|
|
if (!visible) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
int xfd = ConnectionNumber(dpy);
|
||
|
|
if (drainSoftkeys) {
|
||
|
|
int64_t drainDeadline = nowMs() + 180;
|
||
|
|
while (nowMs() < drainDeadline && querySoftkeysDown(dpy) != 0) {
|
||
|
|
while (XPending(dpy)) {
|
||
|
|
XEvent ev;
|
||
|
|
XNextEvent(dpy, &ev);
|
||
|
|
}
|
||
|
|
struct pollfd pfd {};
|
||
|
|
pfd.fd = xfd;
|
||
|
|
pfd.events = POLLIN;
|
||
|
|
::poll(&pfd, 1, 10);
|
||
|
|
}
|
||
|
|
writeSoftkeySuppress(nowMs() + 300);
|
||
|
|
}
|
||
|
|
XUngrabKeyboard(dpy, CurrentTime);
|
||
|
|
XUnmapWindow(dpy, win);
|
||
|
|
XFlush(dpy);
|
||
|
|
::unlink(activePidPath.c_str());
|
||
|
|
visible = false;
|
||
|
|
softkeyPending = 0;
|
||
|
|
softkeyDeadlineMs = 0;
|
||
|
|
zeroHeld = false;
|
||
|
|
if (mode != ViewMode::List || !searchQuery.empty() || selected != 0) {
|
||
|
|
mode = ViewMode::List;
|
||
|
|
searchQuery.clear();
|
||
|
|
selected = 0;
|
||
|
|
updateFilter();
|
||
|
|
render();
|
||
|
|
XFlush(dpy);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!isPreload) {
|
||
|
|
showWindow(initArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
int xfd = ConnectionNumber(dpy);
|
||
|
|
|
||
|
|
auto handleMultiTapNameChar = [&](int digit) {
|
||
|
|
if (digit < 0 || digit > 9) return;
|
||
|
|
int64_t now = nowMs();
|
||
|
|
if (caseMode == TextCaseMode::Num) {
|
||
|
|
editName.push_back(static_cast<char>('0' + digit));
|
||
|
|
lastTapDigit = -1;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const char* seq = kMultiTapLower[digit];
|
||
|
|
int seqLen = static_cast<int>(std::strlen(seq));
|
||
|
|
if (digit == lastTapDigit && (now - lastTapMs) <= 650 && !editName.empty()) {
|
||
|
|
lastTapCycle = (lastTapCycle + 1) % seqLen;
|
||
|
|
char ch = seq[lastTapCycle];
|
||
|
|
bool makeUpper = false;
|
||
|
|
if (caseMode == TextCaseMode::Upper) {
|
||
|
|
makeUpper = true;
|
||
|
|
} else if (caseMode == TextCaseMode::Title) {
|
||
|
|
makeUpper = (editName.size() == 1 ||
|
||
|
|
editName[editName.size() - 2] == ' ');
|
||
|
|
}
|
||
|
|
if (makeUpper) {
|
||
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
|
|
}
|
||
|
|
editName.back() = ch;
|
||
|
|
} else {
|
||
|
|
lastTapDigit = digit;
|
||
|
|
lastTapCycle = 0;
|
||
|
|
char ch = seq[0];
|
||
|
|
bool makeUpper = false;
|
||
|
|
if (caseMode == TextCaseMode::Upper) {
|
||
|
|
makeUpper = true;
|
||
|
|
} else if (caseMode == TextCaseMode::Title) {
|
||
|
|
makeUpper = (editName.empty() || editName.back() == ' ');
|
||
|
|
}
|
||
|
|
if (makeUpper) {
|
||
|
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||
|
|
}
|
||
|
|
editName.push_back(ch);
|
||
|
|
}
|
||
|
|
lastTapMs = now;
|
||
|
|
};
|
||
|
|
|
||
|
|
auto executeDetailAction = [&](int actIdx) {
|
||
|
|
if (selected < 0 || selected >= static_cast<int>(filtered.size())) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
ContactEntry c = filtered[selected];
|
||
|
|
switch (actIdx) {
|
||
|
|
case 0: // Call Contact
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
placePhoneCall(c.number);
|
||
|
|
break;
|
||
|
|
case 1: // Send SMS
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
runAsyncCmd("sxmo_modemtext.sh sendtextmenu '" + c.number + "'");
|
||
|
|
break;
|
||
|
|
case 2: // Read Messages
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
runAsyncCmd("sxmo_hook_tailtextlog.sh '" + c.number + "'");
|
||
|
|
break;
|
||
|
|
case 3: // Open in Dialer
|
||
|
|
writeSoftkeySuppress(nowMs() + 300);
|
||
|
|
hideWindow(false);
|
||
|
|
if (!sendPluginCommand("dialer", "OPEN " + c.number)) {
|
||
|
|
runAsyncArgv("xdialer", c.number.c_str());
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 4: // Edit Contact
|
||
|
|
beginEditContact(c);
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case 5: // Delete Contact
|
||
|
|
confirmSelected = 0;
|
||
|
|
mode = ViewMode::ConfirmDelete;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
auto executeOptionAction = [&](int optIdx) {
|
||
|
|
if (filtered.empty()) {
|
||
|
|
if (optIdx == 0) {
|
||
|
|
beginNewContact(searchQuery);
|
||
|
|
render();
|
||
|
|
} else if (optIdx == 1) {
|
||
|
|
recentUnknowns = book.unknownRecentNumbers();
|
||
|
|
recentSelected = 0;
|
||
|
|
beginNewContact("");
|
||
|
|
mode = ViewMode::PickRecent;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
switch (optIdx) {
|
||
|
|
case 0: // New Contact
|
||
|
|
beginNewContact("");
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case 1: // Add from Recent
|
||
|
|
recentUnknowns = book.unknownRecentNumbers();
|
||
|
|
recentSelected = 0;
|
||
|
|
beginNewContact("");
|
||
|
|
mode = ViewMode::PickRecent;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case 2: // Call
|
||
|
|
executeDetailAction(0);
|
||
|
|
break;
|
||
|
|
case 3: // Send SMS
|
||
|
|
executeDetailAction(1);
|
||
|
|
break;
|
||
|
|
case 4: // Edit
|
||
|
|
executeDetailAction(4);
|
||
|
|
break;
|
||
|
|
case 5: // Delete
|
||
|
|
executeDetailAction(5);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
while (gRunning) {
|
||
|
|
for (const std::string& cmd : readFifoLines(fifoFd)) {
|
||
|
|
if (cmd == "NOP") {
|
||
|
|
continue;
|
||
|
|
} else if (cmd == "CLOSE") {
|
||
|
|
hideWindow(false);
|
||
|
|
} else if (cmd == "TOGGLE") {
|
||
|
|
if (visible) {
|
||
|
|
hideWindow(false);
|
||
|
|
} else {
|
||
|
|
showWindow("");
|
||
|
|
}
|
||
|
|
} else if (cmd == "OPEN") {
|
||
|
|
showWindow("");
|
||
|
|
} else if (cmd.rfind("OPEN ", 0) == 0) {
|
||
|
|
showWindow(cmd.substr(5));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visible && mode == ViewMode::Edit && editField == 1 && zeroHeld) {
|
||
|
|
if (!isKeycodeDown(dpy, 19)) {
|
||
|
|
zeroHeld = false;
|
||
|
|
} else if (nowMs() - zeroPressMs >= 420) {
|
||
|
|
zeroHeld = false;
|
||
|
|
if (!editNumber.empty() && editNumber.back() == '0') {
|
||
|
|
editNumber.back() = '+';
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visible && !initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
|
||
|
|
initialSoftkeysReleased = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
while (XPending(dpy)) {
|
||
|
|
XEvent ev;
|
||
|
|
XNextEvent(dpy, &ev);
|
||
|
|
if (!visible) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!initialSoftkeysReleased && querySoftkeysDown(dpy) == 0) {
|
||
|
|
initialSoftkeysReleased = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ev.type == Expose) {
|
||
|
|
if (ev.xexpose.count == 0) {
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
} else if (ev.type == VisibilityNotify) {
|
||
|
|
if (ev.xvisibility.state != VisibilityUnobscured) {
|
||
|
|
XRaiseWindow(dpy, win);
|
||
|
|
XFlush(dpy);
|
||
|
|
}
|
||
|
|
} else if (ev.type == KeyRelease) {
|
||
|
|
KeySym sym = XLookupKeysym(&ev.xkey, 0);
|
||
|
|
if (ev.xkey.keycode == 19 || sym == XK_0) {
|
||
|
|
zeroHeld = false;
|
||
|
|
}
|
||
|
|
if (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
|
||
|
|
sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
|
||
|
|
ev.xkey.keycode == 135 || ev.xkey.keycode == 166) {
|
||
|
|
if (querySoftkeysDown(dpy) == 0) {
|
||
|
|
initialSoftkeysReleased = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (ev.type == KeyPress) {
|
||
|
|
KeySym sym = XLookupKeysym(&ev.xkey, 0);
|
||
|
|
bool isLeftSoft = (sym == XK_Menu || sym == XF86XK_KbdLcdMenu1 ||
|
||
|
|
ev.xkey.keycode == 135);
|
||
|
|
bool isRightSoft = (sym == XF86XK_Back || sym == XF86XK_KbdLcdMenu2 ||
|
||
|
|
ev.xkey.keycode == 166);
|
||
|
|
|
||
|
|
if (isLeftSoft || isRightSoft) {
|
||
|
|
if (!initialSoftkeysReleased || nowMs() < startupIgnoreUntilMs) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
int which = (isLeftSoft ? 1 : 0) | (isRightSoft ? 2 : 0);
|
||
|
|
softkeyPending |= which | querySoftkeysDown(dpy);
|
||
|
|
if (softkeyPending == 3) {
|
||
|
|
writeSoftkeySuppress(nowMs() + 500);
|
||
|
|
hideWindow(false);
|
||
|
|
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
|
||
|
|
runAsyncArgv("xdrawer");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (softkeyDeadlineMs == 0) {
|
||
|
|
softkeyDeadlineMs = nowMs() + 40;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sym == XK_Escape) {
|
||
|
|
if (mode == ViewMode::List) {
|
||
|
|
writeSoftkeySuppress(nowMs() + 300);
|
||
|
|
hideWindow(true);
|
||
|
|
} else if (mode == ViewMode::PickRecent) {
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
render();
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle mode-specific keys
|
||
|
|
if (mode == ViewMode::List) {
|
||
|
|
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
|
||
|
|
if (dialCh >= '0' && dialCh <= '9') {
|
||
|
|
searchQuery.push_back(dialCh);
|
||
|
|
selected = 0;
|
||
|
|
updateFilter();
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
switch (sym) {
|
||
|
|
case XK_BackSpace:
|
||
|
|
case XK_Left:
|
||
|
|
case XK_KP_Left:
|
||
|
|
if (!searchQuery.empty()) {
|
||
|
|
searchQuery.pop_back();
|
||
|
|
selected = 0;
|
||
|
|
updateFilter();
|
||
|
|
render();
|
||
|
|
} else if (sym == XK_BackSpace) {
|
||
|
|
hideWindow(true);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
if (!filtered.empty()) {
|
||
|
|
selected = (selected <= 0)
|
||
|
|
? (static_cast<int>(filtered.size()) - 1)
|
||
|
|
: (selected - 1);
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
if (!filtered.empty()) {
|
||
|
|
selected = (selected + 1 >= static_cast<int>(filtered.size()))
|
||
|
|
? 0
|
||
|
|
: (selected + 1);
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XF86XK_Phone:
|
||
|
|
case XF86XK_PickupPhone:
|
||
|
|
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
|
||
|
|
std::string target = filtered[selected].number;
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
placePhoneCall(target);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
if (filtered.empty()) {
|
||
|
|
beginNewContact(searchQuery);
|
||
|
|
} else {
|
||
|
|
detailSelected = 0;
|
||
|
|
mode = ViewMode::Detail;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
if (ev.xkey.keycode == 177 && selected >= 0 &&
|
||
|
|
selected < static_cast<int>(filtered.size())) {
|
||
|
|
std::string target = filtered[selected].number;
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
placePhoneCall(target);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Options) {
|
||
|
|
int optCount = filtered.empty() ? 2 : 6;
|
||
|
|
switch (sym) {
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
optionSelected =
|
||
|
|
(optionSelected <= 0) ? (optCount - 1) : (optionSelected - 1);
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
optionSelected =
|
||
|
|
(optionSelected + 1 >= optCount) ? 0 : (optionSelected + 1);
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
executeOptionAction(optionSelected);
|
||
|
|
break;
|
||
|
|
case XK_BackSpace:
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Detail) {
|
||
|
|
constexpr int kActCount = 6;
|
||
|
|
switch (sym) {
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
detailSelected =
|
||
|
|
(detailSelected <= 0) ? (kActCount - 1) : (detailSelected - 1);
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
detailSelected =
|
||
|
|
(detailSelected + 1 >= kActCount) ? 0 : (detailSelected + 1);
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
executeDetailAction(detailSelected);
|
||
|
|
break;
|
||
|
|
case XF86XK_Phone:
|
||
|
|
case XF86XK_PickupPhone:
|
||
|
|
executeDetailAction(0);
|
||
|
|
break;
|
||
|
|
case XK_BackSpace:
|
||
|
|
case XK_Left:
|
||
|
|
case XK_KP_Left:
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
if (ev.xkey.keycode == 177) {
|
||
|
|
executeDetailAction(0);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Edit) {
|
||
|
|
char dialCh = mapDialKey(sym, ev.xkey.keycode, ev.xkey.state);
|
||
|
|
if (editField == 0) {
|
||
|
|
if (dialCh == '#') {
|
||
|
|
caseMode = static_cast<TextCaseMode>(
|
||
|
|
(static_cast<int>(caseMode) + 1) % 4);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (dialCh == '*') {
|
||
|
|
editName.push_back(' ');
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (dialCh >= '0' && dialCh <= '9') {
|
||
|
|
handleMultiTapNameChar(dialCh - '0');
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (sym >= XK_a && sym <= XK_z) {
|
||
|
|
char c = static_cast<char>('a' + (sym - XK_a));
|
||
|
|
if (ev.xkey.state & ShiftMask) {
|
||
|
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||
|
|
}
|
||
|
|
editName.push_back(c);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (sym == XK_space) {
|
||
|
|
editName.push_back(' ');
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
} else if (editField == 1) {
|
||
|
|
if (dialCh != '\0') {
|
||
|
|
editNumber.push_back(dialCh);
|
||
|
|
if (dialCh == '0') {
|
||
|
|
zeroHeld = true;
|
||
|
|
zeroPressMs = nowMs();
|
||
|
|
} else {
|
||
|
|
zeroHeld = false;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (sym) {
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
editField = (editField <= 0) ? 3 : (editField - 1);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
editField = (editField >= 3) ? 0 : (editField + 1);
|
||
|
|
lastTapDigit = -1;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_BackSpace:
|
||
|
|
case XK_Left:
|
||
|
|
case XK_KP_Left:
|
||
|
|
lastTapDigit = -1;
|
||
|
|
if (editField == 0 && !editName.empty()) {
|
||
|
|
editName.pop_back();
|
||
|
|
render();
|
||
|
|
} else if (editField == 1 && !editNumber.empty()) {
|
||
|
|
editNumber.pop_back();
|
||
|
|
render();
|
||
|
|
} else if (sym == XK_BackSpace) {
|
||
|
|
mode = prevModeBeforeEdit;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Right:
|
||
|
|
case XK_KP_Right:
|
||
|
|
lastTapDigit = -1;
|
||
|
|
if (editField == 1 &&
|
||
|
|
(editNumber.empty() || editNumber[0] != '+')) {
|
||
|
|
editNumber = "+" + editNumber;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
lastTapDigit = -1;
|
||
|
|
if (editField == 0) {
|
||
|
|
editField = 1;
|
||
|
|
render();
|
||
|
|
} else if (editField == 1) {
|
||
|
|
if (!saveEditedContact()) {
|
||
|
|
editField = editName.empty() ? 0 : 1;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
} else if (editField == 2) {
|
||
|
|
recentUnknowns = book.unknownRecentNumbers();
|
||
|
|
recentSelected = 0;
|
||
|
|
mode = ViewMode::PickRecent;
|
||
|
|
render();
|
||
|
|
} else if (editField == 3) {
|
||
|
|
if (!saveEditedContact()) {
|
||
|
|
editField = editName.empty() ? 0 : 1;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::PickRecent) {
|
||
|
|
switch (sym) {
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
if (!recentUnknowns.empty()) {
|
||
|
|
recentSelected =
|
||
|
|
(recentSelected <= 0)
|
||
|
|
? (static_cast<int>(recentUnknowns.size()) - 1)
|
||
|
|
: (recentSelected - 1);
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
if (!recentUnknowns.empty()) {
|
||
|
|
recentSelected =
|
||
|
|
(recentSelected + 1 >= static_cast<int>(recentUnknowns.size()))
|
||
|
|
? 0
|
||
|
|
: (recentSelected + 1);
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
if (recentSelected >= 0 &&
|
||
|
|
recentSelected < static_cast<int>(recentUnknowns.size())) {
|
||
|
|
editNumber = recentUnknowns[recentSelected];
|
||
|
|
editField = editName.empty() ? 0 : 3;
|
||
|
|
}
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_BackSpace:
|
||
|
|
case XK_Left:
|
||
|
|
case XK_KP_Left:
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
switch (sym) {
|
||
|
|
case XK_Up:
|
||
|
|
case XK_KP_Up:
|
||
|
|
case XK_Down:
|
||
|
|
case XK_KP_Down:
|
||
|
|
confirmSelected = 1 - confirmSelected;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_Return:
|
||
|
|
case XK_KP_Enter:
|
||
|
|
if (confirmSelected == 1 && selected >= 0 &&
|
||
|
|
selected < static_cast<int>(filtered.size())) {
|
||
|
|
book.deleteContact(filtered[selected].number,
|
||
|
|
filtered[selected].name);
|
||
|
|
contactsStamp.checkAndUpdate(getContactsFilePath());
|
||
|
|
updateFilter();
|
||
|
|
mode = ViewMode::List;
|
||
|
|
} else {
|
||
|
|
mode = ViewMode::Detail;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
case XK_BackSpace:
|
||
|
|
mode = ViewMode::Detail;
|
||
|
|
render();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visible && softkeyPending != 0 && nowMs() >= softkeyDeadlineMs) {
|
||
|
|
int comb = softkeyPending | querySoftkeysDown(dpy);
|
||
|
|
softkeyPending = 0;
|
||
|
|
softkeyDeadlineMs = 0;
|
||
|
|
if (comb == 3) {
|
||
|
|
writeSoftkeySuppress(nowMs() + 500);
|
||
|
|
hideWindow(false);
|
||
|
|
if (!sendPluginCommand("xdrawer", "TOGGLE")) {
|
||
|
|
runAsyncArgv("xdrawer");
|
||
|
|
}
|
||
|
|
} else if (comb == 1) {
|
||
|
|
// Left softkey
|
||
|
|
if (mode == ViewMode::List) {
|
||
|
|
optionSelected = 0;
|
||
|
|
mode = ViewMode::Options;
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::Options) {
|
||
|
|
executeOptionAction(optionSelected);
|
||
|
|
} else if (mode == ViewMode::Detail) {
|
||
|
|
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
|
||
|
|
beginEditContact(filtered[selected]);
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Edit) {
|
||
|
|
if (!saveEditedContact()) {
|
||
|
|
editField = editName.empty() ? 0 : 1;
|
||
|
|
}
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::PickRecent) {
|
||
|
|
if (recentSelected >= 0 &&
|
||
|
|
recentSelected < static_cast<int>(recentUnknowns.size())) {
|
||
|
|
editNumber = recentUnknowns[recentSelected];
|
||
|
|
editField = editName.empty() ? 0 : 3;
|
||
|
|
}
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
if (selected >= 0 && selected < static_cast<int>(filtered.size())) {
|
||
|
|
book.deleteContact(filtered[selected].number,
|
||
|
|
filtered[selected].name);
|
||
|
|
contactsStamp.checkAndUpdate(getContactsFilePath());
|
||
|
|
updateFilter();
|
||
|
|
}
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
} else if (comb == 2) {
|
||
|
|
// Right softkey
|
||
|
|
if (mode == ViewMode::List) {
|
||
|
|
if (!searchQuery.empty()) {
|
||
|
|
searchQuery.clear();
|
||
|
|
selected = 0;
|
||
|
|
updateFilter();
|
||
|
|
render();
|
||
|
|
} else {
|
||
|
|
writeSoftkeySuppress(nowMs() + 350);
|
||
|
|
hideWindow(true);
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::Options) {
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::Detail) {
|
||
|
|
mode = ViewMode::List;
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::Edit) {
|
||
|
|
lastTapDigit = -1;
|
||
|
|
if (editField == 0 && !editName.empty()) {
|
||
|
|
editName.pop_back();
|
||
|
|
render();
|
||
|
|
} else if (editField == 1 && !editNumber.empty()) {
|
||
|
|
editNumber.pop_back();
|
||
|
|
render();
|
||
|
|
} else {
|
||
|
|
mode = (prevModeBeforeEdit == ViewMode::Detail) ? ViewMode::Detail
|
||
|
|
: ViewMode::List;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
} else if (mode == ViewMode::PickRecent) {
|
||
|
|
mode = ViewMode::Edit;
|
||
|
|
render();
|
||
|
|
} else if (mode == ViewMode::ConfirmDelete) {
|
||
|
|
mode = ViewMode::Detail;
|
||
|
|
render();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int timeoutMs = -1;
|
||
|
|
if (visible) {
|
||
|
|
timeoutMs = 200;
|
||
|
|
if (softkeyDeadlineMs > 0) {
|
||
|
|
timeoutMs =
|
||
|
|
static_cast<int>(std::max<int64_t>(5, softkeyDeadlineMs - nowMs()));
|
||
|
|
}
|
||
|
|
if (zeroHeld) {
|
||
|
|
int64_t rem = 420 - (nowMs() - zeroPressMs);
|
||
|
|
timeoutMs = std::min(timeoutMs, static_cast<int>(std::max<int64_t>(5, rem)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct pollfd pfds[2] {};
|
||
|
|
pfds[0].fd = xfd;
|
||
|
|
pfds[0].events = POLLIN;
|
||
|
|
pfds[1].fd = fifoFd;
|
||
|
|
pfds[1].events = POLLIN;
|
||
|
|
::poll(pfds, (fifoFd >= 0) ? 2 : 1, timeoutMs);
|
||
|
|
}
|
||
|
|
|
||
|
|
hideWindow(false);
|
||
|
|
if (fifoFd >= 0) {
|
||
|
|
::close(fifoFd);
|
||
|
|
}
|
||
|
|
::unlink(getPluginFifoPath("contacts").c_str());
|
||
|
|
::unlink(daemonPidPath.c_str());
|
||
|
|
|
||
|
|
pango_font_description_free(descIcon);
|
||
|
|
pango_font_description_free(descSmall);
|
||
|
|
pango_font_description_free(descMain);
|
||
|
|
g_object_unref(layoutIcon);
|
||
|
|
g_object_unref(layoutSmall);
|
||
|
|
g_object_unref(layoutMain);
|
||
|
|
g_object_unref(pangoCtx);
|
||
|
|
XftDrawDestroy(xftDraw);
|
||
|
|
XFreeGC(dpy, gc);
|
||
|
|
XFreePixmap(dpy, pix);
|
||
|
|
XDestroyWindow(dpy, win);
|
||
|
|
XCloseDisplay(dpy);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int contacts_plugin_init(const DwmHostApi* host) {
|
||
|
|
if (host && host->spawn_plugin) {
|
||
|
|
host->spawn_plugin("contacts", "--preload");
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int contacts_on_idle_key(const DwmHostApi* host, KeySym keysym,
|
||
|
|
unsigned int keycode, unsigned int state) {
|
||
|
|
if (!host || !host->is_idle_desktop || !host->is_idle_desktop() || state != 0) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
if (keysym == XF86XK_Back || keysym == XF86XK_KbdLcdMenu2 || keycode == 166) {
|
||
|
|
if (host->spawn_plugin) {
|
||
|
|
host->spawn_plugin("contacts", nullptr);
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
extern "C" __attribute__((visibility("default"))) const DwmPlugin dwm_plugin_entry = {
|
||
|
|
DWM_PLUGIN_API_VERSION,
|
||
|
|
"contacts",
|
||
|
|
"Keypad-driven phonebook & contact manager (opens on idle Right Softkey / Names)",
|
||
|
|
contacts_plugin_init,
|
||
|
|
nullptr,
|
||
|
|
nullptr,
|
||
|
|
contacts_on_idle_key,
|
||
|
|
nullptr,
|
||
|
|
runContactsApp
|
||
|
|
};
|