Provide runner version management for wine

This commit is contained in:
Mathieu Comandon 2015-03-08 00:51:10 +01:00
parent 3f876e328e
commit 6e1f16a9fe
10 changed files with 282 additions and 70 deletions

View file

@ -0,0 +1,45 @@
from gi.repository import Gtk, Pango, GObject
class GridViewCellRendererText(Gtk.CellRendererText):
"""CellRendererText adjusted for grid view display, removes extra padding"""
def __init__(self, width=None, *args, **kwargs):
super(GridViewCellRendererText, self).__init__(*args, **kwargs)
self.props.alignment = Pango.Alignment.CENTER
self.props.wrap_mode = Pango.WrapMode.WORD
self.props.xalign = 0.5
self.props.yalign = 0
self.props.width = width
self.props.wrap_width = width
class CellRendererButton(Gtk.CellRenderer):
value = GObject.Property(
type=str,
nick='value',
blurb='what data to render',
flags=(GObject.PARAM_READWRITE | GObject.PARAM_CONSTRUCT))
def __init__(self, layout):
Gtk.CellRenderer.__init__(self)
self.layout = layout
def do_get_size(self, widget, cell_area=None):
height = 20
max_width = 100
if cell_area:
return (cell_area.x, cell_area.y,
max(cell_area.width, max_width), cell_area.height)
return (0, 0, max_width, height)
def do_render(self, cr, widget, bg_area, cell_area, flags):
context = widget.get_style_context()
context.save()
context.add_class(Gtk.STYLE_CLASS_BUTTON)
self.layout.set_markup("Install")
(x, y, w, h) = self.do_get_size(widget, cell_area)
h -= 4
# Gtk.render_background(context, cr, x, y, w, h)
Gtk.render_frame(context, cr, x, y, w-2, h+4)
Gtk.render_layout(context, cr, x + 10, y, self.layout)
context.restore()

View file

@ -0,0 +1,146 @@
import os
from lutris.util.http import Request
from gi.repository import Gtk, GObject, GLib
from lutris.gui.widgets import Dialog
from lutris.util import system
from lutris.util.extract import extract_archive
from lutris import settings
from lutris.downloader import Downloader
class RunnerInstallDialog(Dialog):
BASE_API_URL = "https://lutris.net/api/runners/"
COL_VER = 0
COL_ARCH = 1
COL_URL = 2
COL_INSTALLED = 3
COL_PROGRESS = 4
def __init__(self, title, parent, runner):
super(RunnerInstallDialog, self).__init__(title, parent)
self.runner = runner
self.runner_info = self.get_runner_info()
label = Gtk.Label("%s version management" % self.runner_info['name'])
self.vbox.add(label)
self.runner_store = self.get_store()
self.treeview = self.get_treeview(self.runner_store)
self.vbox.add(self.treeview)
self.show_all()
def get_runner_info(self):
api_url = self.BASE_API_URL + self.runner
response = Request(api_url).get()
return response.json
def get_treeview(self, model):
treeview = Gtk.TreeView(model=model)
treeview.set_headers_visible(False)
renderer_toggle = Gtk.CellRendererToggle()
renderer_text = Gtk.CellRendererText()
renderer_progress = Gtk.CellRendererProgress()
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=3)
renderer_toggle.connect("toggled", self.on_installed_toggled)
treeview.append_column(installed_column)
version_column = Gtk.TreeViewColumn(None, renderer_text)
version_column.add_attribute(renderer_text, 'text', self.COL_VER)
version_column.set_property('min-width', 80)
treeview.append_column(version_column)
arch_column = Gtk.TreeViewColumn(None, renderer_text,
text=self.COL_ARCH)
arch_column.set_property('min-width', 50)
treeview.append_column(arch_column)
progress_column = Gtk.TreeViewColumn(None, renderer_progress,
value=self.COL_PROGRESS,
visible=self.COL_PROGRESS)
progress_column.set_property('fixed-width', 60)
progress_column.set_property('min-width', 60)
progress_column.set_property('resizable', False)
treeview.append_column(progress_column)
return treeview
def get_store(self):
liststore = Gtk.ListStore(str, str, str, bool, int)
for version_info in self.get_versions():
version = version_info['version']
architecture = version_info['architecture']
progress = 0
is_installed = os.path.exists(
self.get_runner_path(version, architecture)
)
liststore.append(
[version_info['version'],
version_info['architecture'],
version_info['url'],
is_installed,
progress]
)
return liststore
def get_versions(self):
return self.runner_info['versions']
def get_runner_path(self, version, arch):
return os.path.join(settings.RUNNER_DIR, self.runner,
"{}-{}".format(version, arch))
def get_dest_path(self, path):
row = self.runner_store[path]
url = row[2]
filename = os.path.basename(url)
return os.path.join(settings.CACHE_DIR, filename)
def on_installed_toggled(self, widget, path):
row = self.runner_store[path]
if row[self.COL_INSTALLED]:
self.uninstall_runner(path)
else:
self.install_runner(path)
def uninstall_runner(self, path):
row = self.runner_store[path]
version = row[self.COL_VER]
arch = row[self.COL_ARCH]
system.remove_folder(self.get_runner_path(version, arch))
row[self.COL_INSTALLED] = False
def install_runner(self, path):
row = self.runner_store[path]
url = row[2]
dest_path = self.get_dest_path(path)
downloader = Downloader(url, dest_path)
self.download_timer = GLib.timeout_add(100, self.get_progress,
downloader, path)
downloader.start()
def get_progress(self, downloader, path):
row = self.runner_store[path]
row[4] = downloader.progress * 100
if downloader.progress >= 1.0:
self.on_installer_downloaded(path)
return False
return True
def on_installer_downloaded(self, path):
row = self.runner_store[path]
version = row[0]
architecture = row[1]
archive_path = self.get_dest_path(path)
dest_path = self.get_runner_path(version, architecture)
extract_archive(archive_path, dest_path)
row[self.COL_PROGRESS] = 0
row[self.COL_INSTALLED] = True
if __name__ == "__main__":
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
RunnerInstallDialog("test", None, "wine")
GObject.threads_init()
Gtk.main()

View file

@ -7,6 +7,7 @@ from lutris.gui.widgets import get_runner_icon
from lutris.util import system
from lutris.runners import import_runner
from lutris.gui.config_dialogs import RunnerConfigDialog
from lutris.gui.runnerinstalldialog import RunnerInstallDialog
class RunnersDialog(Gtk.Window):
@ -30,9 +31,11 @@ class RunnersDialog(Gtk.Window):
Gtk.PolicyType.AUTOMATIC)
scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
self.vbox.pack_start(scrolled_window, True, True, 0)
self.show_all()
runner_list = lutris.runners.__all__
runner_vbox = Gtk.VBox()
runner_vbox.show()
self.runner_labels = {}
for runner_name in runner_list:
@ -43,12 +46,13 @@ class RunnersDialog(Gtk.Window):
scrolled_window.add_with_viewport(runner_vbox)
buttons_box = Gtk.Box()
buttons_box.show()
open_runner_button = Gtk.Button("Open Runners Folder")
open_runner_button.show()
open_runner_button.connect('clicked', self.on_runner_open_clicked)
buttons_box.add(open_runner_button)
self.vbox.pack_start(buttons_box, False, False, 5)
self.show_all()
self.vbox.pack_start(buttons_box, False, False, 5)
def get_runner_hbox(self, runner_name):
# Get runner details
@ -57,13 +61,16 @@ class RunnersDialog(Gtk.Window):
description = runner.description
hbox = Gtk.HBox()
hbox.show()
# Icon
icon = get_runner_icon(runner_name)
icon.show()
icon.set_alignment(0.5, 0.1)
hbox.pack_start(icon, False, False, 10)
# Label
runner_label = Gtk.Label()
runner_label.show()
if not runner.is_installed():
runner_label.set_sensitive(False)
runner_label.set_markup(
@ -78,43 +85,52 @@ class RunnersDialog(Gtk.Window):
runner_label.set_padding(5, 0)
self.runner_labels[runner] = runner_label
hbox.pack_start(runner_label, True, True, 5)
# Button
button = Gtk.Button()
button.set_size_request(100, 30)
button_align = Gtk.Alignment.new(1.0, 0.0, 0.0, 0.0)
self.configure_button(button, runner)
button_align.add(button)
hbox.pack_start(button_align, False, False, 15)
# Buttons
self.configure_button = Gtk.Button("Configure")
self.configure_button.set_size_request(120, 30)
self.configure_button.connect("clicked", self.on_configure_clicked,
runner)
hbox.pack_start(self.configure_button, False, False, 15)
self.versions_button = Gtk.Button("Manage versions")
self.versions_button.set_size_request(120, 30)
self.versions_button.connect("clicked", self.on_versions_clicked,
runner)
hbox.pack_start(self.versions_button, False, False, 15)
self.install_button = Gtk.Button("Install")
self.install_button.set_size_request(120, 30)
self.install_button.connect("clicked", self.on_install_clicked, runner)
hbox.pack_start(self.install_button, False, False, 15)
self.set_button_display(runner)
return hbox
def configure_button(self, widget, runner):
try:
widget.disconnect(widget.click_signal)
except AttributeError:
pass
if runner.is_installed():
self.runner_labels[runner].set_sensitive(True)
self.setup_configure_button(widget, runner)
def set_button_display(self, runner):
if runner.multiple_versions:
self.versions_button.show()
self.install_button.hide()
else:
self.setup_install_button(widget, runner)
self.versions_button.hide()
self.install_button.show()
def setup_configure_button(self, widget, runner):
widget.set_label('Configure')
widget.set_size_request(100, 30)
handler_id = widget.connect("clicked",
self.on_configure_clicked,
runner)
widget.click_signal = handler_id
if runner.is_installed():
self.configure_button.show()
self.install_button.hide()
else:
self.configure_button.hide()
def setup_install_button(self, widget, runner):
widget.set_label('Install')
handler_id = widget.connect("clicked", self.on_install_clicked, runner)
widget.click_signal = handler_id
def on_versions_clicked(self, widget, runner):
dlg_title = "Manage %s versions" % runner.name
RunnerInstallDialog(dlg_title, self, runner.name)
self.set_button_display(runner)
def on_install_clicked(self, widget, runner):
"""Install a runner"""
runner.install()
self.configure_button(widget, runner)
self.set_button_display(runner)
@staticmethod
def on_configure_clicked(widget, runner):

View file

@ -5,6 +5,7 @@ import os
from gi.repository import Gtk, GObject, Pango, GdkPixbuf, GLib
from gi.repository.GdkPixbuf import Pixbuf
from lutris.gui.cellrenderers import GridViewCellRendererText
from lutris.downloader import Downloader
from lutris.util import datapath
# from lutris.util.log import logger
@ -128,17 +129,6 @@ class ContextualMenu(Gtk.Menu):
event.button, event.time)
class GridViewCellRenderer(Gtk.CellRendererText):
def __init__(self, width=None, *args, **kwargs):
super(GridViewCellRenderer, self).__init__(*args, **kwargs)
self.props.alignment = Pango.Alignment.CENTER
self.props.wrap_mode = Pango.WrapMode.WORD
self.props.xalign = 0.5
self.props.yalign = 0
self.props.width = width
self.props.wrap_width = width
class GameStore(object):
def __init__(self, games, filter_text='', filter_runner='', icon_type=None):
@ -365,7 +355,7 @@ class GameGridView(Gtk.IconView, GameView):
self.set_pixbuf_column(COL_ICON)
self.cell_width = BANNER_SIZE[0] if icon_type == "banner" \
else BANNER_SMALL_SIZE[0]
gridview_cell_renderer = GridViewCellRenderer(width=self.cell_width)
gridview_cell_renderer = GridViewCellRendererText(width=self.cell_width)
self.pack_end(gridview_cell_renderer, False)
self.add_attribute(gridview_cell_renderer, 'markup', COL_NAME)
self.set_item_padding(self.icon_padding)

View file

@ -1,8 +1,6 @@
# -*- coding:Utf-8 -*-
"""Generic runner."""
import os
import shutil
import subprocess
import platform
from gi.repository import Gtk
@ -13,7 +11,7 @@ from lutris.config import LutrisConfig
from lutris.gui import dialogs
from lutris.util.extract import extract_archive
from lutris.util.log import logger
from lutris.util.system import find_executable
from lutris.util import system
def get_arch():
@ -28,6 +26,7 @@ class Runner(object):
"""Generic runner (base class for other runners)."""
is_watchable = True # Is the game's pid a parent of Lutris ?
multiple_versions = False
tarballs = {
'i386': None,
'x64': None
@ -144,7 +143,6 @@ class Runner(object):
def is_installed(self):
"""Return True if runner is installed else False."""
is_installed = False
# Check 'get_executable' first
if hasattr(self, 'get_executable'):
executable = self.get_executable()
@ -154,16 +152,10 @@ class Runner(object):
# Fallback to 'executable' attribute (ssytem-wide install)
if not getattr(self, 'executable', None):
return False
result = find_executable(self.executable)
if result == '':
is_installed = False
else:
is_installed = True
return is_installed
return bool(system.find_executable(self.executable))
def install(self):
"""Install runner using package management systems."""
# Prioritize provided tarballs.
tarball = self.tarballs.get(self.arch)
if tarball:
self.download_and_extract(tarball)
@ -187,5 +179,4 @@ class Runner(object):
os.remove(runner_archive)
def remove_game_data(self, game_path=None):
if os.path.exists(game_path):
shutil.rmtree(game_path)
system.remove_folder(game_path)

View file

@ -204,6 +204,7 @@ class wine(Runner):
"""Run Windows games with Wine."""
human_name = "Wine"
platform = 'Windows'
multiple_versions = True
game_options = [
{
'option': 'exe',

View file

@ -47,6 +47,7 @@ def kill():
# pylint: disable=C0103
class winesteam(wine.wine):
""" Runs Steam for Windows games """
multiple_versions = False
human_name = "Wine Steam"
platform = "Steam for Windows"
is_watchable = False # Steam games pids are not parent of Lutris

View file

@ -55,7 +55,7 @@ class LutrisThread(threading.Thread):
sys.stdout.write(line)
def iter_children(self, process, topdown=True, first=True):
if self.runner.name.startswith('wine') and first:
if self.runner and self.runner.name.startswith('wine') and first:
pids = self.runner.get_pids()
for pid in pids:
wineprocess = Process(pid)

View file

@ -25,23 +25,40 @@ def download_asset(url, dest, overwrite=False):
def download_content(url, data=None, log_errors=True):
timeout = 5
content = None
try:
request = urllib2.urlopen(url, data, timeout)
except urllib2.HTTPError as e:
if log_errors:
logger.error("Unavailable url (%s): %s", url, e)
except (socket.timeout, urllib2.URLError) as e:
if log_errors:
logger.error("Unable to connect to server (%s): %s", url, e)
else:
content = request.read()
return content
request = Request(url, log_errors).get(data)
return request.content
def download_json(url, params=''):
"""Download and decode json string at URL."""
content = download_content(url + "?" + params)
if params:
url = url + "?" + params
content = download_content(url)
if content:
return json.loads(content)
class Request(object):
def __init__(self, url, error_logging=True):
self.url = url
self.error_logging = error_logging
self.content = None
def get(self, data=None, timeout=5):
try:
request = urllib2.urlopen(self.url, data, timeout)
except urllib2.HTTPError as e:
if self.error_logging:
logger.error("Unavailable url (%s): %s", self.url, e)
except (socket.timeout, urllib2.URLError) as e:
if self.error_logging:
logger.error("Unable to connect to server (%s): %s",
self.url, e)
else:
self.content = request.read()
return self
@property
def json(self):
if self.content:
return json.loads(self.content)

View file

@ -118,6 +118,11 @@ def merge_folders(source, destination):
os.path.join(dst_abspath, filename))
def remove_folder(path):
if os.path.exists(path):
shutil.rmtree(path)
def is_removeable(path, excludes=None):
"""Check if a folder is safe to remove (not system or home, ...)"""
if not path: