-
Notifications
You must be signed in to change notification settings - Fork 0
/
window.pike
361 lines (334 loc) · 13.2 KB
/
window.pike
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
class gtksignal(object obj, mixed ... args)
{
int signal_id;
protected void create() {if (obj) signal_id=obj->signal_connect(@args);}
protected void destroy() {if (obj && signal_id) obj->signal_disconnect(signal_id);}
protected void _destruct() {if (obj && signal_id) obj->signal_disconnect(signal_id);}
}
class MessageBox
{
inherit GTK2.MessageDialog;
function callback;
//flags: Normally 0. type: 0 for info, else GTK2.MESSAGE_ERROR or similar. buttons: GTK2.BUTTONS_OK etc.
protected void create(int flags,int type,int buttons,string message,GTK2.Window parent,function|void cb,mixed|void cb_arg)
{
callback=cb;
::create(flags,type,buttons,message,parent);
signal_connect("response", (function)response, cb_arg);
show();
}
void response(object self,int button,mixed cb_arg)
{
if (self->destroy) self->destroy();
if (callback) callback(button,cb_arg);
destruct();
}
}
GTK2.Table two_column(array(array|string|GTK2.Widget) contents) {
contents /= 2;
GTK2.Table tb=GTK2.Table(sizeof(contents[0]),sizeof(contents),0);
foreach (contents;int y;array(string|GTK2.Widget) row) foreach (row;int x;string|GTK2.Widget obj) if (obj)
{
int opt=0;
if (stringp(obj)) {obj=GTK2.Label((["xalign": 1.0, "label":obj])); opt=GTK2.Fill;}
int xend=x+1; while (xend<sizeof(row) && !row[xend]) ++xend; //Span cols by putting 0 after the element
tb->attach(obj,x,xend,y,y+1,opt,opt,1,1);
}
return tb;
}
class window
{
constant provides="window";
constant windowtitle = "Window";
mapping(string:mixed) win=([]);
//Replace this and call the original after assigning to win->mainwindow.
void makewindow() {if (win->accelgroup) win->mainwindow->add_accel_group(win->accelgroup);}
//Stock item creation: Close button. Calls closewindow(), same as clicking the cross does.
GTK2.Button stock_close()
{
return win->stock_close=GTK2.Button((["use-stock":1,"label":GTK2.STOCK_CLOSE]));
}
//Subclasses should call ::dosignals() and then append to to win->signals. This is the
//only place where win->signals is reset. Note that it's perfectly legitimate to have
//nulls in the array, as exploited here.
void dosignals()
{
//NOTE: This does *not* use += here - this is where we (re)initialize the array.
win->signals = ({
gtksignal(win->mainwindow,"delete_event",closewindow),
win->stock_close && gtksignal(win->stock_close,"clicked",closewindow),
});
collect_signals("sig_", win);
}
//NOTE: prefix *must* be a single 'word' followed by an underscore. Stuff breaks otherwise.
void collect_signals(string prefix, mapping(string:mixed) searchme,mixed|void arg)
{
foreach (indices(this),string key) if (has_prefix(key,prefix) && callablep(this[key]))
{
//Function names of format sig_x_y become a signal handler for win->x signal y.
//(Note that classes are callable, so they can be used as signal handlers too.)
//This may pose problems, as it's possible for x and y to have underscores in
//them, so we scan along and find the shortest such name that exists in win[].
//If there's none, ignore the callable (currently without any error or warning,
//despite the explicit prefix). This can create ambiguities, but only in really
//contrived situations, so I'm deciding not to care. :)
array parts=(key/"_")[1..];
int b4=(parts[0]=="b4"); if (b4) parts=parts[1..]; //sig_b4_some_object_some_signal will connect _before_ the normal action
for (int i=0;i<sizeof(parts)-1;++i) if (mixed obj=searchme[parts[..i]*"_"])
{
if (objectp(obj) && callablep(obj->signal_connect))
{
win->signals+=({gtksignal(obj,parts[i+1..]*"_",this[key],arg,UNDEFINED,b4)});
break;
}
}
}
}
protected void create(string|void name)
{
if (name) sscanf(explode_path(name)[-1],"%s.pike",name);
if (name) {if (G->G->windows[name]) win=G->G->windows[name]; else G->G->windows[name]=win;}
win->self=this;
if (!win->mainwindow) win->mainwindow = GTK2.Window((["title": windowtitle]));
else win->mainwindow->remove(win->mainwindow->get_child());
makewindow();
win->mainwindow->show_all();
dosignals();
}
int closewindow()
{
if (win->mainwindow->destroy) win->mainwindow->destroy();
destruct(win->mainwindow);
return 1;
}
}
class menu_item
{
//Provide:
constant menu_label=0; //(string) The initial label for your menu item.
void menu_clicked() { }
//End provide.
GTK2.MenuItem make_menu_item() {return GTK2.MenuItem(menu_label);} //Override if customization is required
protected void create(string|void name)
{
#if !constant(HEADLESS)
if (!name) return;
sscanf(explode_path(name)[-1],"%s.pike",name);
if (object old=G->G->menuitems[name]) {({old->destroy})(); destruct(old);}
object mi = make_menu_item();
G->G->windows->mainwindow->optmenu->add(mi->show());
mi->signal_connect("activate",menu_clicked);
G->G->menuitems[name] = mi;
#endif
}
}
class ircsettings
{
inherit window;
constant windowtitle = "Authenticate StilleBot";
mapping config = G->G->dbsettings->credentials | G->G->instance_config;
void makewindow()
{
win->mainwindow->add(two_column(({
"Twitch user name", win->username=GTK2.Entry()->set_size_request(400, -1)->set_text(config->username||""),
"Real name (optional)", win->display_name=GTK2.Entry()->set_size_request(400, -1)->set_text(config->display_name||""),
"Client ID (optional)", win->clientid=GTK2.Entry()->set_size_request(400, -1)->set_text(config->clientid||""),
"Client Secret (optional)", win->clientsecret=GTK2.Entry()->set_size_request(400, -1)->set_visibility(0),
"OAuth2 token", win->token=GTK2.Entry()->set_size_request(400, -1)->set_visibility(0),
GTK2.Label("Keys will not be shown above. Obtain"),0,
GTK2.Label("one from twitchapps and paste it in."),0,
"Web config address (optional)", win->http_address=GTK2.Entry()->set_size_request(400, -1)->set_text(config->http_address||""),
"Begin with https:// for an encrypted service - see README", 0,
"Listen address/port (advanced)", win->listen_address=GTK2.Entry()->set_size_request(400, -1)->set_text(config->listen_address||""),
GTK2.HbuttonBox()
->add(win->save=GTK2.Button("Save"))
->add(stock_close())
,0
})));
}
void sig_save_clicked()
{
mapping c = G->G->dbsettings->credentials | ([
"username": win->username->get_text(),
"display_name": win->display_name->get_text(),
]);
G->G->instance_config->clientid = win->clientid->get_text();
string secret = win->clientsecret->get_text();
if (secret != "") G->G->instance_config->clientsecret = secret;
string token = win->token->get_text();
if (token != "") c->token = token;
string address = win->http_address->get_text();
if (!sscanf(address, "http%*[s]://%s", string addr) || !addr)
address = "http://" + address;
if (has_suffix(address, "/")) address = address[..<1]; //Strip trailing slash
G->G->instance_config->http_address = address;
G->G->instance_config->listen_address = win->listen_address->get_text();
Stdio.write_file("instance-config.json", Standards.JSON.encode(G->G->instance_config, 7));
werror("Saving to DB.\n");
spawn_task(G->G->DB->query_rw("update stillebot.settings set credentials = :c",
(["c": c]))); //TODO: On moving back to Sql.Sql, check JSON encoding.
closewindow();
}
}
object mainwindow;
class _mainwindow
{
inherit window;
constant windowtitle = "StilleBot";
protected void create() {
::create("mainwindow");
mainwindow = win->mainwindow;
}
//Return the keyword of the selected item, or 0 if none (or new) is selected
string selecteditem()
{
[object iter,object store]=win->sel->get_selected();
string login = iter && store->get_value(iter, 0);
return (login != "-- New --") && login; //TODO: Recognize the "New" entry by something other than its text
}
void sig_pb_save_clicked()
{
string login = selecteditem();
if (!login) { //Connect to new channel
login = win->login->get_text();
if (login == "" || login == "-- New --") return; //Invalid names
connect_to_channel(login);
return;
}
object channel = G->G->irc->channels["#" + login]; if (!channel) return; //TODO: Report error?
channel->botconfig->connprio = (int)win->connprio->get_text();
channel->botconfig->chatlog = (int)win->chatlog->get_active();
channel->botconfig_save();
call_out(sig_sel_changed, 0);
}
void sig_pb_delete_clicked()
{
[object iter,object store]=win->sel->get_selected();
string login = iter && store->get_value(iter, 0);
if (!login || login == "-- New --") return;
store->remove(iter);
object channel = G->G->irc->channels["#" + login];
if (channel) channel->remove_bot_from_channel();
}
void sig_sel_changed()
{
string login = selecteditem();
mapping cfg = G->G->irc->channels["#" + login]->?config || ([]);
win->login->set_text(login || "");
win->display_name->set_text(cfg->display_name || "");
win->connprio->set_text((string)cfg->connprio);
win->chatlog->set_active((int)cfg->chatlog);
}
void makewindow()
{
win->mainwindow->add(GTK2.Vbox(0,10)
->pack_start(GTK2.MenuBar()
->add((object)GTK2.MenuItem("_Options")->set_submenu(win->optmenu=GTK2.Menu()
->add((object)(win->update=GTK2.MenuItem("Update (developer mode)")))
->add((object)(win->updatemodules=GTK2.MenuItem("Update modules (developer mode)")))
->add((object)(win->manual_auth=GTK2.MenuItem("Authenticate manually")))
)),0,0,0)
->add(GTK2.Hbox(0,5)
->add(GTK2.ScrolledWindow()->add(
win->list = GTK2.TreeView(win->ls = GTK2.ListStore(({"string", "string"})))
->append_column(GTK2.TreeViewColumn("Login", GTK2.CellRendererText(), "text", 0))
->append_column(GTK2.TreeViewColumn("ID", GTK2.CellRendererText(), "text", 1))
)->set_policy(GTK2.POLICY_NEVER, GTK2.POLICY_AUTOMATIC))
->add(GTK2.Vbox(0,0)
->add(two_column(({
"Channel", win->login = GTK2.Entry(),
"Displays as", win->display_name = GTK2.Label(),
0, win->chatlog = GTK2.CheckButton("Log chat to console"),
"Connection priority", win->connprio = GTK2.Entry(),
})))
->pack_end(GTK2.HbuttonBox()
->add(win->pb_save=GTK2.Button((["label":"_Save","use-underline":1])))
->add(win->pb_refresh=GTK2.Button((["label":"_Refresh","use-underline":1])))
->add(win->pb_delete=GTK2.Button((["label":"_Delete","use-underline":1,"sensitive":1])))
,0,0,0)
)
)
);
win->sel=win->list->get_selection();
on_irc_loaded(this, "window::mainwindow", "sig_pb_refresh_clicked");
object iter = win->ls->append();
win->ls->set_value(iter, 0, "... loading ...");
win->ls->set_value(iter, 1, "... userid ...");
sig_sel_changed();
::makewindow();
}
void sig_pb_refresh_clicked() {
win->ls->clear();
array channels = values(G->G->irc->id); sort(channels->login, channels);
foreach (channels, object channel) {
object iter = win->ls->append();
win->ls->set_value(iter, 0, channel->login || "...");
win->ls->set_value(iter, 1, (string)channel->userid);
}
win->ls->set_value(win->new_iter = win->ls->append(), 0, "-- New --");
win->sel->select_iter(win->new_iter);
}
void sig_login_changed(object self)
{
string txt = self->get_text();
string lc = lower_case(txt);
if (lc != txt) self->set_text(lc);
}
void sig_update_activate(object self)
{
//object main = G->bootstrap("stillebot.pike"); //Test new bootstrap code
//int err = main ? main->bootstrap_all() : 1;
G->G->warnings = 0;
int err = G->bootstrap_all(); //Normally the current bootstrap code is fine.
gc();
if (!err && !G->G->warnings) return; //All OK? Be silent.
if (string winid = getenv("WINDOWID")) //On some Linux systems we can pop the console up.
catch (Process.create_process(({"wmctrl", "-ia", winid}))->wait()); //Try, but don't mind errors, eg if wmctrl isn't installed.
MessageBox(0, GTK2.MESSAGE_ERROR, GTK2.BUTTONS_OK, err + " compilation error(s), " + G->G->warnings + " warning(s) - see console", win->mainwindow);
}
void sig_updatemodules_activate(object self)
{
G->G->warnings = 0;
int err = 0;
foreach (sort(get_dir("modules")), string f)
if (has_suffix(f, ".pike")) err += !G->bootstrap("modules/" + f);
foreach (sort(get_dir("modules/http")), string f)
if (has_suffix(f, ".pike")) err += !G->bootstrap("modules/http/" + f);
foreach (sort(get_dir("zz_local")), string f)
if (has_suffix(f, ".pike")) err += !G->bootstrap("zz_local/" + f);
gc();
if (!err && !G->G->warnings) return; //All OK? Be silent.
if (string winid = getenv("WINDOWID")) //On some Linux systems we can pop the console up.
catch (Process.create_process(({"wmctrl", "-ia", winid}))->wait()); //Try, but don't mind errors, eg if wmctrl isn't installed.
MessageBox(0, GTK2.MESSAGE_ERROR, GTK2.BUTTONS_OK, err + " compilation error(s), " + G->G->warnings + " warning(s) - see console", win->mainwindow);
}
void sig_manual_auth_activate() {ircsettings();}
void closewindow() {exit(0);}
}
protected void create(string name)
{
add_constant("window", window);
add_constant("menu_item", menu_item);
#if constant(HEADLESS)
//In headless mode, make the window and menu_item inheritables available,
//but don't actually initialize any GUI.
G->G->windows = ([]);
G->G->menuitems = ([]);
return;
#endif
if (!G->G->windows)
{
//First time initialization
G->G->windows = ([]);
GTK2.setup_gtk();
}
G->G->window = this;
if (G->G->menuitems)
{
array mi = values(G->G->menuitems);
mi->destroy();
destruct(mi[*]);
}
G->G->menuitems = ([]);
_mainwindow();
}