Merge branch 'next' of https://github.com/Riesi/lutris into Riesi-next

This commit is contained in:
Mathieu Comandon 2019-04-05 00:39:43 -07:00
commit 6129f9dedb
6 changed files with 275 additions and 10 deletions

View file

@ -8,6 +8,7 @@ from lutris.gui import dialogs
from lutris.gui.widgets.utils import open_uri
from lutris.gui.config.add_game import AddGameDialog
from lutris.gui.config.edit_game import EditGameConfigDialog
from lutris.gui.config.edit_game_categories import EditGameCategoriesDialog
from lutris.gui.installerwindow import InstallerWindow
from lutris.gui.dialogs.uninstall_game import UninstallGameDialog
from lutris.gui.dialogs.log import LogWindow
@ -72,6 +73,10 @@ class GameActions:
"configure", "Configure",
self.on_edit_game_configuration
),
(
"category", "Categories",
self.on_edit_game_categories
),
(
"execute-script", "Execute script",
self.on_execute_script_clicked
@ -119,6 +124,7 @@ class GameActions:
"stop": self.is_game_running,
"show_logs": self.game.is_installed,
"configure": bool(self.game.is_installed),
"category": bool(self.game.is_installed),
"install_more": self.game.is_installed and not self.game.is_search_result,
"execute-script": bool(
self.game.is_installed
@ -200,6 +206,10 @@ class GameActions:
"""Edit game preferences"""
EditGameConfigDialog(self.window, self.game)
def on_edit_game_categories(self, _widget):
"""Edit game categories"""
EditGameCategoriesDialog(self.window, self.game)
def on_execute_script_clicked(self, _widget):
"""Execute the game's associated script"""
manual_command = self.game.runner.system_config.get("manual_command")

View file

@ -0,0 +1,115 @@
import re
from gi.repository import Gtk, Pango
from lutris import pga
from lutris.gui.dialogs import Dialog
from lutris.gui.config.common import GameDialogCommon
#from lutris.gui.config import DIALOG_WIDTH, DIALOG_HEIGHT
class EditGameCategoriesDialog(Dialog, GameDialogCommon):
"""Game category edit dialog."""
def __init__(self, parent, game):
super().__init__("Categories - %s" % game.name, parent=parent)
self.parent = parent
self.game = game
self.game_id = game.id
self.game_categories = pga.get_categories_in_game(self.game_id)
self.grid = Gtk.Grid()
self.set_default_size(350, 250)
self.set_border_width(10)
self.vbox.set_homogeneous(False)
self.vbox.set_spacing(10)
self.vbox.pack_start(self._create_category_checkboxes(), True, True, 0)
self.vbox.pack_start(self._create_add_category(), False, False, 0)
self.build_action_area(self.on_save)
self.show_all()
def _create_category_checkboxes(self):
frame = Gtk.Frame()
# frame.set_label("Categories") # probably too much redundancy
sw = Gtk.ScrolledWindow()
row = Gtk.VBox()
for category in pga.get_categories():
checkbutton_option = Gtk.CheckButton(category)
if category in self.game_categories:
checkbutton_option.set_active(True)
self.grid.attach_next_to(checkbutton_option, None, Gtk.PositionType.BOTTOM, 3, 1)
row.pack_start(self.grid, True, True, 0)
sw.add_with_viewport(row)
frame.add(sw)
return frame
def _create_add_category(self):
def on_add_category(widget=None):
category_text = category_entry.get_text().strip()
if category_text != "":
category_text = re.sub(' +', ' ', category_text) # Remove excessive whitespaces
for category_checkbox in self.grid.get_children():
if category_checkbox.get_label() == category_text:
return
category_entry.set_text("")
checkbutton_option = Gtk.CheckButton(category_text)
checkbutton_option.set_active(True)
self.grid.attach_next_to(checkbutton_option, None, Gtk.PositionType.TOP, 3, 1)
pga.add_category(category_text)
self.vbox.show_all()
hbox = Gtk.HBox()
hbox.set_spacing(10)
category_entry = Gtk.Entry()
category_entry.set_text("")
hbox.pack_start(category_entry, True, True, 0)
button = Gtk.Button.new_with_label("Add Category")
button.connect("clicked", on_add_category)
button.set_tooltip_text("Adds the category to the list.")
hbox.pack_start(button, False, False, 0)
return hbox
# Override the save action box, because we don't need the advanced-checkbox
def build_action_area(self, button_callback, callback2=None):
self.action_area.set_layout(Gtk.ButtonBoxStyle.END)
# Buttons
hbox = Gtk.Box()
cancel_button = Gtk.Button(label="Cancel")
cancel_button.connect("clicked", self.on_cancel_clicked)
hbox.pack_start(cancel_button, True, True, 10)
save_button = Gtk.Button(label="Save")
if callback2:
save_button.connect("clicked", button_callback, callback2)
else:
save_button.connect("clicked", button_callback)
hbox.pack_start(save_button, True, True, 0)
self.action_area.pack_start(hbox, True, True, 0)
def is_valid(self):
return True
def on_save(self, _button):
"""Save game info and destroy widget. Return True if success."""
if not self.is_valid():
return False
for category_checkbox in self.grid.get_children():
label = category_checkbox.get_label()
if label in self.game_categories:
if not category_checkbox.get_active():
pga.delete_game_by_id_from_category(self.game_id, label)
else:
if category_checkbox.get_active():
pga.add_game_to_category(self.game_id, label)
self.parent.on_game_updated(self.game)
self.destroy()

View file

@ -82,6 +82,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.threads_stoppers = []
self.selected_runner = None
self.selected_platform = None
self.selected_category = None
self.icon_type = None
# Load settings
@ -453,7 +454,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
child = scrollwindow_children[0]
child.destroy()
self.games_scrollwindow.add(self.view)
self.set_selected_filter(self.selected_runner, self.selected_platform)
self.set_selected_filter(self.selected_runner, self.selected_platform, self.selected_category)
self.set_show_installed_state(self.filter_installed)
self.view.show_all()
@ -808,18 +809,22 @@ class LutrisWindow(Gtk.ApplicationWindow):
def on_sidebar_changed(self, widget):
row = widget.get_selected_row()
if row is None:
self.set_selected_filter(None, None)
self.set_selected_filter(None, None, None)
elif row.type == "runner":
self.set_selected_filter(row.id, None)
self.set_selected_filter(row.id, None, None)
elif row.type == "categories":
self.set_selected_filter(None, None, row.id)
else:
self.set_selected_filter(None, row.id)
self.set_selected_filter(None, row.id, None)
def set_selected_filter(self, runner, platform):
def set_selected_filter(self, runner, platform, category):
"""Filter the view to a given runner and platform"""
self.selected_runner = runner
self.selected_platform = platform
self.selected_category = category
self.game_store.filter_runner = self.selected_runner
self.game_store.filter_platform = self.selected_platform
self.game_store.filter_category = self.selected_category
self.invalidate_game_filter()
def show_invalid_credential_warning(self):

View file

@ -88,6 +88,7 @@ class GameStore(GObject.Object):
self.filter_text = None
self.filter_runner = None
self.filter_platform = None
self.filter_category = None
self.store = Gtk.ListStore(
int,
str,
@ -218,6 +219,10 @@ class GameStore(GObject.Object):
platform = model.get_value(_iter, COL_PLATFORM)
if platform != self.filter_platform:
return False
if self.filter_category:
identifier = model.get_value(_iter, COL_ID)
if (identifier is None) or identifier not in pga.get_games_in_categories(self.filter_category):
return False
return True
def sort_view(self, key="name", ascending=True):

View file

@ -137,6 +137,7 @@ class SidebarListBox(Gtk.ListBox):
self.active_platforms = pga.get_used_platforms()
self.runners = sorted(runners.__all__)
self.platforms = sorted(platforms.__all__)
self.sidebar_categories = dict() # We have to keep track on the elements somewhere
GObject.add_emission_hook(RunnersDialog, "runner-installed", self.update)
GObject.add_emission_hook(RunnersDialog, "runner-removed", self.update)
@ -177,6 +178,10 @@ class SidebarListBox(Gtk.ListBox):
if row.id is None:
return True # 'All'
return row.id in self.installed_runners
elif row.type == "categories":
if len(self.sidebar_categories) < 1:
return False # Hide useless filter
return True
else:
if len(self.active_platforms) <= 1:
return False # Hide useless filter
@ -192,8 +197,28 @@ class SidebarListBox(Gtk.ListBox):
row.set_header(SidebarHeader("Runners"))
elif before.type == "runner" and row.type == "platform":
row.set_header(SidebarHeader("Platforms"))
elif before.type == "platform" and row.type == "categories":
row.set_header(SidebarHeader("Categories"))
def add_category_entries(self):
pga.delete_categories_without_games()
categories = pga.get_categories()
for category in pga.get_categories():
if category not in self.sidebar_categories.keys():
temp = SidebarRow(category, "categories", category, None)
self.sidebar_categories[category] = temp
self.add(temp)
removalbe_categories = []
for sidebar_category in self.sidebar_categories.keys():
if sidebar_category not in categories:
self.remove(self.sidebar_categories[sidebar_category])
removalbe_categories.append(sidebar_category)
for rem_category in removalbe_categories:
del self.sidebar_categories[rem_category]
self.show_all()
def update(self, *args):
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.active_platforms = pga.get_used_platforms()
self.add_category_entries()
self.invalidate_filter()

View file

@ -49,6 +49,18 @@ DATABASE = {
"sources": [
{"name": "id", "type": "INTEGER", "indexed": True},
{"name": "uri", "type": "TEXT UNIQUE"},
],
"categories": [
{"name": "id", "type": "INTEGER", "indexed": True},
{"name": "category", "type": "TEXT"},
{"name": "category", "type": "UNIQUE"},
],
"games2categories": [
{"name": "games", "type": "INTEGER", "indexed": False},
{"name": "categories", "type": "INTEGER", "indexed": False},
{"name": "games", "type": "REFERENCE", "indexed": False, "referenced": "id"},
{"name": "categories", "type": "REFERENCE", "indexed": False, "referenced": "id"},
#{"name": "games, categories", "type": "UNIQUE"},
]
}
@ -79,13 +91,19 @@ def get_schema(tablename):
def field_to_string(
name="", type="", indexed=False
name="", type="", indexed=False, referenced=None
): # pylint: disable=redefined-builtin
"""Converts a python based table definition to it's SQL statement"""
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
return field_query
if type == "UNIQUE":
return "UNIQUE (%s)" % name
if referenced is None:
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
return field_query
else:
return "FOREIGN KEY (%s) REFERENCES %s(%s)" % (name, name, referenced)
def create_table(name, schema):
@ -415,3 +433,90 @@ def get_used_platforms_game_count():
rows = cursor.execute(query)
results = rows.fetchall()
return {result[0]: result[1] for result in results if result[0]}
def get_categories(select="*"):
"""Get the list of every category in database."""
query = "select " + select + " from categories"
params = []
query += " ORDER BY category"
return_categories = []
for category in sql.db_query(PGA_DB, query, tuple(params)):
return_categories.append(category["category"])
return return_categories
def get_games_in_categories(category="*"):
"""Get the ids of games in database."""
query = "select games.id from games " \
"JOIN games2categories ON games.id = games2categories.games " \
"JOIN categories ON categories.id = games2categories.categories " \
"WHERE categories.category = \"" + category + "\""
params = []
return_ids = []
for category in sql.db_query(PGA_DB, query, tuple(params)):
return_ids.append(category["id"])
return return_ids
def get_categories_in_game(game_id=-1):
"""Get the categories of a game in database."""
if game_id < 0:
return None
query = "select categories.category from categories " \
"JOIN games2categories ON categories.id = games2categories.categories " \
"JOIN games ON games.id = games2categories.games " \
"WHERE games.id = \"" + str(game_id) + "\""
params = []
return_ids = []
for category in sql.db_query(PGA_DB, query, tuple(params)):
return_ids.append(category["category"])
return return_ids
def get_games2categories(select="*"):
"""Get the m2m table for Games2Categories in database."""
query = "select " + select + " from games2categories"
params = []
return_categories = []
for category in sql.db_query(PGA_DB, query, tuple(params)):
return_categories.append({category["categories"]: category["games"]})
print("m2m-List Games2Categories")
print(return_categories)
return return_categories
def add_category(category_name=None):
"""Add a category to the PGA database."""
return sql.db_insert(PGA_DB, "categories", {"category": category_name})
def add_game_to_category(game_id=-1, category=None):
"""Add a m2m reference from game2category to the PGA database."""
query = "insert into games2categories (games, categories) " \
"select " + str(game_id) + " as games, categories.id as categories from categories " \
"where categories.category = \"" + category + "\""
with sql.db_cursor(PGA_DB) as cursor:
sql.cursor_execute(cursor, query)
def delete_category(category):
"""Delete a category from the PGA."""
sql.db_delete(PGA_DB, "categories", "category", category)
def delete_categories_without_games():
"""Deletes category that has no entry in the m2m table."""
query = "delete from categories where id not in ( " \
"select categories.id from games2categories " \
"where games2categories.categories = categories.id )"
with sql.db_cursor(PGA_DB) as cursor:
sql.cursor_execute(cursor, query)
def delete_game_by_id_from_category(game_id, category):
"""Delete a game to category entry from the m2m table."""
query = "DELETE FROM games2categories WHERE categories IN " \
"( SELECT games2categories.categories from categories WHERE games2categories.categories = categories.id " \
"AND categories.category = \"" + category + "\" ) AND games2categories.games = " + str(game_id)
with sql.db_cursor(PGA_DB) as cursor:
sql.cursor_execute(cursor, query)