/
gtkctl.py
564 lines (462 loc) · 17.2 KB
/
gtkctl.py
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# Copyright (c) 2008 Gareth Latty
# Copyright (c) 2009 Sebastian Bartos
# See COPYING for details
"""
This module contains the GTK+ GUI subsystem of AniChou. It sets up the
user interface and enters the main event loop.
MODULE GLOBALS
==============
They are not nice, but the best in this situation. This file has two globals
defined in the gtkctl class constructor:
- MODCTL: pointer to the gtkctl class instance.
- WIDGETS: pointer to the widgets wrapper.
"""
import os, gtk, gtk.glade, gobject, webbrowser, datetime
import globs, data, players, recognizinig
def sb_push(msg):
WIDGETS['bottom_statusbar'].push(-1, msg)
def sb_clear():
"""
Clear the status bar.
This is a callback for glib.timeout_add which is executed until it returns
False, the first time after the interval specified.
"""
# Not necessary to sleep here, in fact that would lock up the GUI.
WIDGETS['bottom_statusbar'].pop(-1)
try:
WIDGETS['bottom_statusbar'].pop(-1)
except:
pass
return False
class glade_handlers(object):
"""
We put all of our glade gtk signal handlers into this class.
This lets us bind all of them at once, because their names are in the class
dict. If you want to know where these signals are assigned, then take a look
at the data/ui.glade file (glade XML).
"""
def gtk_main_quit(event):
gtk.main_quit()
def on_button_ac_clicked(event):
webbrowser.open('http://myanimelist.net/clubs.php?cid=10642', 2)
def on_button_mal_clicked(event):
webbrowser.open('http://myanimelist.net', 2)
def on_button_sync_clicked(event):
sb_push('Syncing with MyAnimeList server..')
gtk.main_iteration()
if MODCTL.anime_data.sync():
MODCTL.update_from_db_all()
WIDGETS['bottom_statusbar'].pop(-1)
sb_push('Syncing done..')
else:
WIDGETS['bottom_statusbar'].pop(-1)
sb_push('Sync failed..')
# Clear the status bar in five seconds.
gobject.timeout_add(5000, sb_clear)
def on_playbar_toggled(event):
if not INIT:
if WIDGETS['statusbar_now_playing'].flags() & gtk.VISIBLE:
WIDGETS['statusbar_now_playing'].hide()
else:
WIDGETS['statusbar_now_playing'].show()
def on_about(event):
WIDGETS['aboutdialog'].show_all()
def on_about_close(widget, event):
WIDGETS['aboutdialog'].hide_all()
return True
def on_menuitem_prefs_activate(event):
WIDGETS['preferences'].show_all()
def on_prefs_close(widget=None, event=None):
WIDGETS['preferences'].hide_all()
new_name = WIDGETS['entry_maluser'].get_text()
new_pw = WIDGETS['entry_malpasswd'].get_text()
new_path = WIDGETS['entry_searchdir'].get_text()
MODCTL.cfg.set('mal', 'username', new_name)
MODCTL.cfg.set('mal', 'password', new_pw)
MODCTL.cfg.set('search_dir', 'dir1', new_path)
MODCTL.cfg.save()
MODCTL.anime_data.username = new_name
MODCTL.anime_data.password = new_pw
return True
def sync_on_start_toggled(event):
if not INIT:
old = MODCTL.cfg.get('startup', 'sync', True)
new = not old
MODCTL.cfg.set('startup', 'sync', new )
WIDGETS['sync_on_start'].set_active(new)
MODCTL.cfg.save()
def tracker_on_start_toggled(event):
if not INIT:
old = MODCTL.cfg.get('startup', 'tracker', True)
new = not old
MODCTL.cfg.set('startup', 'tracker', new )
WIDGETS['playtracker_on_start'].set_active(new)
MODCTL.cfg.save()
class widget_wrapper(object):
"""
Load and set up the glade user interface and connect the signal hanlers.
Provide a convenient way to access the glade widgets.
"""
def __init__(self):
""" Load user interface and connect signal handlers. """
self.widgets = \
gtk.glade.XML(
os.path.join(globs.ac_package_path, 'data', 'ui.glade'))
self.widgets.signal_autoconnect(glade_handlers.__dict__)
def __getitem__(self, key):
""" Make widgets accessable by name.
It's simply done by overriding the aquisiton default of the class. Makes
referencing widgets by name much more convinient:
EXAMPLE
=======
To reference a widget by name (based on the glade file) and perform a
GTK action with it you use:
widgets['widget_name'].action()
"""
return self.widgets.get_widget(key)
class list_treeview(gtk.TreeView):
""" This is one of the two more interesting classes in the GUI subsystem.
It is used to create and control the anime list treeview's (the big table in
the middle that shows anime enries). It also handles manual editing on the
table via callbacks, which are called when an episode, status or score entry
is edited (slow double click on one of these enries).
"""
def __init__(self, tab_id):
""" Initialize the treeview.
Call the parent constructor, store some references, define some class
constants, add columns to the treeview and glue this together.
The exciting part is the clomuns schemata setup, which tells the columns
how they look like and what they should do when edited (connect
callbacks).
ARGUMENTS
=========
- tab_id: MAL schema based (data.STATUS) tab id (watching, etc..)
PROPERTIES
==========
- liststore: probably the most interseting property, as it stores the
data of the table. It can be accessed by index, like
liststore[row][column], both starting with 0.
- col: reference to the tree view columns the class has. Not really
interesting outside init, but may come in handy for plugin
development.
"""
# Call parrent constructor
gtk.TreeView.__init__(self)
# Mal tab type id
self.tab_id = tab_id
self.data = {}
# This one is used to keep the db keys addresseble with the row indices
self.keylist = list()
# Some treeview specific constants (column id's)
( self.NAME, self.EPISODE, self.STATUS, self.SCORE, self.PROGRESS ) = \
range (5)
# Add columns to self and store references by name
self.col = dict()
for colname in ['Title', 'Episodes', 'Status', 'Score', 'Progress']:
self.col[colname] = gtk.TreeViewColumn(colname)
self.append_column(self.col[colname])
## Set up the column schemata
# Title column schema
titlecell = gtk.CellRendererText()
self.col['Title'].pack_start(titlecell, True)
self.col['Title'].add_attribute(titlecell, 'text', 0)
# Episode column schema
# Editable spin column that is connected with a callback
epcell = gtk.CellRendererSpin()
epcell.set_property("editable", True)
adjustment = gtk.Adjustment(0, 0, 999, 1)
epcell.set_property("adjustment", adjustment)
epcell.connect('edited', self.cell_episode_edited)
self.col['Episodes'].pack_start(epcell, False)
self.col['Episodes'].add_attribute(epcell, 'text', 1)
# Status column schema
# Combo box column with selectable status
# The first part contains the choices
combomodel = gtk.ListStore(str)
combomodel.append(['Watching'])
combomodel.append(['On Hold'])
combomodel.append(['Completed'])
combomodel.append(['Plan to Watch'])
combomodel.append(['Dropped'])
statuscell = gtk.CellRendererCombo()
statuscell.set_property('model', combomodel)
statuscell.set_property('has-entry', False)
statuscell.set_property('editable', True)
statuscell.set_property('text-column', 0)
statuscell.connect('edited', self.cell_status_edited)
self.col['Status'].pack_start(statuscell, False)
self.col['Status'].add_attribute(statuscell, 'text', 2)
# Score column schema
# Basically the same as the episodes one.
scorecell = gtk.CellRendererSpin()
scorecell.set_property("editable", True)
adjustment = gtk.Adjustment(0, 0, 10, 1)
scorecell.set_property("adjustment", adjustment)
scorecell.connect('edited', self.cell_score_edited)
self.col['Score'].pack_start(scorecell, False)
self.col['Score'].add_attribute(scorecell, 'text', 3)
# Progress column schema
# Progress bar, nothing fancy
progresscell = gtk.CellRendererProgress()
self.col['Progress'].pack_start(progresscell, True)
self.col['Progress'].add_attribute(progresscell, 'value', 4)
## Create liststore model (table containing the treeview data) and hook
# it up to the treeview (self).
self.liststore = gtk.ListStore(str, str, str, int, int)
self.set_model(self.liststore)
def repopulate(self):
""" Resets/Initializes data to liststore (data table)
INPUT
=====
- self.tv_data: the update is performed with this data
"""
# clear previous data
self.liststore.clear()
# comupte and add new data based on self.data
for key, anime in self.data.items():
# Extract series title
name = anime['series_title']
name = name.replace(''', '\'')
# Extract episodes/max and construct display string
if anime['series_episodes']:
max_episodes = anime['series_episodes']
else:
max_episodes = '-'
current_episode = anime['my_watched_episodes']
epstr = str(current_episode) + ' / ' + str(max_episodes)
# Calculate progress bar
progress = 0
if isinstance(max_episodes, int):
try:
progress = \
int(float(current_episode) / float(max_episodes) * 100)
except:
progress = 0
# Extract score
score = anime['my_score']
# Construct row list and add it to the liststore
row = [name, epstr, data.STATUSB[self.tab_id], score, progress]
self.liststore.append(row)
# Store key in row position
self.keylist.append(key)
def cell_score_edited(self, spinr, row, value):
""" Handles editing / change of score cells.
Not too much here, only database update.
"""
maxvalue = 10
if int(value) <= maxvalue and \
int(value) != self.liststore[row][self.SCORE]:
self.liststore[row][self.SCORE] = int(value)
self.push_change_to_db(row, {'my_score': int(value)})
def cell_status_edited(self, combr, row, value):
""" Handles selection of status combo cells.
Pushes the entry in other categories and eventually updates the episode
number (from non-complete to complete -> maximize my_episodes)
"""
# We don't move to self?
if value != self.liststore[row][self.STATUS]:
# Prepare data
maxepisodes = self.liststore[row][self.EPISODE].split(' / ')[1]
self.liststore[row][self.STATUS] = value
if value == data.STATUSB[data.COMPLETED]:
# We move to the comleted tab?
self.liststore[row][self.PROGRESS] = 100
self.liststore[row][self.EPISODE] = \
maxepisodes + ' / ' + maxepisodes
self.push_change_to_db(row, {
'my_watched_episodes': int(maxepisodes),
'my_status': data.COMPLETED })
else:
# we move to another tab?
self.push_change_to_db(row, {
'my_status': data.STATUS_REV[value] })
# Move row
MODCTL.tv[data.STATUS_REV[value]].liststore.append(
self.liststore[row])
del self.liststore[row]
def cell_episode_edited(self, spinr, row, value):
""" Handles the modification of the episode number spin button.
If all a valid new value is entered, the row is updated and parrent
update function is called to update the local database.
If maximal episode is entered as new value, the enry is pushed to the
completed table.
If entry is in the completed table and the ep count is lowered, it is
pushed in the watching table.
"""
# Prepare data set
oldstr = self.liststore[row][self.EPISODE].replace('-','0')
(old, max) = oldstr.split(' / ')
oldvalue = int(old)
maxvalue = int(max)
newvalue = int(value)
# Determine if action is required
if newvalue != oldvalue and \
(newvalue <= maxvalue or maxvalue == 0):
# Compute new common row data
newstr = str(newvalue) + ' / ' + str(maxvalue)
self.liststore[row][self.EPISODE] = newstr
self.liststore[row][self.PROGRESS] = \
int(float(newvalue) / float(maxvalue) * 100)
# Stuff to be done with smaller than max ep count
if newvalue < maxvalue:
# In the completd table
if self.tab_id == data.COMPLETED:
self.push_change_to_db(row,
{'my_watched_episodes': newvalue,
'my_status': data.WATCHING })
self.liststore[row][self.STATUS] = \
data.STATUSB[data.WATCHING]
MODCTL.tv[data.WATCHING].liststore.append(
self.liststore[row])
del self.liststore[row]
# In all other tables that are not complete
else:
self.push_change_to_db(row,
{'my_watched_episodes': newvalue})
# Did we reach treashhold and was not completed?
if self.tab_id != data.COMPLETED and newvalue == maxvalue:
# Send to compted
self.push_change_to_db(row,
{'my_watched_episodes': newvalue,
'my_status': data.COMPLETED})
self.liststore[row][self.STATUS] = \
data.STATUSB[data.COMPLETED]
MODCTL.tv[data.COMPLETED].liststore.append(
self.liststore[row])
del self.liststore[row]
def push_change_to_db(self, row, changes):
""" Shorthand for pusing changes to the anime database
It's called from the treeview callbacks to commit a local change to the
local anime database.
ARGUMENTS
=========
- row: the row we are in, which is used to look up the keyname we want
to change
- changes: dict with changes that have to be done
"""
# set new values in database
for key, value in changes.items():
MODCTL.anime_data.db[self.keylist[int(row)]][key] = value
# set update timestamp
MODCTL.anime_data.db[self.keylist[int(row)]]\
['my_last_updated'] = datetime.datetime.now()
# save changes in local database
MODCTL.anime_data.save()
class guictl(object):
""" GUI control interface class.
This class starts up and controls the general user interface. Simply
initialize it with a config and anime_data reference pointer and a shiny GTK
GUI will pop up in the middle of your screen (if all goes well).
"""
def __init__(self, the_data, config):
"""
Load interface and enter main loop.
ARGUMENTS
=========
- config: reference to config.ac_config instance
- data: reference to myanimelist.anime_data instance
"""
global INIT
INIT = True
# Hook to make the conrol module reachable from all over the
# file, especially from the autoconnect handlers.
global MODCTL
MODCTL = self
# Store the references to the config and data instances
self.cfg = config
self.anime_data = the_data
# Initialize base widgets from XML and connect signal handlers
# Set widget hook
global WIDGETS
WIDGETS = widget_wrapper()
# Initialize treeviews
self.tv = dict()
for tab_id, name in data.STATUS.items():
tv = list_treeview(tab_id)
self.tv[tab_id] = tv
WIDGETS['scrolledwindow_' + name].add(tv)
# Check if we need to sync, and sync
if self.cfg.get('startup', 'sync'):
self.anime_data.sync()
self.update_from_db_all()
# Set preferences dialog from config
WIDGETS['entry_maluser'].set_text(
self.cfg.get('mal','username', True))
WIDGETS['entry_malpasswd'].set_text(
self.cfg.get('mal','password', True))
WIDGETS['sync_on_start'].set_active(
self.cfg.get('startup', 'sync', True))
WIDGETS['playtracker_on_start'].set_active(
self.cfg.get('startup','tracker', True))
WIDGETS['entry_searchdir'].set_text(self.cfg.get('search_dir', 'dir1'))
## Show main window, connect the quit signal handler and hide the
# now_playing statusbar
WIDGETS['main_window'].show_all()
WIDGETS['main_window'].connect('delete_event', lambda e,w:
gtk.main_quit())
if not self.cfg.get('startup', 'tracker'):
WIDGETS['statusbar_now_playing'].hide()
else:
WIDGETS['menuitem_playbar'].set_active(True)
#gobject.timeout_add(5000, self.idle_cb)
gobject.timeout_add(500, self.idle_cb)
INIT = False
# Run main loop
gtk.main()
def idle_cb(self):
if WIDGETS['statusbar_now_playing'].flags() & gtk.VISIBLE:
track = players.get_playing(
['mplayer', 'totem'], [self.cfg.get('search_dir', 'dir1')])
for key in track.keys():
e = recognizinig.engine(key, self.anime_data.db)
m = e.match()
try:
ep = int(e._getEpisode().strip('/'))
if m:
#print m, ep
if self.anime_data.db[m]['my_watched_episodes'] == \
ep - 1 and \
self.anime_data.db[m]['my_status'] == \
data.WATCHING:
msg = 'Playing: ' + m + ' -- Episode: ' + str(ep)
WIDGETS['statusbar_now_playing'].push(-1, msg)
#print self.anime_data.db[m]['my_watched_episodes']
#print self.anime_data.db[m]['series_episodes']
if ep < self.anime_data.db[m]['series_episodes']:
self.anime_data.db[m]['my_watched_episodes'] = ep
MODCTL.anime_data.db[m]\
['my_last_updated'] = datetime.datetime.now()
MODCTL.anime_data.save()
newep = str(ep) + ' / ' + str(self.anime_data.db[m]['series_episodes'])
sw = self.tv[1].keylist
i = 0 # row tracker
for key in sw:
if key == m:
break
else:
i += 1
self.tv[1].liststore[str(i)][1] = newep
except:
break
return True
def update_from_db_all(self):
""" Update all anime tables views from database.
This is used on initialization and after syncronization.
"""
# We copy (references to) data from anime_data.db into
# list_treeview.tb and from there into liststore.
# Might be better to give anime_data a facade to act
# as TreeModel.
# Remove old data
for tab in self.tv.values():
tab.data = {}
# Just changing self.tv[].data is not enough, as after deletion on
# the site a sync will remove keys from anime_data.db and we have
# to mirror that.
# Separate anime data according to their status
for key, value in self.anime_data.db.items():
status = value['my_status']
self.tv[status].data[key] = value
# Populate tree views
for tab in self.tv.values():
tab.repopulate()