mirror of
https://github.com/lutris/lutris
synced 2024-09-15 22:09:55 +00:00
Provide runner version management for wine
This commit is contained in:
parent
3f876e328e
commit
6e1f16a9fe
45
lutris/gui/cellrenderers.py
Normal file
45
lutris/gui/cellrenderers.py
Normal 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()
|
146
lutris/gui/runnerinstalldialog.py
Normal file
146
lutris/gui/runnerinstalldialog.py
Normal 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()
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -204,6 +204,7 @@ class wine(Runner):
|
|||
"""Run Windows games with Wine."""
|
||||
human_name = "Wine"
|
||||
platform = 'Windows'
|
||||
multiple_versions = True
|
||||
game_options = [
|
||||
{
|
||||
'option': 'exe',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue