ren-ui-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ren_ui/__init__.py +11 -0
- ren_ui/__main__.py +3 -0
- ren_ui/_native.py +70 -0
- ren_ui/app.py +152 -0
- ren_ui/build.py +256 -0
- ren_ui/cli.py +155 -0
- ren_ui/native/bridge.cpp +764 -0
- ren_ui/native/icon.cpp +79 -0
- ren_ui/native/launcher.cpp +64 -0
- ren_ui/project.py +209 -0
- ren_ui/py.typed +0 -0
- ren_ui/resources.py +30 -0
- ren_ui/system.py +70 -0
- ren_ui/widgets.py +380 -0
- ren_ui_python-0.1.0.dist-info/METADATA +83 -0
- ren_ui_python-0.1.0.dist-info/RECORD +20 -0
- ren_ui_python-0.1.0.dist-info/WHEEL +5 -0
- ren_ui_python-0.1.0.dist-info/entry_points.txt +3 -0
- ren_ui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- ren_ui_python-0.1.0.dist-info/top_level.txt +1 -0
ren_ui/native/bridge.cpp
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// ABI v5. Only opaque application pointers and integer object IDs cross the ABI.
|
|
3
|
+
#include <Alert.h>
|
|
4
|
+
#include <Application.h>
|
|
5
|
+
#include <Button.h>
|
|
6
|
+
#include <Bitmap.h>
|
|
7
|
+
#include <Entry.h>
|
|
8
|
+
#include <File.h>
|
|
9
|
+
#include <FilePanel.h>
|
|
10
|
+
#include <IconUtils.h>
|
|
11
|
+
#include <Looper.h>
|
|
12
|
+
#include <TranslationUtils.h>
|
|
13
|
+
#include <CheckBox.h>
|
|
14
|
+
#include <Clipboard.h>
|
|
15
|
+
#include <FindDirectory.h>
|
|
16
|
+
#include <GroupLayout.h>
|
|
17
|
+
#include <GroupView.h>
|
|
18
|
+
#include <Message.h>
|
|
19
|
+
#include <Messenger.h>
|
|
20
|
+
#include <Path.h>
|
|
21
|
+
#include <Roster.h>
|
|
22
|
+
#include <ScrollView.h>
|
|
23
|
+
#include <Slider.h>
|
|
24
|
+
#include <StringView.h>
|
|
25
|
+
#include <TextControl.h>
|
|
26
|
+
#include <TextView.h>
|
|
27
|
+
#include <Window.h>
|
|
28
|
+
|
|
29
|
+
#include <algorithm>
|
|
30
|
+
#include <cmath>
|
|
31
|
+
#include <vector>
|
|
32
|
+
#include <atomic>
|
|
33
|
+
#include <cstdint>
|
|
34
|
+
#include <cstring>
|
|
35
|
+
#include <map>
|
|
36
|
+
#include <memory>
|
|
37
|
+
#include <mutex>
|
|
38
|
+
#include <stdexcept>
|
|
39
|
+
#include <string>
|
|
40
|
+
|
|
41
|
+
namespace {
|
|
42
|
+
thread_local std::string error;
|
|
43
|
+
thread_local std::string result;
|
|
44
|
+
using Callback = void (*)(int64_t, int64_t, const char*, const char*, int);
|
|
45
|
+
constexpr uint32 kControl = 'renC';
|
|
46
|
+
constexpr uint32 kCloseAccepted = 'renQ';
|
|
47
|
+
|
|
48
|
+
void check(status_t status, const char* operation)
|
|
49
|
+
{
|
|
50
|
+
if (status < B_OK)
|
|
51
|
+
throw std::runtime_error(std::string(operation) + ": " + strerror(status));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Decode before replacing a live image, so errors keep the previous bitmap.
|
|
55
|
+
std::unique_ptr<BBitmap> loadBitmap(const char* path, int vectorSize)
|
|
56
|
+
{
|
|
57
|
+
BFile file(path, B_READ_ONLY);
|
|
58
|
+
check(file.InitCheck(), "Open image");
|
|
59
|
+
char magic[4];
|
|
60
|
+
bool vector = file.Read(magic, sizeof(magic)) == 4 && !memcmp(magic, "ncif", 4);
|
|
61
|
+
std::unique_ptr<BBitmap> bitmap;
|
|
62
|
+
if (vector) {
|
|
63
|
+
off_t size;
|
|
64
|
+
check(file.GetSize(&size), "Read HVIF size");
|
|
65
|
+
if (size < 7 || size > 64 * 1024)
|
|
66
|
+
throw std::runtime_error("HVIF must be between 7 bytes and 64 KiB");
|
|
67
|
+
std::vector<uint8> data(static_cast<size_t>(size));
|
|
68
|
+
if (file.ReadAt(0, data.data(), data.size()) != size)
|
|
69
|
+
throw std::runtime_error("Could not read HVIF");
|
|
70
|
+
bitmap.reset(new BBitmap(BRect(0, 0, vectorSize - 1, vectorSize - 1), B_RGBA32));
|
|
71
|
+
check(bitmap->InitCheck(), "HVIF bitmap");
|
|
72
|
+
check(BIconUtils::GetVectorIcon(data.data(), data.size(), bitmap.get()), "Decode HVIF");
|
|
73
|
+
} else {
|
|
74
|
+
bitmap.reset(BTranslationUtils::GetBitmapFile(path));
|
|
75
|
+
if (!bitmap) throw std::runtime_error(std::string("Cannot decode image: ") + path);
|
|
76
|
+
check(bitmap->InitCheck(), "Image bitmap");
|
|
77
|
+
}
|
|
78
|
+
if (bitmap->Bounds().Width() >= 16384 || bitmap->Bounds().Height() >= 16384
|
|
79
|
+
|| bitmap->BitsLength() > 64 * 1024 * 1024)
|
|
80
|
+
throw std::runtime_error("Decoded image exceeds 16384 pixels or 64 MiB");
|
|
81
|
+
return bitmap;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
BRect imageRect(BRect source, BRect target, const std::string& fit)
|
|
85
|
+
{
|
|
86
|
+
if (fit == "stretch") return target;
|
|
87
|
+
float sx = (target.Width() + 1) / (source.Width() + 1);
|
|
88
|
+
float sy = (target.Height() + 1) / (source.Height() + 1);
|
|
89
|
+
float scale = fit == "cover" ? std::max(sx, sy) : std::min(sx, sy);
|
|
90
|
+
float width = (source.Width() + 1) * scale;
|
|
91
|
+
float height = (source.Height() + 1) * scale;
|
|
92
|
+
float x = target.left + (target.Width() + 1 - width) / 2;
|
|
93
|
+
float y = target.top + (target.Height() + 1 - height) / 2;
|
|
94
|
+
return BRect(x, y, x + width - 1, y + height - 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class ImageView : public BView {
|
|
98
|
+
std::unique_ptr<BBitmap> bitmap;
|
|
99
|
+
std::string fit;
|
|
100
|
+
int vectorSize;
|
|
101
|
+
public:
|
|
102
|
+
ImageView(const char* name, const char* path, float width, float height, const char* mode)
|
|
103
|
+
: BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE), fit(mode),
|
|
104
|
+
vectorSize(0)
|
|
105
|
+
{
|
|
106
|
+
if (width <= 0 || height <= 0 || width > 4096 || height > 4096
|
|
107
|
+
|| !std::isfinite(width) || !std::isfinite(height)
|
|
108
|
+
|| (fit != "contain" && fit != "cover" && fit != "stretch"))
|
|
109
|
+
throw std::runtime_error("Invalid image viewport");
|
|
110
|
+
vectorSize = static_cast<int>(std::ceil(std::max(width, height)));
|
|
111
|
+
bitmap = loadBitmap(path, vectorSize);
|
|
112
|
+
// Haiku's BSize uses coordinate distances (pixel count minus one).
|
|
113
|
+
BSize size(width - 1, height - 1);
|
|
114
|
+
SetExplicitMinSize(size);
|
|
115
|
+
SetExplicitPreferredSize(size);
|
|
116
|
+
SetExplicitMaxSize(size);
|
|
117
|
+
SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
void Replace(const char* path)
|
|
121
|
+
{
|
|
122
|
+
auto next = loadBitmap(path, vectorSize);
|
|
123
|
+
bitmap.swap(next);
|
|
124
|
+
Invalidate();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void Draw(BRect) override
|
|
128
|
+
{
|
|
129
|
+
SetDrawingMode(B_OP_COPY);
|
|
130
|
+
SetHighColor(ViewColor());
|
|
131
|
+
FillRect(Bounds());
|
|
132
|
+
SetDrawingMode(B_OP_ALPHA);
|
|
133
|
+
SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
|
|
134
|
+
DrawBitmap(bitmap.get(), bitmap->Bounds(), imageRect(bitmap->Bounds(), Bounds(), fit),
|
|
135
|
+
B_FILTER_BITMAP_BILINEAR);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
std::unique_ptr<BBitmap> buttonBitmap(const char* path, int size)
|
|
140
|
+
{
|
|
141
|
+
auto source = loadBitmap(path, size);
|
|
142
|
+
auto scaled = std::make_unique<BBitmap>(BRect(0, 0, size - 1, size - 1),
|
|
143
|
+
B_BITMAP_ACCEPTS_VIEWS, B_RGBA32);
|
|
144
|
+
check(scaled->InitCheck(), "Icon bitmap");
|
|
145
|
+
// Start transparent so non-square icons retain their aspect ratio.
|
|
146
|
+
memset(scaled->Bits(), 0, scaled->BitsLength());
|
|
147
|
+
auto painter = std::make_unique<BView>(scaled->Bounds(), "icon", B_FOLLOW_NONE, B_WILL_DRAW);
|
|
148
|
+
if (!scaled->Lock()) throw std::runtime_error("Cannot lock icon bitmap");
|
|
149
|
+
scaled->AddChild(painter.get());
|
|
150
|
+
painter->SetDrawingMode(B_OP_ALPHA);
|
|
151
|
+
painter->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_COMPOSITE);
|
|
152
|
+
painter->DrawBitmap(source.get(), source->Bounds(),
|
|
153
|
+
imageRect(source->Bounds(), scaled->Bounds(), "contain"), B_FILTER_BITMAP_BILINEAR);
|
|
154
|
+
painter->Sync();
|
|
155
|
+
scaled->RemoveChild(painter.get());
|
|
156
|
+
scaled->Unlock();
|
|
157
|
+
return scaled;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
struct Context {
|
|
161
|
+
BApplication* app = nullptr;
|
|
162
|
+
BMessenger messenger;
|
|
163
|
+
Callback callback = nullptr;
|
|
164
|
+
std::mutex mutex;
|
|
165
|
+
std::map<int64_t, BMessenger> windows;
|
|
166
|
+
std::atomic<int> live{0};
|
|
167
|
+
std::atomic<bool> destroying{false};
|
|
168
|
+
int64_t nextWindow = 1;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
class Application : public BApplication {
|
|
172
|
+
Context* context;
|
|
173
|
+
public:
|
|
174
|
+
Application(const char* signature, Context* c)
|
|
175
|
+
: BApplication(signature), context(c) {}
|
|
176
|
+
|
|
177
|
+
bool QuitRequested() override
|
|
178
|
+
{
|
|
179
|
+
if (context->live == 0 || context->destroying)
|
|
180
|
+
return true;
|
|
181
|
+
// BApplication's default stops at the first asynchronous window veto.
|
|
182
|
+
// Ask Python to request every window, then exit after the last closes.
|
|
183
|
+
context->callback(0, 0, "quit_requested", "", 0);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
class TextArea : public BTextView {
|
|
189
|
+
Context* context;
|
|
190
|
+
int64_t windowID;
|
|
191
|
+
int64_t viewID;
|
|
192
|
+
bool silent = false;
|
|
193
|
+
|
|
194
|
+
void Changed()
|
|
195
|
+
{
|
|
196
|
+
// Only snapshot into Python's queue; never execute user code here.
|
|
197
|
+
if (!silent && BView::Window())
|
|
198
|
+
context->callback(windowID, viewID, "change", Text(), 0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protected:
|
|
202
|
+
void InsertText(const char* text, int32 length, int32 offset,
|
|
203
|
+
const text_run_array* runs) override
|
|
204
|
+
{
|
|
205
|
+
BTextView::InsertText(text, length, offset, runs);
|
|
206
|
+
Changed();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
void DeleteText(int32 from, int32 to) override
|
|
210
|
+
{
|
|
211
|
+
BTextView::DeleteText(from, to);
|
|
212
|
+
Changed();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public:
|
|
216
|
+
TextArea(const char* name, Context* c, int64_t wid, int64_t id, bool wrap)
|
|
217
|
+
: BTextView(name, B_WILL_DRAW | B_PULSE_NEEDED | B_NAVIGABLE),
|
|
218
|
+
context(c), windowID(wid), viewID(id)
|
|
219
|
+
{
|
|
220
|
+
SetWordWrap(wrap);
|
|
221
|
+
SetDoesUndo(true);
|
|
222
|
+
SetMaxBytes(1024 * 1024);
|
|
223
|
+
SetInsets(8, 8, 8, 8);
|
|
224
|
+
SetExplicitMinSize(BSize(240, 180));
|
|
225
|
+
SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void ReplaceText(const char* text)
|
|
229
|
+
{
|
|
230
|
+
if (strlen(text) > static_cast<size_t>(MaxBytes()))
|
|
231
|
+
throw std::runtime_error("TextArea supports at most 1 MiB of UTF-8 text");
|
|
232
|
+
silent = true;
|
|
233
|
+
try {
|
|
234
|
+
SetText(text);
|
|
235
|
+
} catch (...) {
|
|
236
|
+
silent = false;
|
|
237
|
+
throw;
|
|
238
|
+
}
|
|
239
|
+
silent = false;
|
|
240
|
+
Changed(); // Exactly one snapshot for a programmatic replacement.
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
class Window : public BWindow {
|
|
245
|
+
public:
|
|
246
|
+
Context* context;
|
|
247
|
+
int64_t id;
|
|
248
|
+
float initialWidth;
|
|
249
|
+
float initialHeight;
|
|
250
|
+
int64_t nextView = 1;
|
|
251
|
+
bool closePending = false;
|
|
252
|
+
std::map<int64_t, BView*> views;
|
|
253
|
+
|
|
254
|
+
Window(Context* c, int64_t n, const char* title, float width, float height, bool resize)
|
|
255
|
+
: BWindow(BRect(80, 80, 80 + width, 80 + height), title, B_TITLED_WINDOW,
|
|
256
|
+
B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
|
|
257
|
+
| (resize ? 0 : B_NOT_RESIZABLE | B_NOT_ZOOMABLE)), context(c), id(n),
|
|
258
|
+
initialWidth(width), initialHeight(height)
|
|
259
|
+
{
|
|
260
|
+
SetLayout(new BGroupLayout(B_VERTICAL, 0));
|
|
261
|
+
++context->live;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
~Window() override
|
|
265
|
+
{
|
|
266
|
+
context->callback(id, 0, "closed", "", 0);
|
|
267
|
+
if (--context->live == 0 && !context->destroying)
|
|
268
|
+
context->messenger.SendMessage(B_QUIT_REQUESTED);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
bool QuitRequested() override
|
|
272
|
+
{
|
|
273
|
+
// Decide on Python's dispatcher without holding this looper's lock.
|
|
274
|
+
// In particular, a synchronous BAlert::Go() must not run here.
|
|
275
|
+
if (!closePending) {
|
|
276
|
+
closePending = true;
|
|
277
|
+
context->callback(id, 0, "close_requested", "", 0);
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void MessageReceived(BMessage* message) override
|
|
283
|
+
{
|
|
284
|
+
if (message->what == kCloseAccepted) {
|
|
285
|
+
Quit(); // Already approved; do not ask QuitRequested a second time.
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (message->what != kControl) {
|
|
289
|
+
BWindow::MessageReceived(message);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
int64 viewID;
|
|
293
|
+
if (message->FindInt64("ren:id", &viewID) != B_OK)
|
|
294
|
+
return;
|
|
295
|
+
auto found = views.find(viewID);
|
|
296
|
+
if (found == views.end())
|
|
297
|
+
return;
|
|
298
|
+
BControl* control = dynamic_cast<BControl*>(found->second);
|
|
299
|
+
if (!control)
|
|
300
|
+
return;
|
|
301
|
+
BTextControl* text = dynamic_cast<BTextControl*>(control);
|
|
302
|
+
context->callback(id, viewID, dynamic_cast<BButton*>(control) ? "click" : "change",
|
|
303
|
+
text ? text->Text() : (control->Label() ? control->Label() : ""), control->Value());
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Lock through BMessenger, never dereference a raw pointer to a closed window.
|
|
308
|
+
class WindowLock {
|
|
309
|
+
public:
|
|
310
|
+
Window* window = nullptr;
|
|
311
|
+
WindowLock(Context* context, int64_t id)
|
|
312
|
+
{
|
|
313
|
+
BMessenger messenger;
|
|
314
|
+
{
|
|
315
|
+
std::lock_guard<std::mutex> lock(context->mutex);
|
|
316
|
+
auto found = context->windows.find(id);
|
|
317
|
+
if (found == context->windows.end())
|
|
318
|
+
throw std::runtime_error("Unknown window");
|
|
319
|
+
messenger = found->second;
|
|
320
|
+
}
|
|
321
|
+
check(messenger.LockTargetWithTimeout(5000000), "Window has closed or is busy");
|
|
322
|
+
BLooper* looper = nullptr;
|
|
323
|
+
messenger.Target(&looper);
|
|
324
|
+
window = dynamic_cast<Window*>(looper);
|
|
325
|
+
if (!window) {
|
|
326
|
+
if (looper) looper->Unlock();
|
|
327
|
+
throw std::runtime_error("Invalid window target");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
~WindowLock() { if (window) window->Unlock(); }
|
|
331
|
+
BView* view(int64_t id)
|
|
332
|
+
{
|
|
333
|
+
auto found = window->views.find(id);
|
|
334
|
+
if (found == window->views.end())
|
|
335
|
+
throw std::runtime_error("Unknown widget");
|
|
336
|
+
return found->second;
|
|
337
|
+
}
|
|
338
|
+
BControl* control(int64_t id)
|
|
339
|
+
{
|
|
340
|
+
auto control = dynamic_cast<BControl*>(view(id));
|
|
341
|
+
if (!control) throw std::runtime_error("Widget is not a control");
|
|
342
|
+
return control;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Receives a file panel's reply on its own looper while the caller waits.
|
|
347
|
+
class PanelReply : public BLooper {
|
|
348
|
+
public:
|
|
349
|
+
std::string path;
|
|
350
|
+
sem_id done;
|
|
351
|
+
|
|
352
|
+
PanelReply() : BLooper("ren_file_panel"), done(create_sem(0, "ren_file_panel")) {}
|
|
353
|
+
~PanelReply() override { delete_sem(done); }
|
|
354
|
+
|
|
355
|
+
void MessageReceived(BMessage* message) override
|
|
356
|
+
{
|
|
357
|
+
entry_ref ref;
|
|
358
|
+
const char* name;
|
|
359
|
+
switch (message->what) {
|
|
360
|
+
case B_REFS_RECEIVED:
|
|
361
|
+
if (message->FindRef("refs", &ref) == B_OK)
|
|
362
|
+
path = BPath(&ref).Path();
|
|
363
|
+
break;
|
|
364
|
+
case B_SAVE_REQUESTED:
|
|
365
|
+
if (message->FindRef("directory", &ref) == B_OK
|
|
366
|
+
&& message->FindString("name", &name) == B_OK) {
|
|
367
|
+
BPath target(&ref);
|
|
368
|
+
if (target.Append(name) == B_OK) path = target.Path();
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
case B_CANCEL:
|
|
372
|
+
// Sent whenever the panel hides, also after a selection.
|
|
373
|
+
release_sem(done);
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
BLooper::MessageReceived(message);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
BMessage* controlMessage(int64_t id)
|
|
382
|
+
{
|
|
383
|
+
auto message = new BMessage(kControl);
|
|
384
|
+
message->AddInt64("ren:id", id);
|
|
385
|
+
return message;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#define BEGIN_API error.clear(); try {
|
|
389
|
+
#define END_API(failure) } catch (const std::exception& e) { error = e.what(); } \
|
|
390
|
+
catch (...) { error = "Unknown native exception"; } return failure;
|
|
391
|
+
} // namespace
|
|
392
|
+
|
|
393
|
+
extern "C" {
|
|
394
|
+
int ren_abi() { return 5; }
|
|
395
|
+
const char* ren_error() { return error.c_str(); }
|
|
396
|
+
|
|
397
|
+
void* ren_app_create(const char* signature, Callback callback)
|
|
398
|
+
{
|
|
399
|
+
BEGIN_API
|
|
400
|
+
if (be_app) throw std::runtime_error("A BApplication already exists");
|
|
401
|
+
auto context = std::make_unique<Context>();
|
|
402
|
+
auto app = std::make_unique<Application>(signature, context.get());
|
|
403
|
+
check(app->InitCheck(), "BApplication");
|
|
404
|
+
context->app = app.release();
|
|
405
|
+
context->messenger = BMessenger(context->app);
|
|
406
|
+
context->callback = callback;
|
|
407
|
+
return context.release();
|
|
408
|
+
END_API(nullptr)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
int ren_app_run(Context* context)
|
|
412
|
+
{
|
|
413
|
+
BEGIN_API
|
|
414
|
+
check(context->app->Run(), "BApplication::Run");
|
|
415
|
+
return 0;
|
|
416
|
+
END_API(-1)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
int ren_app_quit(Context* context)
|
|
420
|
+
{
|
|
421
|
+
BEGIN_API
|
|
422
|
+
check(context->messenger.SendMessage(B_QUIT_REQUESTED), "Application quit");
|
|
423
|
+
return 0;
|
|
424
|
+
END_API(-1)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
int ren_app_destroy(Context* context)
|
|
428
|
+
{
|
|
429
|
+
BEGIN_API
|
|
430
|
+
context->destroying = true;
|
|
431
|
+
for (auto& entry : context->windows) {
|
|
432
|
+
if (entry.second.LockTarget()) {
|
|
433
|
+
BLooper* looper = nullptr;
|
|
434
|
+
entry.second.Target(&looper);
|
|
435
|
+
if (looper) looper->Quit(); // Deletes and unlocks the looper.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
delete context->app;
|
|
439
|
+
delete context;
|
|
440
|
+
return 0;
|
|
441
|
+
END_API(-1)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
int64_t ren_window_create(Context* context, const char* title, float width, float height, int resize)
|
|
445
|
+
{
|
|
446
|
+
BEGIN_API
|
|
447
|
+
std::lock_guard<std::mutex> lock(context->mutex);
|
|
448
|
+
int64_t id = context->nextWindow++;
|
|
449
|
+
auto window = new Window(context, id, title, width, height, resize);
|
|
450
|
+
context->windows.emplace(id, BMessenger(window));
|
|
451
|
+
// BLooper::Run starts the looper and releases its initial constructor lock.
|
|
452
|
+
// Starting hidden makes subsequent Show calls have ordinary locking rules.
|
|
453
|
+
thread_id thread = window->Run();
|
|
454
|
+
if (thread < B_OK) {
|
|
455
|
+
window->Quit();
|
|
456
|
+
check(thread, "BWindow::Run");
|
|
457
|
+
}
|
|
458
|
+
return id;
|
|
459
|
+
END_API(0)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
int ren_window_show(Context* context, int64_t id)
|
|
463
|
+
{
|
|
464
|
+
BEGIN_API
|
|
465
|
+
WindowLock lock(context, id);
|
|
466
|
+
if (lock.window->IsHidden()) {
|
|
467
|
+
// Auto size limits change while the Python tree is mounted one view
|
|
468
|
+
// at a time. Restore the requested size after the tree is complete.
|
|
469
|
+
lock.window->ResizeTo(lock.window->initialWidth, lock.window->initialHeight);
|
|
470
|
+
lock.window->CenterOnScreen();
|
|
471
|
+
lock.window->Show();
|
|
472
|
+
}
|
|
473
|
+
lock.window->Activate();
|
|
474
|
+
return 0;
|
|
475
|
+
END_API(-1)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
int ren_window_close(Context* context, int64_t id)
|
|
479
|
+
{
|
|
480
|
+
BEGIN_API
|
|
481
|
+
WindowLock lock(context, id);
|
|
482
|
+
check(lock.window->PostMessage(B_QUIT_REQUESTED), "Window close");
|
|
483
|
+
return 0;
|
|
484
|
+
END_API(-1)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
int ren_window_close_reply(Context* context, int64_t id, int allow)
|
|
488
|
+
{
|
|
489
|
+
BEGIN_API
|
|
490
|
+
WindowLock lock(context, id);
|
|
491
|
+
if (lock.window->closePending) {
|
|
492
|
+
if (allow)
|
|
493
|
+
check(lock.window->PostMessage(kCloseAccepted), "Approve window close");
|
|
494
|
+
else
|
|
495
|
+
lock.window->closePending = false;
|
|
496
|
+
}
|
|
497
|
+
return 0;
|
|
498
|
+
END_API(-1)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
int ren_window_title(Context* context, int64_t id, const char* title)
|
|
502
|
+
{
|
|
503
|
+
BEGIN_API
|
|
504
|
+
WindowLock lock(context, id);
|
|
505
|
+
lock.window->SetTitle(title);
|
|
506
|
+
return 0;
|
|
507
|
+
END_API(-1)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
int64_t ren_view_create(Context* context, int64_t wid, int64_t parent,
|
|
511
|
+
const char* kind, const char* text, const char* label, float a, float b)
|
|
512
|
+
{
|
|
513
|
+
BEGIN_API
|
|
514
|
+
WindowLock lock(context, wid);
|
|
515
|
+
BGroupLayout* layout = parent == 0
|
|
516
|
+
? dynamic_cast<BGroupLayout*>(lock.window->GetLayout())
|
|
517
|
+
: dynamic_cast<BGroupLayout*>(lock.view(parent)->GetLayout());
|
|
518
|
+
if (!layout) throw std::runtime_error("Parent is not a group layout");
|
|
519
|
+
int64_t id = lock.window->nextView++;
|
|
520
|
+
std::string name = "ren_" + std::to_string(id);
|
|
521
|
+
std::unique_ptr<BView> view;
|
|
522
|
+
BView* exposed = nullptr;
|
|
523
|
+
if (!strcmp(kind, "column") || !strcmp(kind, "row")) {
|
|
524
|
+
auto group = new BGroupView(name.c_str(), !strcmp(kind, "row") ? B_HORIZONTAL : B_VERTICAL, a);
|
|
525
|
+
group->GroupLayout()->SetInsets(b, b, b, b);
|
|
526
|
+
// Rows of fixed-size controls must not cap the containing window at
|
|
527
|
+
// their preferred size. Honor Window's requested size and resizing.
|
|
528
|
+
group->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED,
|
|
529
|
+
!strcmp(kind, "row") ? B_SIZE_UNSET : B_SIZE_UNLIMITED));
|
|
530
|
+
view.reset(group);
|
|
531
|
+
} else if (!strcmp(kind, "image")) {
|
|
532
|
+
view.reset(new ImageView(name.c_str(), text, a, b, label));
|
|
533
|
+
} else if (!strcmp(kind, "label")) {
|
|
534
|
+
view.reset(new BStringView(name.c_str(), text));
|
|
535
|
+
} else if (!strcmp(kind, "button")) {
|
|
536
|
+
view.reset(new BButton(name.c_str(), text, controlMessage(id)));
|
|
537
|
+
} else if (!strcmp(kind, "checkbox")) {
|
|
538
|
+
view.reset(new BCheckBox(name.c_str(), text, controlMessage(id)));
|
|
539
|
+
} else if (!strcmp(kind, "text")) {
|
|
540
|
+
auto input = new BTextControl(name.c_str(), label, text, controlMessage(id));
|
|
541
|
+
input->SetModificationMessage(controlMessage(id));
|
|
542
|
+
view.reset(input);
|
|
543
|
+
} else if (!strcmp(kind, "textarea")) {
|
|
544
|
+
auto input = std::make_unique<TextArea>(name.c_str(), context, wid, id, a != 0);
|
|
545
|
+
input->ReplaceText(text);
|
|
546
|
+
exposed = input.get();
|
|
547
|
+
// The scroll view owns its target, but public property access uses the
|
|
548
|
+
// inner text view. Both are destroyed with the containing window.
|
|
549
|
+
view.reset(new BScrollView((name + "_scroll").c_str(), input.get(),
|
|
550
|
+
uint32(0), a == 0, true, B_FANCY_BORDER));
|
|
551
|
+
input.release();
|
|
552
|
+
} else if (!strcmp(kind, "slider")) {
|
|
553
|
+
auto slider = new BSlider(name.c_str(), text, controlMessage(id), (int32)a, (int32)b, B_HORIZONTAL);
|
|
554
|
+
slider->SetModificationMessage(controlMessage(id));
|
|
555
|
+
view.reset(slider);
|
|
556
|
+
} else {
|
|
557
|
+
throw std::runtime_error("Unsupported widget type");
|
|
558
|
+
}
|
|
559
|
+
BView* raw = view.get();
|
|
560
|
+
if (!layout->AddView(raw)) throw std::runtime_error("Could not attach widget");
|
|
561
|
+
view.release(); // The window hierarchy now owns the view.
|
|
562
|
+
lock.window->views.emplace(id, exposed ? exposed : raw);
|
|
563
|
+
if (auto control = dynamic_cast<BControl*>(raw))
|
|
564
|
+
check(control->SetTarget(lock.window), "Control target");
|
|
565
|
+
return id;
|
|
566
|
+
END_API(0)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
int ren_image_source(Context* context, int64_t wid, int64_t id, const char* path)
|
|
570
|
+
{
|
|
571
|
+
BEGIN_API
|
|
572
|
+
WindowLock lock(context, wid);
|
|
573
|
+
auto image = dynamic_cast<ImageView*>(lock.view(id));
|
|
574
|
+
if (!image) throw std::runtime_error("Widget is not an image");
|
|
575
|
+
image->Replace(path);
|
|
576
|
+
return 0;
|
|
577
|
+
END_API(-1)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
int ren_button_icon(Context* context, int64_t wid, int64_t id, const char* path, int size)
|
|
581
|
+
{
|
|
582
|
+
BEGIN_API
|
|
583
|
+
if (size < 1 || size > 256) throw std::runtime_error("Invalid button icon size");
|
|
584
|
+
WindowLock lock(context, wid);
|
|
585
|
+
auto button = dynamic_cast<BButton*>(lock.view(id));
|
|
586
|
+
if (!button) throw std::runtime_error("Widget is not a button");
|
|
587
|
+
auto bitmap = path[0] ? buttonBitmap(path, size) : std::unique_ptr<BBitmap>();
|
|
588
|
+
// BButton copies the bitmap and supplies its native disabled/pressed states.
|
|
589
|
+
check(button->SetIcon(bitmap.get()), "Set button icon");
|
|
590
|
+
return 0;
|
|
591
|
+
END_API(-1)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
int ren_view_set_text(Context* context, int64_t wid, int64_t id, const char* text)
|
|
595
|
+
{
|
|
596
|
+
BEGIN_API
|
|
597
|
+
WindowLock lock(context, wid);
|
|
598
|
+
BView* view = lock.view(id);
|
|
599
|
+
if (auto area = dynamic_cast<TextArea*>(view)) area->ReplaceText(text);
|
|
600
|
+
else if (auto input = dynamic_cast<BTextControl*>(view)) input->SetText(text);
|
|
601
|
+
else if (auto label = dynamic_cast<BStringView*>(view)) label->SetText(text);
|
|
602
|
+
else if (auto control = dynamic_cast<BControl*>(view)) control->SetLabel(text);
|
|
603
|
+
else throw std::runtime_error("Widget has no text");
|
|
604
|
+
return 0;
|
|
605
|
+
END_API(-1)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const char* ren_view_text(Context* context, int64_t wid, int64_t id)
|
|
609
|
+
{
|
|
610
|
+
BEGIN_API
|
|
611
|
+
WindowLock lock(context, wid);
|
|
612
|
+
BView* view = lock.view(id);
|
|
613
|
+
if (auto area = dynamic_cast<TextArea*>(view)) result = area->Text();
|
|
614
|
+
else if (auto input = dynamic_cast<BTextControl*>(view)) result = input->Text();
|
|
615
|
+
else if (auto label = dynamic_cast<BStringView*>(view)) result = label->Text();
|
|
616
|
+
else if (auto control = dynamic_cast<BControl*>(view)) result = control->Label() ? control->Label() : "";
|
|
617
|
+
else throw std::runtime_error("Widget has no text");
|
|
618
|
+
return result.c_str();
|
|
619
|
+
END_API(nullptr)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
int ren_view_set_value(Context* context, int64_t wid, int64_t id, int value)
|
|
623
|
+
{
|
|
624
|
+
BEGIN_API
|
|
625
|
+
WindowLock lock(context, wid);
|
|
626
|
+
lock.control(id)->SetValue(value);
|
|
627
|
+
return 0;
|
|
628
|
+
END_API(-1)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
int ren_view_value(Context* context, int64_t wid, int64_t id, int* value)
|
|
632
|
+
{
|
|
633
|
+
BEGIN_API
|
|
634
|
+
WindowLock lock(context, wid);
|
|
635
|
+
*value = lock.control(id)->Value();
|
|
636
|
+
return 0;
|
|
637
|
+
END_API(-1)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
int ren_view_enabled(Context* context, int64_t wid, int64_t id, int enabled)
|
|
641
|
+
{
|
|
642
|
+
BEGIN_API
|
|
643
|
+
WindowLock lock(context, wid);
|
|
644
|
+
if (auto area = dynamic_cast<TextArea*>(lock.view(id))) area->MakeEditable(enabled != 0);
|
|
645
|
+
else lock.control(id)->SetEnabled(enabled != 0);
|
|
646
|
+
return 0;
|
|
647
|
+
END_API(-1)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
int ren_view_invoke(Context* context, int64_t wid, int64_t id)
|
|
651
|
+
{
|
|
652
|
+
BEGIN_API
|
|
653
|
+
WindowLock lock(context, wid);
|
|
654
|
+
auto control = lock.control(id);
|
|
655
|
+
if (control->IsEnabled()) check(control->Invoke(), "Control invocation");
|
|
656
|
+
return 0;
|
|
657
|
+
END_API(-1)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
int ren_alert(const char* title, const char* text)
|
|
661
|
+
{
|
|
662
|
+
BEGIN_API
|
|
663
|
+
(new BAlert(title, text, "OK"))->Go();
|
|
664
|
+
return 0;
|
|
665
|
+
END_API(-1)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
int ren_confirm(const char* title, const char* text, const char* accept, const char* cancel)
|
|
669
|
+
{
|
|
670
|
+
BEGIN_API
|
|
671
|
+
auto dialog = new BAlert(title, text, cancel, accept);
|
|
672
|
+
dialog->SetShortcut(0, B_ESCAPE);
|
|
673
|
+
return dialog->Go() == 1 ? 1 : 0;
|
|
674
|
+
END_API(-1)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const char* ren_file_panel(int save, const char* title, const char* directory, const char* name)
|
|
678
|
+
{
|
|
679
|
+
BEGIN_API
|
|
680
|
+
result.clear();
|
|
681
|
+
entry_ref start;
|
|
682
|
+
if (directory[0]) check(get_ref_for_path(directory, &start), "File panel directory");
|
|
683
|
+
auto reply = new PanelReply();
|
|
684
|
+
if (reply->done < B_OK || reply->Run() < B_OK) {
|
|
685
|
+
reply->Quit(); // Still holds its constructor lock.
|
|
686
|
+
throw std::runtime_error("Cannot start file panel");
|
|
687
|
+
}
|
|
688
|
+
{
|
|
689
|
+
BMessenger target(reply);
|
|
690
|
+
BFilePanel panel(save ? B_SAVE_PANEL : B_OPEN_PANEL, &target, directory[0] ? &start : nullptr,
|
|
691
|
+
B_FILE_NODE, false, nullptr, nullptr, true, true);
|
|
692
|
+
if (save && name[0]) panel.SetSaveText(name);
|
|
693
|
+
if (title[0] && panel.Window()->Lock()) {
|
|
694
|
+
panel.Window()->SetTitle(title);
|
|
695
|
+
panel.Window()->Unlock();
|
|
696
|
+
}
|
|
697
|
+
panel.Show();
|
|
698
|
+
// The panel is modal; wait here without holding any looper lock.
|
|
699
|
+
while (acquire_sem(reply->done) == B_INTERRUPTED) {}
|
|
700
|
+
}
|
|
701
|
+
if (reply->Lock()) {
|
|
702
|
+
result = reply->path;
|
|
703
|
+
reply->Quit();
|
|
704
|
+
}
|
|
705
|
+
return result.c_str();
|
|
706
|
+
END_API(nullptr)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
int ren_clipboard_set(const char* text)
|
|
710
|
+
{
|
|
711
|
+
BEGIN_API
|
|
712
|
+
if (!be_clipboard->Lock()) throw std::runtime_error("Clipboard is busy");
|
|
713
|
+
status_t status = be_clipboard->Clear();
|
|
714
|
+
if (status == B_OK)
|
|
715
|
+
status = be_clipboard->Data()->AddData("text/plain", B_MIME_TYPE, text, strlen(text));
|
|
716
|
+
if (status == B_OK) status = be_clipboard->Commit();
|
|
717
|
+
be_clipboard->Unlock();
|
|
718
|
+
check(status, "Clipboard write");
|
|
719
|
+
return 0;
|
|
720
|
+
END_API(-1)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const char* ren_clipboard_get()
|
|
724
|
+
{
|
|
725
|
+
BEGIN_API
|
|
726
|
+
if (!be_clipboard->Lock()) throw std::runtime_error("Clipboard is busy");
|
|
727
|
+
const void* data = nullptr;
|
|
728
|
+
ssize_t size = 0;
|
|
729
|
+
status_t status = be_clipboard->Data()->FindData("text/plain", B_MIME_TYPE, &data, &size);
|
|
730
|
+
try {
|
|
731
|
+
result.assign(status == B_OK ? static_cast<const char*>(data) : "", status == B_OK ? size : 0);
|
|
732
|
+
} catch (...) {
|
|
733
|
+
be_clipboard->Unlock();
|
|
734
|
+
throw;
|
|
735
|
+
}
|
|
736
|
+
be_clipboard->Unlock();
|
|
737
|
+
return result.c_str();
|
|
738
|
+
END_API(nullptr)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
int ren_launch(const char* url)
|
|
742
|
+
{
|
|
743
|
+
BEGIN_API
|
|
744
|
+
std::string scheme(url);
|
|
745
|
+
scheme.resize(scheme.find(':'));
|
|
746
|
+
for (char& c : scheme) if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
|
747
|
+
std::string mime = "application/x-vnd.Be.URL." + scheme;
|
|
748
|
+
const char* arguments[] = {url};
|
|
749
|
+
check(be_roster->Launch(mime.c_str(), 1, arguments), "BRoster::Launch");
|
|
750
|
+
return 0;
|
|
751
|
+
END_API(-1)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const char* ren_directory(int which)
|
|
755
|
+
{
|
|
756
|
+
BEGIN_API
|
|
757
|
+
if (which != 0) throw std::runtime_error("Unsupported directory");
|
|
758
|
+
BPath path;
|
|
759
|
+
check(find_directory(B_USER_SETTINGS_DIRECTORY, &path, true), "find_directory");
|
|
760
|
+
result = path.Path();
|
|
761
|
+
return result.c_str();
|
|
762
|
+
END_API(nullptr)
|
|
763
|
+
}
|
|
764
|
+
} // extern C
|