PageRenderTime 197ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 1ms

/progenitus/editor/gui.py

https://github.com/TheGurke/Progenitus
Python | 1366 lines | 1365 code | 0 blank | 1 comment | 5 complexity | 40dbdcf80ae905df4b3348553d4a247c MD5 | raw file
  1. # Written by TheGurke 2011
  2. """GUI for the deck editor"""
  3. import os
  4. import sqlite3
  5. import re
  6. import shutil
  7. import subprocess
  8. from gettext import gettext as _
  9. import logging
  10. import glib
  11. import gio
  12. import gtk
  13. import pango
  14. from progenitus import *
  15. from progenitus.db import cards
  16. from progenitus.db import pics
  17. import decks
  18. _query_new_in_set = ('"setname" = ? AND "name" IN '
  19. '(SELECT "name" FROM "cards" WHERE "setname" = ? EXCEPT '
  20. 'SELECT "name" FROM "cards" WHERE "releasedate" < '
  21. '(SELECT "releasedate" FROM "cards" WHERE "setname" = ? LIMIT 1))'
  22. )
  23. class Interface(uiloader.Interface):
  24. isfullscreen = False
  25. _enlarged_card = None
  26. _select_active = True
  27. def __init__(self):
  28. super(self.__class__, self).__init__()
  29. self.load(config.GTKBUILDER_DECKEDITOR)
  30. self.main_win.set_title(config.APP_NAME_EDITOR)
  31. self.main_win.maximize()
  32. self.textview_deckdesc.get_buffer().connect("changed",
  33. self.deckdesc_changed)
  34. self.quicksearch_entry.grab_focus()
  35. self.cardview.get_model().set_sort_func(3, self.sort_by_type, 3)
  36. self.resultview.get_model().set_sort_func(3, self.sort_by_type, 3)
  37. self.cardview.get_model().set_sort_func(2, self.sort_by_cost, 2)
  38. self.resultview.get_model().set_sort_func(2, self.sort_by_cost, 2)
  39. self.treestore_files.set_sort_func(2, self.sort_files)
  40. self.treestore_files.set_sort_column_id(2, gtk.SORT_ASCENDING)
  41. self.cardview.get_model().set_sort_column_id(3, gtk.SORT_ASCENDING)
  42. self.resultview.get_model().set_sort_column_id(10, gtk.SORT_DESCENDING)
  43. gtk.quit_add(0, self.save_deck) # one extra decksave just to be sure
  44. # Render folder and deck icons
  45. self._folder_icon = self.main_win.render_icon(gtk.STOCK_DIRECTORY,
  46. gtk.ICON_SIZE_MENU, None)
  47. self._deck_icon = self.main_win.render_icon(gtk.STOCK_FILE,
  48. gtk.ICON_SIZE_MENU, None)
  49. # Check if the database is accessable
  50. db_file = os.path.join(settings.cache_dir, config.DB_FILE)
  51. if not os.path.exists(db_file):
  52. self.warn_about_empty_db()
  53. return
  54. cards.connect()
  55. num = cards.count()
  56. if num == 0:
  57. self.warn_about_empty_db()
  58. return
  59. else:
  60. self.label_results.set_text("%d cards available" % num)
  61. # Create deck directory if it doesn't exist
  62. if not os.path.exists(settings.deck_dir):
  63. os.mkdir(settings.deck_dir)
  64. if os.name == 'posix':
  65. os.symlink(os.path.abspath(config.DEFAULT_DECKS_PATH),
  66. os.path.join(settings.deck_dir, _("default decks")))
  67. # Initialize the file view
  68. async.start(self._update_dir(settings.deck_dir))
  69. self._create_monitor(settings.deck_dir)
  70. # Initialize the quicksearch autocompletion
  71. async.start(self.init_qs_autocomplete())
  72. def init_qs_autocomplete(self):
  73. """Initialize the quicksearch entry autocompletion"""
  74. completion = gtk.EntryCompletion()
  75. completion.set_model(self.liststore_qs_autocomplete)
  76. completion.set_property("text-column", 0)
  77. completion.set_inline_completion(False)
  78. completion.set_minimum_key_length(3)
  79. completion.set_popup_set_width(False)
  80. completion.connect("match-selected", self.qs_autocomplete_pick)
  81. renderer = gtk.CellRendererText()
  82. completion.pack_start(renderer, True)
  83. completion.set_attributes(renderer, markup=1)
  84. descrenderer = gtk.CellRendererText()
  85. completion.pack_end(descrenderer)
  86. self.quicksearch_entry.set_completion(completion)
  87. # Populate quicksearch autocomplete
  88. for setname in cards.sets:
  89. desc1 = setname + " <span size=\"x-small\">" \
  90. "(Card set - all in set)</span>"
  91. desc2 = setname + " <span size=\"x-small\">" \
  92. "(Card set - new in that set)</span>"
  93. self.liststore_qs_autocomplete.append((setname, desc1,
  94. '"setname" = ?', setname))
  95. self.liststore_qs_autocomplete.append((setname, desc2,
  96. _query_new_in_set, setname))
  97. if not settings.save_ram:
  98. # Because it requires a lot of RAM, the card and card type
  99. # autocomplete feature is not available in the reduced RAM mode
  100. subtypes = dict()
  101. for card in cards.cards:
  102. for subtype in card.subtype.split(" "):
  103. yield
  104. if subtype in subtypes:
  105. subtypes[subtype] += 1
  106. else:
  107. subtypes[subtype] = 1
  108. for subtype in subtypes:
  109. if subtypes[subtype] >= 3:
  110. # Only use subtypes that occur more than 3 times on cards
  111. desc = (subtype +
  112. " <span size=\"x-small\">(Creature type)</span>")
  113. self.liststore_qs_autocomplete.append((subtype, desc,
  114. '"subtype" LIKE ?', "%" + subtype + "%"))
  115. cardnames = yield set(card.name for card in cards.cards)
  116. for cardname in cardnames:
  117. card = yield cards.find_by_name(cardname)[0]
  118. desc = card.name + " <span size=\"x-small\">" + card.cardtype
  119. if card.subtype != "":
  120. desc += " - " + card.subtype
  121. if card.manacost != "":
  122. desc += " (%s)" % card.manacost
  123. desc += "</span>"
  124. self.liststore_qs_autocomplete.append((cardname, desc,
  125. '"name" = ?', card.name))
  126. #
  127. # Interface callbacks
  128. #
  129. def warn_about_empty_db(self):
  130. """Display a warning that there are no cards in the database"""
  131. dialog = self.show_dialog(self.main_win,
  132. _("The card database is empty. Starting the updater."), "warning")
  133. dialog.connect("destroy", self.quit)
  134. dialog.connect("destroy", self.run_updater)
  135. def show_about(self, widget):
  136. """Display information about this program"""
  137. dialog = gtk.AboutDialog()
  138. dialog.set_name(config.APP_NAME_EDITOR)
  139. dialog.set_version(str(config.VERSION))
  140. dialog.set_copyright(_("Copyright by TheGurke 2011-2012"))
  141. dialog.set_website(config.APP_WEBSITE)
  142. dialog.set_comments(_("This program is Free Software by the GPL3."))
  143. dialog.run()
  144. dialog.destroy()
  145. def run_updater(self, *args):
  146. """Start the updater program"""
  147. subprocess.Popen(["python", "progenitus.py", "--updater"])
  148. # os.spawnlp(os.P_NOWAIT, "python", "python",
  149. # "progenitus.py", "--updater")
  150. def select_all(self, widget, event):
  151. """Selects all text in an entry"""
  152. if isinstance(widget, gtk.Editable):
  153. widget.select_region(0, -1)
  154. def searchview_keypress(self, widget, event):
  155. """A key has been pressed on the searchview"""
  156. if event.type == gtk.gdk.KEY_PRESS:
  157. if event.keyval == 65379: # insert, shift for sideboard
  158. if self.deck is not None:
  159. cardid = self.get_selected_result()
  160. self.add_to_deck(cardid, event.state & gtk.gdk.SHIFT_MASK)
  161. def cardview_keypress(self, widget, event):
  162. """A key has been pressed on the cardview"""
  163. if event.type == gtk.gdk.KEY_PRESS:
  164. cardid, sb, removed = self.get_selected_card()
  165. if event.keyval == 65535: # delete
  166. if cardid is not None and not removed:
  167. self.remove_from_deck(cardid, sb)
  168. if event.keyval == 65379: # insert, shift for sideboard
  169. if cardid is not None:
  170. self.add_to_deck(cardid, event.state & gtk.gdk.SHIFT_MASK)
  171. # if event.keyval == ord('c') and event.state & gtk.gdk.CONTROL_MASK:
  172. # c = gtk.Clipboard()
  173. # card = cards.get(cardid)
  174. # c.set_text("%s (%s)" % (card.name, card.setname))
  175. # if event.keyval == ord('v') and event.state & gtk.gdk.CONTROL_MASK:
  176. # c = gtk.Clipboard()
  177. # text = c.wait_for_text()
  178. # match = re.match(r'(.+?) \(([^)]+)\)', text)
  179. # if match is not None:
  180. # cardname, setname = match.groups()
  181. # l = cards.find_by_name(cardname, setname)
  182. # if l != []:
  183. # card = l[0]
  184. # self.add_to_deck(card.id, False)
  185. def keypress(self, widget, event):
  186. """Global keypress handler"""
  187. if event.type == gtk.gdk.KEY_PRESS:
  188. if event.keyval == ord('f') and event.state & gtk.gdk.CONTROL_MASK:
  189. self.quicksearch_entry.grab_focus()
  190. if event.keyval == ord('q') and event.state & gtk.gdk.CONTROL_MASK:
  191. self.show_extended_search(None)
  192. if event.keyval == ord('n') and event.state & gtk.gdk.CONTROL_MASK:
  193. self.new_deck()
  194. if event.keyval == ord('s') and event.state & gtk.gdk.CONTROL_MASK:
  195. self.export_deck()
  196. if event.keyval == ord('C') and event.state & gtk.gdk.CONTROL_MASK:
  197. self.clear_search(None)
  198. if event.keyval == ord('e') and event.state & gtk.gdk.CONTROL_MASK:
  199. self.edit_deck(None)
  200. if event.keyval == 65480: # F11
  201. self.toggle_fullscreen(None)
  202. def toggle_fullscreen(self, widget):
  203. """Change the fullscreen state"""
  204. if self.isfullscreen:
  205. self.main_win.unfullscreen()
  206. else:
  207. self.main_win.fullscreen()
  208. self.isfullscreen = not self.isfullscreen
  209. def qs_autocomplete_pick(self, widget, model, it):
  210. """Picked a suggested autocompletion item"""
  211. row = model[it]
  212. self._execute_search(row[2], (row[3],) * row[2].count("?"))
  213. def show_extended_search(self, widget):
  214. """Clicked on the extended search button"""
  215. self.notebook_search.set_current_page(1)
  216. self.entry_text.grab_focus()
  217. def show_custom_search(self, widget):
  218. """Clicked on the custom search button"""
  219. self.notebook_search.set_current_page(2)
  220. def more_results(self, widget):
  221. """Get more results to the previously executed search query"""
  222. self._show_results(cards.more_results())
  223. def sqlquery_keypress(self, widget, event):
  224. """Keypress on the textview_sqlquery"""
  225. if event.type == gtk.gdk.KEY_PRESS and event.keyval == 65293 and \
  226. event.state & gtk.gdk.SHIFT_MASK: # shift + enter
  227. self.execute_custom_search(self.textview_sqlquery)
  228. return True
  229. #
  230. # Sort functions
  231. #
  232. def sort_files(self, model, it1, it2):
  233. """Sort the files first by type (folder or file) and then by name"""
  234. isdir1, name1 = model.get(it1, 0, 2)
  235. isdir2, name2 = model.get(it2, 0, 2)
  236. if isdir1 == isdir2:
  237. return cmp(name1, name2)
  238. else:
  239. return -cmp(isdir1, isdir2)
  240. def sort_by_type(self, model, it1, it2, column):
  241. """Sort function for the resultview/cardview"""
  242. types = ["Plainswalker", "Creature", "Enchantment", "Artifact",
  243. "Instant", "Sorcery", "Land", ""]
  244. v1 = model.get_value(it1, column)
  245. v1 = "" if v1 is None else v1
  246. v2 = model.get_value(it2, column)
  247. v2 = "" if v2 is None else v2
  248. i, j = 0, 0
  249. while v1.find(types[i]) < 0:
  250. i += 1
  251. while v2.find(types[j]) < 0:
  252. j += 1
  253. if i != j:
  254. return cmp(i, j)
  255. n1 = model.get_value(it1, 1) # name of card at it1
  256. n1 = "" if n1 is None else n1
  257. n2 = model.get_value(it2, 1) # name of card at it2
  258. n2 = "" if n2 is None else n2
  259. return cmp(n1, n2)
  260. def sort_by_cost(self, model, it1, it2, column):
  261. """Sort function for the resultview/cardview"""
  262. v1 = model.get_value(it1, column)
  263. v2 = model.get_value(it2, column)
  264. t1 = model.get_value(it1, 3) # type of card at it1
  265. t1 = "" if t1 is None else t1
  266. t2 = model.get_value(it2, 3) # type of card at it2
  267. t2 = "" if t2 is None else t2
  268. # Lands sort last
  269. c1 = 1000 if t1.find("Land") >= 0 else cards.convert_mana(v1)
  270. c2 = 1000 if t2.find("Land") >= 0 else cards.convert_mana(v2)
  271. if c1 == c2:
  272. return cmp(v1[::-1], v2[::-1])
  273. return cmp(c1, c2)
  274. #
  275. # Preferences
  276. #
  277. def show_preferences(self, widget):
  278. """Show the program's preferences"""
  279. self.filechooserbutton_cache.set_filename(settings.cache_dir)
  280. self.filechooserbutton_decks.set_filename(settings.deck_dir)
  281. self.filechooserbutton_replays.set_filename(settings.replay_dir)
  282. self.checkbutton_save_ram.set_active(settings.save_ram)
  283. #self.spinbutton_decksave_interval.set_value(settings.decksave_timeout
  284. # / 1000)
  285. self.notebook_search.set_current_page(5)
  286. def save_preferences(self, widget, nothing=None):
  287. """Save the changed settings to disk"""
  288. #settings.decksave_timeout = \
  289. # int(self.spinbutton_decksave_interval.get_value()) * 1000
  290. settings.save_ram = self.checkbutton_save_ram.get_active()
  291. new_cache_dir = unicode(self.filechooserbutton_cache.get_filename())
  292. if new_cache_dir != "None":
  293. settings.cache_dir = new_cache_dir
  294. new_replay_dir = unicode(self.filechooserbutton_replays.get_filename())
  295. if new_replay_dir != "None":
  296. settings.replay_dir = new_replay_dir
  297. old_deck_dir = settings.deck_dir
  298. new_deck_dir = unicode(self.filechooserbutton_decks.get_filename())
  299. if new_deck_dir != "None" and new_deck_dir != old_deck_dir:
  300. settings.deck_dir = new_deck_dir
  301. self.treestore_files.clear()
  302. async.start(self.refresh_files())
  303. settings.save()
  304. logging.info(_("Settings saved."))
  305. #
  306. # Deck files and folders
  307. #
  308. _it_by_path = dict()
  309. _filemonitors = dict()
  310. _folder_icon = None
  311. _deck_icon = None
  312. def _create_monitor(self, path):
  313. """Create a file monitor for a directory"""
  314. logging.debug(_("Monitoring '%s' for updates."), path)
  315. filemonitor = gio.File(path).monitor_directory()
  316. filemonitor.connect("changed", self.update_files)
  317. self._filemonitors[path] = filemonitor
  318. def _expand_dirs(self, path):
  319. """Extract a list of folders from a path"""
  320. l = []
  321. while path != "":
  322. l.append(path)
  323. path, name = os.path.split(path)
  324. l.reverse()
  325. return l
  326. def _get_path(self, it):
  327. """Derive the file path from the tree structure"""
  328. assert(it is not None)
  329. isdir, path, name = self.treestore_files.get(it, 0, 1, 2)
  330. path = name + ("" if isdir else config.DECKFILE_SUFFIX)
  331. while it is not None:
  332. it = self.treestore_files.iter_parent(it)
  333. if it is not None:
  334. os.path.join(self.treestore_files.get_value(it, 2), path)
  335. path = os.path.join(settings.deck_dir, path)
  336. return path
  337. def _update_dir(self, path):
  338. """Recursively add a directory to the files view"""
  339. assert(path == settings.deck_dir or path in self._it_by_path)
  340. for filename in os.listdir(path):
  341. yield self._add_file(os.path.join(path, filename))
  342. def _add_file(self, path):
  343. """Add a path to the file view"""
  344. root, filename = os.path.split(path)
  345. suffix = config.DECKFILE_SUFFIX
  346. if os.path.isfile(path) and filename[-len(suffix):] != suffix:
  347. return # ignore non-deck files
  348. it_root = self._it_by_path.get(root, None)
  349. # File already in the tree?
  350. it = self.treestore_files.iter_children(it_root)
  351. while it is not None:
  352. if self.treestore_files.get_value(it, 1) == path:
  353. break # entry found
  354. it = self.treestore_files.iter_next(it)
  355. else:
  356. if os.path.isdir(path):
  357. self._it_by_path[path] = self.treestore_files.append(it_root,
  358. (True, path, filename, self._folder_icon))
  359. self._create_monitor(path) # Monitor subfolder for changes
  360. async.start(self._update_dir(path))
  361. return self._it_by_path[path]
  362. else:
  363. name = decks.Deck("").derive_name(path)
  364. return self.treestore_files.append(it_root,
  365. (False, path, name, self._deck_icon))
  366. def _remove_file(self, path):
  367. """Remove a path from the file view"""
  368. root, filename = os.path.split(path)
  369. it_root = self._it_by_path.get(root, None)
  370. it = self.treestore_files.iter_children(it_root)
  371. while it is not None:
  372. if self.treestore_files.get_value(it, 1) == path:
  373. isdir = self.treestore_files.get_value(it, 0)
  374. self.treestore_files.remove(it)
  375. if isdir:
  376. del self._it_by_path[path]
  377. del self._filemonitors[path]
  378. break
  379. it = self.treestore_files.iter_next(it)
  380. else:
  381. logging.debug(_("Recieved a file delete event for '%s', "
  382. "but the file was not found in the files view."), path)
  383. def update_files(self, filemonitor, gfile1, gfile2, event):
  384. """Filemonitor callback if something changed in the deck dir"""
  385. if event == gio.FILE_MONITOR_EVENT_CREATED:
  386. self._add_file(gfile1.get_path())
  387. if event == gio.FILE_MONITOR_EVENT_DELETED:
  388. self._remove_file(gfile1.get_path())
  389. def move_deckorfolder(self, model, modelpath, it):
  390. """Moved a deck or folder in the decklistview using drag and drop"""
  391. # This is also triggered by the insertions from refresh_files()
  392. assert(model is self.treestore_files)
  393. # # Check if row is fully populated
  394. # isdir, path, name = model.get(it, 0, 1, 2)
  395. # if name is None:
  396. # return
  397. #
  398. # # Calculate the new path
  399. # it_parent = model.iter_parent(it)
  400. # while it_parent is not None and not model.get_value(it_parent, 0):
  401. # it_parent = model.iter_parent(it_parent)
  402. # new_dirname = model.get_value(it_parent, 1)
  403. ## if it_parent is None:
  404. ## new_dirname = settings.deck_dir
  405. ## else:
  406. ## new_dirname = self._get_path(it_parent)
  407. # new_path = os.path.join(new_dirname, name +
  408. # ("" if isdir else config.DECKFILE_SUFFIX))
  409. #
  410. # # Check if file/folder needs to be moved
  411. # if new_path != path:
  412. # # File/folder has been moved
  413. # try:
  414. # pass
  415. # print "rename", path, new_path
  416. ## os.rename(path, new_path)
  417. # except:
  418. # # TODO: undo move in the treemodel
  419. # raise
  420. def rename_file(self, cellrenderer, modelpath, new_name):
  421. """Renamed a file or folder in treeview_files"""
  422. it = self.treestore_files.get_iter(modelpath)
  423. isdir, old_path, old_name = self.treestore_files.get(it, 0, 1, 2)
  424. if old_name == new_name:
  425. return
  426. new_path = os.path.join(os.path.dirname(old_path), new_name)
  427. if not isdir:
  428. new_path += config.DECKFILE_SUFFIX
  429. if os.path.exists(new_path):
  430. self.show_dialog(self.main_win,
  431. (_("Cannot rename '%s' to '%s': a file with that name exists.")
  432. if os.path.isfile(new_path) else
  433. _("Cannot rename '%s' to '%s': a folder with that name "
  434. "exists.")) % (old_name, new_name), 'error')
  435. try:
  436. self._select_active = False
  437. os.rename(old_path, new_path)
  438. self.treestore_files.set(it, 1, new_path, 2, new_name)
  439. if isdir:
  440. del self._it_by_path[old_path]
  441. self._it_by_path[new_path] = it
  442. # remove old filemonitors
  443. to_remove = []
  444. for path, monitor in self._filemonitors.items():
  445. if path.startswith(old_path):
  446. to_remove.append(path)
  447. for path in to_remove:
  448. del self._filemonitors[path]
  449. self._create_monitor(new_path)
  450. it = self.treestore_files.iter_children(it)
  451. while it is not None:
  452. if not self.treestore_files.remove(it):
  453. it = None
  454. async.start(self._update_dir(new_path))
  455. elif self.deck.filename == old_path:
  456. self.deck.filename = new_path
  457. self.deck.name = self.deck.derive_name()
  458. except OSError as e:
  459. logging.warning(_("Could not rename '%s' to '%s': %s"), old_path,
  460. new_path, str(e))
  461. self.show_dialog(self.main_win, _("Cannot rename '%s' to '%s': %s.")
  462. % (old_name, new_name, str(e)), 'error')
  463. finally:
  464. self._select_active = True
  465. def remove_file(self, *args):
  466. """Delete the currently selected file or directory"""
  467. model, it = self.treeview_files.get_selection().get_selected()
  468. if it is None:
  469. return
  470. isdir, path = model.get(it, 0, 1)
  471. if not isdir and self.deck is not None:
  472. modified = (len(self.deck.decklist) > 0 or
  473. len(self.deck.sideboard) > 0 or self.deck.description != "")
  474. if modified:
  475. deckname = self.deck.name
  476. text = (_("Are you sure you want to delete the deck '%s'?\n" +
  477. "(This cannot be undone.)")) % deckname
  478. md = gtk.MessageDialog(self.main_win,
  479. gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING,
  480. gtk.BUTTONS_YES_NO, text)
  481. md.set_default_response(gtk.RESPONSE_NO)
  482. result = md.run()
  483. md.destroy()
  484. if not modified or result == gtk.RESPONSE_YES:
  485. filename = self.deck.filename
  486. self.treestore_files.remove(it)
  487. self.unload_deck()
  488. os.remove(filename)
  489. if isdir:
  490. isempty = len(os.listdir(path)) == 0
  491. if not isempty:
  492. text = ((_("Are you sure you want to delete the folder '%s'" +
  493. " and all of its content?\n(This cannot be undone.)"))
  494. % path)
  495. md = gtk.MessageDialog(self.main_win,
  496. gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING,
  497. gtk.BUTTONS_YES_NO, text)
  498. md.set_default_response(gtk.RESPONSE_NO)
  499. result = md.run()
  500. md.destroy()
  501. if isempty or result == gtk.RESPONSE_YES:
  502. shutil.rmtree(path)
  503. self.treestore_files.remove(it)
  504. self.unload_deck()
  505. def new_folder(self, *args):
  506. """Create a new subfolder"""
  507. model, it = self.treeview_files.get_selection().get_selected()
  508. if it is None:
  509. isdir = True
  510. path = settings.deck_dir
  511. else:
  512. isdir, path = model.get(it, 0, 1)
  513. root = path if isdir else os.path.dirname(path)
  514. name = _("new folder")
  515. i = 1
  516. while os.path.exists(os.path.join(root, name)):
  517. i += 1
  518. name = _("new folder (%d)") % i
  519. path = os.path.join(root, name)
  520. os.mkdir(path)
  521. it = self._add_file(path)
  522. self.treeview_files.expand_to_path(self.treestore_files.get_path(it))
  523. self.treeview_files.get_selection().select_iter(it)
  524. def select_file(self, widget):
  525. if not self._select_active:
  526. return
  527. model, it = self.treeview_files.get_selection().get_selected()
  528. if it is None:
  529. self.unload_deck()
  530. self.toolbutton_delete_deck.set_sensitive(False)
  531. return
  532. isdir, path = model.get(it, 0, 1)
  533. if isdir:
  534. self.unload_deck()
  535. else:
  536. assert(os.path.isfile(path))
  537. self.load_deck(path)
  538. self.toolbutton_delete_deck.set_sensitive(True)
  539. #
  540. # Deck save/load and display
  541. #
  542. deck = None
  543. _deck_load_async_handle = None
  544. _is_loading = False
  545. _waiting_for_decksave = False
  546. def enable_deck(self):
  547. """Make all deck-related widgets sensitive"""
  548. # Opposite: unload_deck
  549. self.cards.clear()
  550. self._is_loading = True
  551. self.entry_author.set_text(self.deck.author)
  552. self.textview_deckdesc.get_buffer().set_text(self.deck.description)
  553. self.cardview.set_sensitive(True)
  554. self.entry_author.set_sensitive(True)
  555. self.textview_deckdesc.set_sensitive(True)
  556. self.toolbutton_copy_deck.set_sensitive(True)
  557. self.toolbutton_delete_deck.set_sensitive(True)
  558. self.toolbutton_export_deck.set_sensitive(True)
  559. self.toolbutton_deckedit.set_sensitive(True)
  560. self.toolbutton_stats.set_sensitive(True)
  561. self.toolbutton_search_lands.set_sensitive(True)
  562. self._is_loading = False
  563. self.cardview.grab_focus()
  564. def unload_deck(self):
  565. """Unload the current deck"""
  566. # Opposite: enable_deck
  567. if self._deck_load_async_handle is not None:
  568. # Currently loading a deck
  569. self._deck_load_async_handle.cancel()
  570. self._deck_load_async_handle = None
  571. if self._waiting_for_decksave:
  572. self.save_deck()
  573. self.deck = None
  574. self.cardview.set_sensitive(False)
  575. self.cards.clear()
  576. self.entry_author.set_text("")
  577. self.textview_deckdesc.get_buffer().set_text("")
  578. self.toolbutton_copy_deck.set_sensitive(False)
  579. self.toolbutton_delete_deck.set_sensitive(False)
  580. self.toolbutton_export_deck.set_sensitive(False)
  581. self.toolbutton_deckedit.set_sensitive(False)
  582. self.toolbutton_stats.set_sensitive(False)
  583. self.toolbutton_search_lands.set_sensitive(False)
  584. self.entry_author.set_sensitive(False)
  585. self.textview_deckdesc.set_sensitive(False)
  586. for c in ["white", "blue", "black", "red", "green"]:
  587. getattr(self, "mana_" + c).hide()
  588. self.update_cardcount()
  589. def refresh_deck(self):
  590. """Refresh the deck card list"""
  591. if self.deck is None:
  592. return
  593. self.cards.clear()
  594. for sb in [True, False]:
  595. l = self.deck.sideboard if sb else self.deck.decklist
  596. for card in l:
  597. self.cards.append((card.id, card.name, card.manacost,
  598. card.get_composed_type(), card.power, card.toughness,
  599. card.rarity[0], card.setname, sb, False, card.price,
  600. _price_to_text(card.price), card.releasedate))
  601. self.update_cardcount()
  602. def new_deck(self, *args):
  603. """Create a new empty deck"""
  604. self.unload_deck()
  605. # Find the parent directory
  606. model, it = self.treeview_files.get_selection().get_selected()
  607. while it is not None and not self.treestore_files.get_value(it, 0):
  608. it = model.iter_parent(it)
  609. if it is None:
  610. parent_dir = settings.deck_dir
  611. else:
  612. parent_dir = self.treestore_files.get_value(it, 1)
  613. # Find the new file name
  614. name = _("new deck")
  615. path = os.path.join(parent_dir, name + config.DECKFILE_SUFFIX)
  616. i = 2
  617. while os.path.exists(path):
  618. name = _("new deck (%d)") % i
  619. path = os.path.join(parent_dir, name + config.DECKFILE_SUFFIX)
  620. i += 1
  621. logging.info(_("New deck: %s"), name)
  622. # Enter the deck to the decks treestore
  623. icon = self.main_win.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU,
  624. None)
  625. it = self.treestore_files.append(it, (False, path, name, icon))
  626. # Initialize deck
  627. self.deck = decks.Deck(path)
  628. self.deck.save()
  629. self.enable_deck()
  630. # Set focus back to the treeview_files
  631. self._select_active = False
  632. self.treeview_files.grab_focus()
  633. self.treeview_files.expand_to_path(model.get_path(it))
  634. self.treeview_files.set_cursor(model.get_path(it),
  635. focus_column=self.treeviewcolumn9, start_editing=True)
  636. self._select_active = True
  637. def copy_deck(self, *args):
  638. """Copy the currently selected deck"""
  639. if self.deck is not None:
  640. icon = self.main_win.render_icon(gtk.STOCK_FILE,
  641. gtk.ICON_SIZE_MENU, None)
  642. new_name = self.deck.name + _(" (copy)")
  643. filename = os.path.join(os.path.dirname(self.deck.filename),
  644. new_name + config.DECKFILE_SUFFIX)
  645. i = 2
  646. while os.path.exists(filename):
  647. new_name = self.deck.name + (_(" (copy %d)") % i)
  648. filename = os.path.join(os.path.dirname(self.deck.filename),
  649. new_name + config.DECKFILE_SUFFIX)
  650. i += 1
  651. self.deck.name = new_name
  652. self.deck.filename = filename
  653. it = self.treeview_files.get_selection().get_selected()[1]
  654. if it is None:
  655. return # no deck selected
  656. parent = self.treestore_files.iter_parent(it)
  657. it = self.treestore_files.insert_after(parent, it,
  658. (False, self.deck.filename, self.deck.name, icon))
  659. self.treeview_files.set_cursor(self.treestore_files.get_path(it))
  660. self._waiting_for_decksave = True
  661. self.save_deck() # save deck instantly
  662. def load_deck(self, filename):
  663. """Load a deck from a file"""
  664. # Save old deck before proceeding
  665. if self._waiting_for_decksave:
  666. self.save_deck()
  667. self.unload_deck()
  668. if settings.save_ram:
  669. # In reduced RAM mode the loading will take much longer
  670. self.progressbar_deckload.show()
  671. # progress callback
  672. def progresscallback(fraction):
  673. self.progressbar_deckload.set_fraction(fraction)
  674. # return callback
  675. def finish_deckload(deck):
  676. self.deck = deck
  677. self.enable_deck()
  678. self.refresh_deck()
  679. self.progressbar_deckload.hide()
  680. self._deck_load_async_handle = \
  681. async.start(decks.load(filename, progresscallback,
  682. finish_deckload))
  683. else:
  684. # No need to display any progress bar here
  685. def finish_deckload(deck):
  686. self.deck = deck
  687. logging.info(_("Deck '%s' loaded."), deck.filename)
  688. self.enable_deck()
  689. self.refresh_deck()
  690. async.run(decks.load(filename, None, finish_deckload))
  691. def save_deck(self):
  692. """Save the currently edited deck to disk"""
  693. if not self._waiting_for_decksave:
  694. return # deck has been saved in the meantime
  695. self._waiting_for_decksave = False
  696. old_filename = None
  697. if self.deck.name != self.deck.derive_name():
  698. new_filename = self.deck.derive_filename()
  699. if not os.path.exists(new_filename):
  700. old_filename = self.deck.filename
  701. self.deck.filename = new_filename
  702. self.except_safe(self.deck.save)
  703. logging.info(_("Deck saved: %s"), self.deck.filename)
  704. if old_filename is not None and os.path.exists(old_filename):
  705. os.remove(old_filename)
  706. def export_deck(self, *args):
  707. """Export a deck to a file"""
  708. dialog = gtk.FileChooserDialog(_("Export deck..."), self.main_win,
  709. gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL,
  710. gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT))
  711. dialog.set_default_response(gtk.RESPONSE_CANCEL)
  712. dialog.set_do_overwrite_confirmation(True)
  713. dialog.set_current_folder(settings.deck_dir)
  714. dialog.set_current_name(self.deck.name + config.DECKFILE_SUFFIX)
  715. response = dialog.run()
  716. if response == gtk.RESPONSE_ACCEPT:
  717. old_filename = self.deck.filename
  718. self.deck.filename = dialog.get_filename()
  719. self.except_safe(self.deck.save)
  720. logging.info(_("Deck exported as: %s"), self.deck.filename)
  721. self.deck.filename = old_filename
  722. dialog.destroy()
  723. def edit_deck(self, *args):
  724. """Edit the deck description and author"""
  725. if self.deck is not None:
  726. self.notebook_search.set_current_page(3)
  727. self.textview_deckdesc.grab_focus()
  728. def show_deckstats(self, widget):
  729. """Show statistics about the deck"""
  730. if self.deck is not None:
  731. self.notebook_search.set_current_page(4)
  732. def delayed_decksave(self):
  733. if not self._waiting_for_decksave:
  734. self._waiting_for_decksave = True
  735. glib.timeout_add(settings.decksave_timeout, self.save_deck)
  736. def update_cardcount(self):
  737. """Update the decklist and sideboard card count display"""
  738. if self.deck is not None:
  739. lands = 0
  740. for c in self.deck.decklist:
  741. if c.cardtype.find("Land") >= 0:
  742. lands += 1
  743. self.decksummary.set_text(_("Deck: %d (Lands: %d), Sideboard: %d") %
  744. (len(self.deck.decklist), lands, len(self.deck.sideboard)))
  745. self.deck.derive_color()
  746. for c in ["white", "blue", "black", "red", "green"]:
  747. if c in self.deck.color:
  748. getattr(self, "mana_" + c).show()
  749. else:
  750. getattr(self, "mana_" + c).hide()
  751. else:
  752. self.decksummary.set_text("")
  753. for c in ["white", "blue", "black", "red", "green"]:
  754. getattr(self, "mana_" + c).hide()
  755. #
  756. # Select a card / deck
  757. #
  758. def select_card(self, widget):
  759. """Click on the deck card list"""
  760. if self.deck is not None:
  761. cardid = self.get_selected_card()[0]
  762. if cardid is not None:
  763. self.show_card(cardid)
  764. def select_result(self, widget):
  765. """Click on the search view"""
  766. cardid = self.get_selected_result()
  767. self.show_card(cardid)
  768. def doubleclick_result(self, *args):
  769. """Double ckick on the search view"""
  770. if self.deck is not None:
  771. cardid = self.get_selected_result()
  772. self.add_to_deck(cardid, False)
  773. # TODO: shift determines sb
  774. def doubleclick_card(self, *args):
  775. """Double click on the card view"""
  776. cardid, sb, removed = self.get_selected_card()
  777. if cardid is not None:
  778. if removed:
  779. self.add_to_deck(cardid, sb)
  780. # else:
  781. # self.toggle_sideboard()
  782. def cardview_click(self, widget, event):
  783. """Clicked on the card view"""
  784. if event.button == 3:
  785. # show popup menu
  786. cardid, sb, removed = self.get_selected_card()
  787. if cardid is not None:
  788. text = "to deck" if sb else "to sideboard"
  789. self.menuitem3.get_child().set_text(text)
  790. self.cardview_menu.popup(None, None, None, event.button,
  791. event.time)
  792. def show_card(self, cardid):
  793. """Show a card picture and information"""
  794. self.hbuttonbox_transform.hide()
  795. if cardid is not None:
  796. try:
  797. self.cardpic.set_from_pixbuf(pics.get(cardid))
  798. except RuntimeError:
  799. pass # If there is not picture, continue anyways
  800. card = cards.get(cardid)
  801. self._enlarged_card = card
  802. self.carddetails.set_markup(card.markup())
  803. if cardid[-1] in ("a", "b"):
  804. self.hbuttonbox_transform.show()
  805. else:
  806. self.cardpic.set_from_pixbuf(pics.get("deckmaster"))
  807. def transform_card(self, widget):
  808. """View the respective transformed card"""
  809. card = self._enlarged_card
  810. if card is not None and card.id[-1] in ("a", "b"):
  811. self.show_card(card.id[:-1] + ("b" if card.id[-1] == "a" else "a"))
  812. def get_selected_result(self):
  813. model, it = self.resultview.get_selection().get_selected()
  814. if it is None:
  815. return None
  816. cardid = model.get_value(it, 0)
  817. return cardid
  818. def get_selected_card(self):
  819. """Get the currently selected card in the deck"""
  820. model, it = self.cardview.get_selection().get_selected()
  821. if it is None:
  822. return None, None, None
  823. cardid = model.get_value(it, 0)
  824. sb = model.get_value(it, 8)
  825. removed = model.get_value(it, 9)
  826. return cardid, sb, removed
  827. def get_selected_deck(self):
  828. """Get the currently selected deck"""
  829. model, it = self.treeview_files.get_selection().get_selected()
  830. if it is None:
  831. return None
  832. filename = model.get_value(it, 1)
  833. return filename
  834. #
  835. # Deck editing
  836. #
  837. def deckname_changed(self, widget):
  838. """The deckname has been changed"""
  839. if self._is_loading or self.deck is None:
  840. return
  841. new_name = self.deckname_entry.get_text()
  842. new_filename = self.deck.derive_filename(new_name)
  843. if new_name != "" and not os.path.exists(new_filename):
  844. self.deckname_entry.set_property("secondary-icon-stock", None)
  845. self.deck.name = new_name
  846. model, it = self.treeview_files.get_selection().get_selected()
  847. model.set_value(it, 0, new_filename)
  848. model.set_value(it, 1, new_name)
  849. self.delayed_decksave()
  850. else:
  851. self.deckname_entry.set_property("secondary-icon-stock",
  852. gtk.STOCK_STOP)
  853. if new_name == "":
  854. tooltip = _("A deck's name cannot be empty.")
  855. elif os.path.isdir(new_filename):
  856. tooltip = _("A directory with that name exists.")
  857. else:
  858. tooltip = _("A deck with that name already exists.")
  859. self.deckname_entry.set_property("secondary-icon-tooltip-text",
  860. tooltip)
  861. def author_changed(self, widget):
  862. """The author has been changed"""
  863. if not self._is_loading and self.deck is not None:
  864. self.deck.author = self.entry_author.get_text()
  865. self.delayed_decksave()
  866. def deckdesc_changed(self, widget):
  867. """The deck description has been changed"""
  868. if not self._is_loading and self.deck is not None:
  869. buf = self.textview_deckdesc.get_buffer()
  870. self.deck.description = buf.get_text(buf.get_start_iter(),
  871. buf.get_end_iter())
  872. self.delayed_decksave()
  873. def insert_one(self, *args):
  874. """Insert an additional card of this kind to the deck"""
  875. cardid, sb, removed = self.get_selected_card()
  876. if cardid is not None:
  877. self.add_to_deck(cardid, sb)
  878. def remove_one(self, *args):
  879. """Remove currently selected card from the deck"""
  880. cardid, sb, removed = self.get_selected_card()
  881. if cardid is not None:
  882. self.remove_from_deck(cardid, sb)
  883. def add_to_deck(self, cardid, sideboard=False):
  884. """Add a card to the deck"""
  885. if self.deck.readonly:
  886. self.show_dialog(None, _("This deck is read-only."),
  887. dialog_type="error")
  888. return
  889. card = cards.get(cardid)
  890. (self.deck.sideboard if sideboard else self.deck.decklist).append(card)
  891. # Look if the card has recently been deleted
  892. for row in self.cards:
  893. if row[0] == cardid and row[8] == sideboard and row[9]:
  894. row[9] = False
  895. break
  896. else:
  897. it = self.cards.append((card.id, card.name, card.manacost,
  898. card.get_composed_type(), card.power, card.toughness,
  899. card.rarity, card.setname, sideboard, False, card.price,
  900. _price_to_text(card.price), card.releasedate))
  901. self.cardview.set_cursor(self.cards.get_path(it))
  902. self.cardview.scroll_to_cell(self.cardview.get_model().get_path(it))
  903. self.delayed_decksave()
  904. self.update_cardcount()
  905. def remove_from_deck(self, cardid, sideboard=False):
  906. """Remove a card from the deck"""
  907. if self.deck.readonly:
  908. self.show_dialog(None, _("This deck is read-only."),
  909. dialog_type="error")
  910. return
  911. cardid, sb, removed = self.get_selected_card()
  912. if cardid is not None and not removed:
  913. l = self.deck.sideboard if sb else self.deck.decklist
  914. c = filter(lambda c: c.id == cardid, l)[0]
  915. l.remove(c)
  916. model, it = self.cardview.get_selection().get_selected()
  917. model.set_value(it, 9, True)
  918. # select next card
  919. it = model.iter_next(it)
  920. if it is not None:
  921. self.cardview.set_cursor(model.get_path(it))
  922. self.cardview.scroll_to_cell(model.get_path(it))
  923. self.delayed_decksave()
  924. self.update_cardcount()
  925. def toggle_sideboard(self, *args):
  926. if self.deck.readonly:
  927. self.show_dialog(None, _("This deck is read-only."),
  928. dialog_type="error")
  929. return
  930. if isinstance(args[0], gtk.CellRendererToggle):
  931. path = args[1]
  932. cardid = self.cards[path][0]
  933. sb = self.cards[path][8]
  934. removed = self.cards[path][9]
  935. self.cards[path][8] = not sb
  936. else:
  937. cardid, sb, removed = self.get_selected_card()
  938. model, it = self.cardview.get_selection().get_selected()
  939. model.set_value(it, 8, not sb)
  940. if cardid is not None and not removed:
  941. old = self.deck.sideboard if sb else self.deck.decklist
  942. new = self.deck.decklist if sb else self.deck.sideboard
  943. card = filter(lambda c: c.id == cardid, old)[0]
  944. old.remove(card)
  945. new.append(card)
  946. self.delayed_decksave()
  947. self.update_cardcount()
  948. #
  949. # Card search
  950. #
  951. def quicksearch(self, widget):
  952. """Pressed enter on the quicksearch field"""
  953. query = self.quicksearch_entry.get_text()
  954. i = 0
  955. for q in ['"id" == ?', '"manacost" == ?',
  956. '"name" LIKE ? OR "type" LIKE ? OR "subtype" LIKE ?',
  957. '"setname" LIKE ?', '"artist" LIKE ?', '"text" LIKE ?']:
  958. l = cards.search(q, (query,) * q.count("?"))
  959. if l != []:
  960. break
  961. i += 1
  962. if i >= 2:
  963. query = "%" + _replace_chars(query) + "%"
  964. if l == []:
  965. self.quicksearch_entry.modify_base(gtk.STATE_NORMAL,
  966. gtk.gdk.color_parse("#A51818"))
  967. glib.timeout_add(500, self.quicksearch_entry.modify_base,
  968. gtk.STATE_NORMAL, gtk.gdk.color_parse("#FFFFFF"))
  969. # FIXME: #ffffff might not be the default background color
  970. self._show_results(l)
  971. def clear_search(self, widget):
  972. """Clear the extended search fields"""
  973. self.entry_name.set_text("")
  974. self.entry_text.set_text("")
  975. self.entry_types.set_text("")
  976. self.entry_sets.set_text("")
  977. for c in [self.checkbutton_white, self.checkbutton_blue,
  978. self.checkbutton_black, self.checkbutton_red,
  979. self.checkbutton_green, self.checkbutton_colorless,
  980. self.checkbutton_lands, self.checkbutton_multicolor]:
  981. c.set_active(False)
  982. self.checkbutton_exclude.set_active(False)
  983. self.combobox_eq_manacost.set_active(-1)
  984. self.entry_manacost.set_text("")
  985. self.combobox_eq_price.set_active(-1)
  986. self.spinbutton_price.set_value(0)
  987. self.combobox_eq_converted_cost.set_active(-1)
  988. self.spinbutton_converted_cost.set_value(0)
  989. self.combobox_eq_power.set_active(-1)
  990. self.spinbutton_power.set_value(0)
  991. self.combobox_eq_toughness.set_active(-1)
  992. self.spinbutton_toughness.set_value(0)
  993. self.entry_rarity.set_text("")
  994. self.entry_artist.set_text("")
  995. self.entry_flavor.set_text("")
  996. def search(self, widget):
  997. """Execute the extended search"""
  998. # Construct query
  999. query = ''
  1000. args = []
  1001. name = self.entry_name.get_text()
  1002. if name != "":
  1003. query += ' "name" LIKE ? AND'
  1004. args.append("%" + _replace_chars(name) + "%")
  1005. text = self.entry_text.get_text()
  1006. if text != "":
  1007. query += ' "text" LIKE ? AND'
  1008. args.append("%" + _replace_chars(text) + "%")
  1009. cardtypes = self.entry_types.get_text()
  1010. if cardtypes != "":
  1011. words = _replace_chars(cardtypes).split("%")
  1012. for word in words:
  1013. query += ' ("type" LIKE ? OR "subtype" LIKE ?) AND'
  1014. args.extend(2 * ["%" + word + "%"])
  1015. artist = self.entry_artist.get_text()
  1016. if artist != "":
  1017. query += ' "artist" LIKE ? AND'
  1018. args.append("%" + _replace_chars(artist) + "%")
  1019. flavor = self.entry_flavor.get_text()
  1020. if flavor != "":
  1021. query += ' "flavor" LIKE ? AND'
  1022. args.append("%" + _replace_chars(flavor) + "%")
  1023. cardsets = self.entry_sets.get_text()
  1024. if cardsets != "":
  1025. cardsets = cardsets.replace(",", "") # remove commas
  1026. cl = _replace_chars(cardsets).split("%")
  1027. if cl != []:
  1028. query += ' ('
  1029. for cset in cl:
  1030. query += ' "setname" LIKE ? OR'
  1031. args.append("%" + cset + "%")
  1032. query = query[:-2]
  1033. query += ') AND'
  1034. rarities = self.entry_rarity.get_text()
  1035. if rarities != "":
  1036. rarities = rarities.replace(",", "") # remove commas
  1037. r = _replace_chars(rarities).split("%")
  1038. if r != []:
  1039. query += ' ('
  1040. for rarity in r:
  1041. query += ' "rarity" LIKE ? OR'
  1042. args.append(rarity + "%")
  1043. query = query[:-2]
  1044. query += ') AND'
  1045. exact_color = self.checkbutton_exclude.get_active()
  1046. clist = ["white", "blue", "black", "red", "green", "colorless"]
  1047. if any(map(lambda c: getattr(self, "checkbutton_" + c).get_active(),
  1048. clist)) or self.checkbutton_lands.get_active():
  1049. if not exact_color:
  1050. query += '('
  1051. for c in clist:
  1052. cb = getattr(self, "checkbutton_" + c).get_active()
  1053. if cb or exact_color:
  1054. query += ' "is%s" == ? %s' % (c,
  1055. 'AND' if exact_color else 'OR')
  1056. args.append(cb)
  1057. if self.checkbutton_lands.get_active():
  1058. query += '"type" LIKE "%Land%" '
  1059. query += 'AND' if exact_color else 'OR'
  1060. if not exact_color:
  1061. query = query[:-2] + ') AND'
  1062. if self.checkbutton_multicolor.get_active():
  1063. query += ' "iswhite" + "isblue" + "isblack" + "isred" + "isgreen"' \
  1064. ' >= 2 AND'
  1065. eq = ["", "=", "<=", ">="]
  1066. price_eq = self.combobox_eq_price.get_active()
  1067. if price_eq > 0:
  1068. query += ' "price" %s ? AND "price" >= 0 AND' % eq[price_eq]
  1069. args.append(int(self.spinbutton_price.get_value() * 100))
  1070. converted_eq = self.combobox_eq_converted_cost.get_active()
  1071. if converted_eq > 0:
  1072. query += ' "converted" %s ? AND' % eq[converted_eq]
  1073. args.append(self.spinbutton_converted_cost.get_value_as_int())
  1074. power_eq = self.combobox_eq_power.get_active()
  1075. if power_eq > 0:
  1076. query += ' CAST("power" AS INTEGER) %s ? AND' % eq[power_eq]
  1077. args.append(self.spinbutton_power.get_value_as_int())
  1078. toughness_eq = self.combobox_eq_toughness.get_active()
  1079. if toughness_eq > 0:
  1080. query += ' CAST("toughness" AS INTEGER) %s ? AND' % eq[toughness_eq]
  1081. args.append(self.spinbutton_toughness.get_value_as_int())
  1082. mana_eq = self.combobox_eq_manacost.get_active()
  1083. manacost = self.entry_manacost.get_text()
  1084. if mana_eq == 1 and manacost != "":
  1085. query += ' "manacost" == ? AND'
  1086. args.append(manacost)
  1087. if mana_eq == 2 and manacost != "":
  1088. total = 0
  1089. mana_cl = manacost
  1090. for c in "WUBRGXYZP":
  1091. query += ' "manacost" LIKE ? AND'
  1092. args.append("%" + manacost.count(c) * c + "%")
  1093. if c not in "XYZP":
  1094. total += manacost.count(c)
  1095. mana_cl = mana_cl.replace(c, "")
  1096. try:
  1097. i = int(mana_cl)
  1098. except:
  1099. pass
  1100. else:
  1101. query += ' "converted" >= ? AND'
  1102. args.append(total + i)
  1103. # Execute query
  1104. if self._execute_search(query[:-3], args) != []:
  1105. self.label_no_results.hide()
  1106. else:
  1107. self.label_no_results.show()
  1108. glib.timeout_add(400, self.label_no_results.hide)
  1109. glib.timeout_add(800, self.label_no_results.show)
  1110. # FIXME: another search might get executed in the mean time
  1111. def execute_custom_search(self, widget):
  1112. """Execute the custom search"""
  1113. bfr = self.textview_sqlquery.get_buffer()
  1114. query = bfr.get_text(bfr.get_start_iter(), bfr.get_end_iter())
  1115. self._execute_search(query)
  1116. def search_lands(self, widget):
  1117. """Find lands matching a deck's colors"""
  1118. if self.deck is None:
  1119. return
  1120. query = '"type" LIKE "%Land%" AND '
  1121. mana = {"white":"W", "blue":"U", "black":"B", "red":"R", "green":"G"}
  1122. basic = {"white":"Plains", "blue":"Island", "black":"Swamp",
  1123. "red":"Mountain", "green":"Forest"}
  1124. if len(self.deck.color) >= 1:
  1125. query += '('
  1126. for c in self.deck.color:
  1127. query += '"text" LIKE "%%{%s}%%" OR ' % mana[c]
  1128. query += '"text" LIKE "%%%s%%" OR ' % basic[c]
  1129. query = query[:-4]
  1130. if len(self.deck.color) >= 1:
  1131. query += ')'
  1132. query += ' AND '
  1133. for c in ["white", "blue", "black", "red", "green"]:
  1134. if c not in self.deck.color:
  1135. query += 'NOT "text" LIKE "%%{%s}%%" AND ' % mana[c]
  1136. query += 'NOT "text" LIKE "%%%s%%" AND ' % basic[c]
  1137. query = query[:-5]
  1138. self._execute_search(query)
  1139. def view_new_cards_show_query(self, widget):
  1140. self.win_set_query.show()
  1141. self.entry_set_query.set_text("")
  1142. self.entry_set_query.grab_focus()
  1143. #
  1144. # Database access
  1145. #
  1146. def _execute_search(self, query, args=()):
  1147. if query == "":
  1148. return # Don't execute an empty query
  1149. # Protect against SQL injection
  1150. if query.find(";") >= 0:
  1151. self.show_dialog(self.main_win,
  1152. _("The query must not contain ';'."), "error")
  1153. return
  1154. try:
  1155. l = cards.search(query, args)
  1156. except sqlite3.OperationalError as e:
  1157. message = "SQL error:\n" + str(e)
  1158. self.show_dialog(self.main_win, message, "error")
  1159. else:
  1160. self._show_results(l)
  1161. return l
  1162. def _show_results(self, cardlist):
  1163. # Insert results into the TreeStore
  1164. self.results.clear()
  1165. i = -1
  1166. while i + 1 < len(cardlist):
  1167. i += 1
  1168. # Group cards with the same name
  1169. versions = filter(lambda c: c.name == cardlist[i].name, cardlist)
  1170. if versions.index(cardlist[i]) > 0:
  1171. # This card has been handled
  1172. continue
  1173. if len(versions) <= 1:
  1174. it = None
  1175. else:
  1176. # Insert a parent card
  1177. card = max(versions, key=lambda card: card.releasedate)
  1178. versions_ = filter(lambda card: card.price >= 0, versions)
  1179. if len(versions_) == 0:
  1180. minprice = -1
  1181. else:
  1182. minprice = min(versions_, key=lambda card: card.price).price
  1183. it = self.results.append(None, (card.id, card.name,
  1184. card.manacost, card.get_composed_type(), card.power,
  1185. card.toughness, card.rarity[0], "...", minprice,
  1186. _price_to_text(minprice), card.releasedate))
  1187. # Insert all child cards
  1188. for card in versions:
  1189. self.results.append(it, (card.id, card.name, card.manacost,
  1190. card.get_composed_type(), card.power, card.toughness,
  1191. card.rarity[0], card.setname, card.price,
  1192. _price_to_text(card.price), card.releasedate))
  1193. # Handle gui
  1194. if len(cardlist) == 0:
  1195. text = _("no results")
  1196. elif len(cardlist) == 1:
  1197. text = _("one result")
  1198. elif len(cardlist) >= settings.results_limit:
  1199. text = _("at least %d results") % len(cardlist)
  1200. # self.button_more_results.show() # FIXME
  1201. else:
  1202. text = _("%d results") % len(cardlist)
  1203. self.label_results.set_text(text)
  1204. if len(cardlist) > 0:
  1205. self.notebook_search.set_current_page(0)
  1206. it = self.results.get_iter_first()
  1207. self.resultview.set_cursor(self.results.get_path(it))
  1208. self.resultview.grab_focus()
  1209. self.select_result(None)
  1210. # If there is only one card result, expand the versions
  1211. if self.results.iter_next(it) is None:
  1212. self.resultview.expand_all()
  1213. if len(cardlist) < settings.results_limit:
  1214. self.button_more_results.hide()
  1215. # Helper functions
  1216. def _price_to_text(price):
  1217. assert(isinstance(price, int))
  1218. if price < 0:
  1219. return _("N/A")
  1220. else:
  1221. return _("$%.2f") % (float(price) / 100)
  1222. def _replace_chars(s):
  1223. """Replace every space not enclosed in quotes by %"""
  1224. t = s.split("\"")
  1225. for i in range(len(t)):
  1226. if i % 2 == 0:
  1227. t[i] = t[i].replace(" ", "%")
  1228. return "".join(t)