· 6 years ago · Jul 10, 2019, 10:16 PM
1from fbs_runtime.application_context.PySide2 import ApplicationContext
2
3import sys
4import random
5import sqlite3
6from PySide2 import QtCore, QtWidgets, QtGui
7from typing import List
8from typing import Callable
9
10class Item():
11 def __init__(self, id: int, name: str):
12 self.id = id
13 self.name = name
14
15class Database():
16 def __init__(self, db: sqlite3.Connection):
17 self.db = db
18
19 def query(self, query: str="", limit=999) -> List[Item]:
20 """
21 query the database for items by name
22
23 query
24 prefix of the title to search for
25
26 limit
27 maximum number of results to return
28 """
29 results = []
30 c = self.db.cursor()
31 c.execute("select * from items where name like '%' || ? || '%' LIMIT ?", (query, limit))
32 for id, name in c:
33 results.append(Item(id, name))
34 c.close()
35 return results
36
37 def createItem(self, name: str):
38 """
39 insert a new item into the database
40 """
41 c = self.db.cursor()
42 c.execute("insert into items(name) VALUES (?)", (name,))
43 self.db.commit()
44
45 def getItemByName(self, name: str):
46 c = self.db.cursor()
47 c.execute("select * from items where name = ?", (name,))
48 row = c.fetchone()
49 if row == None:
50 return None
51 id, name = row
52 return Item(id, name)
53
54 def getOrCreateItemByName(self, name: str):
55 """
56 attempt to fetch an existing record from the database by name
57 if it does not exist, create a new one and return it
58
59 name
60 exact name of the record you wish to retrieve
61 """
62 c = self.db.cursor()
63 c.execute("select * from items where name = ?", (name,))
64 row = c.fetchone()
65 if row == None:
66 self.createItem(name)
67 return self.getItemByName(name)
68 id, name = row
69 return Item(id, name)
70
71 def getItemById(self, id: int):
72 c = self.db.cursor()
73 c.execute("select * from items where id = ?", (id,))
74 id, name = c.fetchone()
75 return Item(id, name)
76
77 def getChildren(self, id: int) -> List[Item]:
78 """
79 Return the direct children for an item
80 """
81 results = []
82 c = self.db.cursor()
83 for id, name in c.execute("SELECT b.ID, b.name FROM children a INNER JOIN items b ON a.child = b.id where a.item = ?", (id,)):
84 results.append(Item(id, name))
85 return results
86
87 def delete_item_by_name(self, name: str):
88 """
89 Delete an item from the database by name
90 """
91 c = self.db.cursor()
92 c.execute("delete from items where name=?", (name,))
93 self.db.commit()
94
95 def delete_child(self, item_id: int, child_id: int):
96 """
97 Delete a child from the database
98 """
99 c = self.db.cursor()
100 c.execute("DELETE FROM children WHERE item = ? and child = ?", (item_id, child_id))
101 self.db.commit()
102
103 def add_child(self, itemID: int, childID: int):
104 """
105 Add a child to an existing item
106 """
107 c = self.db.cursor()
108 c.execute("INSERT INTO children(item, child) VALUES (?, ?)", (itemID, childID))
109 self.db.commit()
110
111 def get_parents(self, itemID: int) -> List[Item]:
112 """
113 Get a list of all items that are parents to the selected item
114 """
115 results = []
116 c = self.db.cursor()
117 c.execute("select a.id, a.name from items a inner join children b on a.ID = b.item where b.child = ?", (itemID,))
118 for id, name in c:
119 results.append(Item(id, name))
120 return results
121
122 def item_rename(self, itemName: str, newName: str):
123 c = self.db.cursor()
124 c.execute("UPDATE items SET name = ? where name = ?", (newName,itemName))
125 self.db.commit()
126
127class SearchResults(QtWidgets.QListWidget):
128 """
129 a grid of search results
130 """
131 def __init__(self, db: Database):
132 super().__init__()
133 self.db = db
134
135 def load_data(self, query: str=""):
136 """
137 load data from the database for the given query
138 """
139 self.clear()
140 for item in self.db.query(query):
141 self.addItem((item.name))
142
143 def load_children(self, itemID: str=""):
144 """
145 load the children of an item into view
146 """
147 children = self.db.getChildren(itemID)
148 self.clear()
149 for item in children:
150 self.addItem(item.name)
151
152
153class ItemView(QtWidgets.QWidget):
154 BackPressed = QtCore.Signal()
155
156 """
157 A view of an item
158 """
159 def __init__(self, db: Database):
160 super().__init__()
161 self.db = db
162 self.current_item: Item = None
163
164 # Create widgets
165 self.label = QtWidgets.QLabel("Nothing loaded")
166 self.children = SearchResults(self.db)
167 self.childAddField = QtWidgets.QLineEdit()
168 self.childAddField.setPlaceholderText("add child")
169 self.btnDelete = QtWidgets.QPushButton("delete")
170 self.btnParent = QtWidgets.QPushButton("parent")
171 self.btnParent.pressed.connect(self.load_parent)
172
173 # Set layout
174 self.layout = QtWidgets.QVBoxLayout()
175 self.layout.setAlignment(QtCore.Qt.AlignTop)
176 self.layout.addWidget(self.btnParent)
177 self.layout.addWidget(self.label)
178 self.layout.addWidget(self.children)
179 self.layout.addWidget(self.childAddField)
180 self.setLayout(self.layout)
181
182 # Handlers
183 self.childAddField.returnPressed.connect(self.add_child)
184 self.children.itemActivated.connect(self.child_select)
185
186 def load_item(self, id: int):
187 """
188 load an item into the view
189 """
190 self.current_item = self.db.getItemById(id)
191 self.label.setText(self.current_item.name + "\t\t" + str(self.current_item.id))
192 self.children.load_children(self.current_item.id)
193
194 def load_parent(self):
195 parents = self.db.get_parents(self.current_item.id)
196 if len(parents) < 1:
197 print("this is a root item, could not find a parent")
198 return
199 self.load_item(parents[0].id)
200
201 def child_select(self, child):
202 """
203 When a child is selected from the child list we should open its item view page
204 """
205 item = self.db.getItemByName(child.text())
206 self.load_item(item.id)
207
208 def delete_selected_child(self):
209 """
210 Delete the currently selected child
211 """
212 child = self.children.currentItem()
213 if type(child) == None:
214 print("no child is selected")
215 return
216 item = self.db.getItemByName(child.text())
217 self.db.delete_child(self.current_item.id, item.id)
218 self.load_item(self.current_item.id)
219
220 def add_child(self):
221 """
222 Adds a child to the item list
223 """
224 value = self.childAddField.text().lower().strip()
225 print("adding child: %s" % value)
226 if value in [x.name for x in self.db.getChildren(self.current_item.id)]:
227 print("Item already exists in list")
228 return
229 child = self.db.getOrCreateItemByName(value)
230 self.db.add_child(self.current_item.id, child.id)
231 self.load_item(self.current_item.id)
232 self.childAddField.setText("")
233 self.childAddField.setFocus()
234
235class SearchBar(QtWidgets.QLineEdit):
236 """
237 Search bar for searching things.
238 has a custom keypress handler
239 """
240 TabPress = QtCore.Signal()
241
242 def __init__(self):
243 super().__init__()
244
245 def event(self, event: QtGui.QKeyEvent):
246 if event.type() == QtCore.QEvent.KeyPress and event.nScanCode == 15:
247 self.TabPress.emit()
248 return True
249 return super(SearchBar, self).event(event)
250
251class TreeView(QtWidgets.QListWidget):
252 """
253 View all the recipes for an item
254 """
255 def __init__(self, db: Database):
256 super().__init__()
257 self.db = db
258 self.items = []
259
260 def load_item(self, id: int):
261 """
262 Clear the list and load this item and its children into it
263 """
264 self.clear()
265 self.items.clear()
266 self.add_item(id)
267 for item in self.items:
268 self.addItem(item.name)
269
270 def select_name(self, name: str):
271 """
272 select an item in the list by name
273 """
274 for i in range(self.count()):
275 n = self.item(i).text()
276 if n == name:
277 self.setCurrentRow(i)
278 return i
279
280 def add_item(self, id: int):
281 """
282 load an item and its children into the list
283 """
284 item = self.db.getItemById(id)
285 if item is None:
286 print("Item does not exist in database")
287 return
288
289 # edge case: infinite loop
290 self.items.append(item)
291 print([x.id for x in self.items])
292 if item.id in [x.id for x in self.items][:-1]:
293 print("children create an infinite loop: terminating")
294 return
295
296 # edge case: no children
297 children = self.db.getChildren(id)
298 for child in children:
299 self.add_item(child.id)
300
301class MainWindow(QtWidgets.QWidget):
302 def __init__(self, db: Database):
303 super().__init__()
304 self.promptfn: Callable[[], ()] = Callable
305 self.db = db
306 # create widgets
307 self.search = self.createSearchBar()
308 self.dialogueWindow, self.dialogueResponse = self.createDialogueWindow()
309
310 # tabulated widgets for multiple views
311 self.tabulator = QtWidgets.QStackedWidget()
312 self.itemView = ItemView(db)
313 self.searchResults = SearchResults(db)
314 self.treeView = TreeView(db)
315
316 self.tabulator.addWidget(self.searchResults)
317 self.tabulator.addWidget(self.itemView)
318
319 # button to switch views
320 self.btnSearch = QtWidgets.QPushButton("search")
321 self.btnEdit = QtWidgets.QPushButton("edit")
322 self.btnDelete = QtWidgets.QPushButton("delete")
323
324 # Button handlers
325 self.btnDelete.pressed.connect(self.delete_handler)
326 self.btnSearch.pressed.connect(lambda: [self.tabulator.setCurrentWidget(self.searchResults), self.search.setFocus()])
327 self.btnEdit.pressed.connect(self.editPressed)
328
329 # signal handlers
330 self.searchResults.itemActivated.connect(lambda item: self.open_item(item.text()))
331
332 # create layout
333 self.layout = QtWidgets.QVBoxLayout()
334 self.layout.setAlignment(QtCore.Qt.AlignTop)
335
336 self.layout.addWidget(self.search)
337 self.layout.addWidget(self.tabulator)
338 self.layout.addWidget(self.treeView)
339
340 # Horizontal line of options buttons
341 self.optionsContainer = QtWidgets.QWidget()
342 self.optionsLayout = QtWidgets.QHBoxLayout(self.optionsContainer)
343
344 self.optionsLayout.addWidget(self.btnSearch)
345 self.optionsLayout.addWidget(self.btnEdit)
346 self.optionsLayout.addWidget(self.btnDelete)
347
348 self.optionsContainer.setLayout(self.optionsLayout)
349
350 self.layout.addWidget(self.optionsContainer)
351 self.setLayout(self.layout)
352
353 # load initial data
354 self.searchResults.load_data("")
355
356 # add handlers
357 self.itemView.BackPressed.connect(lambda: self.tabulator.setCurrentWidget(self.searchResults))
358 self.searchResults.currentItemChanged.connect(lambda current: self.treeView.load_item(self.db.getItemByName(current.text()).id))
359 self.dialogueResponse.returnPressed.connect(self.prompt_submit)
360
361 def createSearchBar(self):
362 search = SearchBar()
363 search.textChanged.connect(self.search_callback)
364 search.returnPressed.connect(self.search_enter)
365 search.TabPress.connect(self.search_tabpress)
366 search.setPlaceholderText("search")
367 search.setMaxLength(50)
368 return search
369
370 def createDialogueWindow(self) -> (QtWidgets.QWidget, QtWidgets.QLineEdit):
371 dialogueWindow = QtWidgets.QWidget()
372 dialogueWindow.resize(500, 100)
373
374 # Set layouts
375 dialogueLayout = QtWidgets.QHBoxLayout()
376 dialogueResponse = QtWidgets.QLineEdit()
377 dialogueLayout.addWidget(dialogueResponse)
378 dialogueWindow.setLayout(dialogueLayout)
379
380 return dialogueWindow, dialogueResponse
381
382 def prompt_submit(self):
383 self.setDisabled(False)
384 self.dialogueWindow.hide()
385 if self.promptfn is not None:
386 self.promptfn()
387 self.search_callback()
388
389 def prompt(self, query: str) -> str:
390 self.dialogueResponse.setPlaceholderText(query)
391 self.dialogueResponse.setText("")
392 self.dialogueWindow.show()
393
394 def editPressed(self):
395 if self.tabulator.currentWidget() == self.itemView:
396 pass
397 elif self.tabulator.currentWidget() == self.searchResults:
398 self.prompt("enter text to rename item to")
399 self.setDisabled(True)
400 self.promptfn = lambda: self.db.item_rename(self.searchResults.currentItem().text(), self.dialogueResponse.text().lower().strip())
401
402 def delete_handler(self):
403 w = self.tabulator.currentWidget()
404 if w == self.searchResults:
405 current_item = self.searchResults.currentItem()
406 self.db.delete_item_by_name(current_item.text())
407 self.search_callback(self.search.text())
408 elif w == self.itemView:
409 self.itemView.delete_selected_child()
410
411 def search_callback(self, value: str=""):
412 self.tabulator.setCurrentIndex(0)
413 self.searchResults.load_data(value)
414 print("searching: [%s]" % value)
415
416 def search_enter(self):
417 value = self.search.text().strip().lower()
418
419 # Don't insert any whitespace values
420 if value.strip() == "":
421 self.tabulator.setCurrentWidget(self.searchResults)
422 self.searchResults.load_data("")
423 return
424
425 self.open_item(value)
426
427 def open_item(self, value: str=""):
428 value = value.strip().lower()
429 item = self.db.getOrCreateItemByName(value)
430 self.tabulator.setCurrentWidget(self.itemView)
431 self.itemView.load_item(item.id)
432 self.treeView.load_item(item.id)
433
434 def search_tabpress(self):
435 """
436 Deal with autocompletion of form values on the search field
437 """
438 if self.tabulator.currentWidget() == self.itemView:
439 self.itemView.childAddField.setFocus()
440 return
441 data = self.searchResults.item(0)
442 if data is None:
443 return
444 self.search.setText(data.text())
445 print("search tab pressed")
446
447def init_db(db: sqlite3.Connection):
448 c = db.cursor()
449
450 try:
451 c.executescript("""
452 -- DROP TABLE IF EXISTS items;
453 -- DROP TABLE IF EXISTS children;
454 CREATE TABLE IF NOT EXISTS items (
455 ID integer primary key,
456 name TEXT UNIQUE
457 );
458 CREATE TABLE IF NOT EXISTS children (
459 ID int primary key,
460 item int REFERENCES items(ID) ON DELETE CASCADE,
461 child int REFERENCES items(ID) ON DELETE CASCADE
462 );
463 """)
464 # INSERT INTO items(name) VALUES
465 # ("cobblestone"),
466 # ("stone"),
467 # ("obsidian"),
468 # ("netherrack");
469
470 # INSERT INTO children(item, child) VALUES
471 # (1, 2),
472 # (1, 3);
473 # """)
474 db.commit()
475 except sqlite3.Warning as e:
476 print("Error initializing database %s: " % e)
477 db.rollback()
478 return
479 finally:
480 c.close()
481
482if __name__ == "__main__":
483 import os
484 from os import path
485
486 # Obtain database path
487 if os.name == 'nt': # running windows so set the db file to %appdata%\minecraft_tracker
488 exedir = path.join(os.getenv("appdata"), "minecraft_tracker")
489 try:
490 os.makedirs(exedir)
491 except:
492 print("directory already exists")
493 else:
494 exedir = path.dirname(sys.argv[0])
495
496 dbpath = path.join(exedir, "database.sqlite")
497 print("loading database at: ", dbpath)
498
499 # Connect to the sqlite database
500 db = sqlite3.connect(dbpath)
501 init_db(db)
502
503 # Start the Qt Application
504 # app = QtWidgets.QApplication([])
505 appctxt = ApplicationContext() # 1. Instantiate ApplicationContext
506
507 window = MainWindow(Database(db))
508 window.setWindowTitle("Minecraft Randomizer Tracker")
509 window.resize(500, 500)
510 window.show()
511
512 exit_code = appctxt.app.exec_() # 2. Invoke appctxt.app.exec_()
513 # make sure to close the database
514 db.close()
515 sys.exit(exit_code)