qvm-template: Port initial code to PyQt.

This commit is contained in:
WillyPillow 2020-08-25 00:17:24 +08:00
parent 79b6d8f72c
commit 7e8ee7e8cc
No known key found for this signature in database
GPG Key ID: 3839E194B1415A9C
4 changed files with 574 additions and 296 deletions

View File

@ -1,19 +1,19 @@
import asyncio
import collections import collections
import concurrent
import concurrent.futures
import itertools import itertools
import json import json
import os import os
import subprocess
import typing import typing
import gi import PyQt5
gi.require_version('Gtk', '3.0') import PyQt5.QtWidgets
#pylint: disable=wrong-import-position from . import ui_qvmtemplate
from gi.repository import GLib from . import ui_templateinstallconfirmdlg
from gi.repository import Gtk from . import ui_templateinstallprogressdlg
from gi.repository import Pango from . import utils
#pylint: disable=invalid-name
BASE_CMD = ['qvm-template', '--enablerepo=*', '--yes', '--quiet'] BASE_CMD = ['qvm-template', '--enablerepo=*', '--yes', '--quiet']
@ -28,16 +28,10 @@ class Template(typing.NamedTuple):
licence: str licence: str
url: str url: str
summary: str summary: str
# --- internal --- # ---- internal ----
description: str description: str
default_status: str default_status: str
weight: int # ------------------
model: Gtk.TreeModel
# ----------------
# XXX: Is there a better way of doing this?
TYPES = [str, str, str, str, int, str, str, str,
str, str, str, str, int, Gtk.TreeModel]
COL_NAMES = [ COL_NAMES = [
'Status', 'Status',
@ -49,27 +43,24 @@ class Template(typing.NamedTuple):
'Install Time', 'Install Time',
'License', 'License',
'URL', 'URL',
'Summary'] 'Summary'
]
@staticmethod @staticmethod
def build(status, entry, model): def build(status, entry):
return Template( return Template(
status, status,
entry['name'], entry['name'],
'%s:%s-%s' % (entry['epoch'], entry['version'], entry['release']), '%s:%s-%s' % (entry['epoch'], entry['version'], entry['release']),
entry['reponame'], entry['reponame'],
# XXX: This may overflow glib ints, though pretty unlikely in the int(entry['size']) // 1000,
# foreseeable future
int(entry['size']) / 1000,
entry['buildtime'], entry['buildtime'],
entry['installtime'], entry['installtime'],
entry['license'], entry['license'],
entry['url'], entry['url'],
entry['summary'], entry['summary'],
entry['description'], entry['description'],
status, status
Pango.Weight.BOOK,
model
) )
class Action(typing.NamedTuple): class Action(typing.NamedTuple):
@ -80,88 +71,7 @@ class Action(typing.NamedTuple):
TYPES = [str, str, str] TYPES = [str, str, str]
COL_NAMES = ['Operation', 'Name', 'Version'] COL_NAMES = ['Operation', 'Name', 'Version']
# TODO: Set default window sizes class TemplateStatusDelegate(PyQt5.QtWidgets.QStyledItemDelegate):
class ConfirmDialog(Gtk.Dialog):
def __init__(self, parent, actions):
super(ConfirmDialog, self).__init__(
title='Confirmation', transient_for=parent, modal=True)
self.add_buttons(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK)
box = self.get_content_area()
self.msg = Gtk.Label()
self.msg.set_markup((
'<b>WARNING: Local changes made to the following'
' templates will be overwritten! Continue?</b>'))
box.add(self.msg)
self.store = Gtk.ListStore(*Action.TYPES)
self.listing = Gtk.TreeView(model=self.store)
for idx, colname in enumerate(Action.COL_NAMES):
renderer = Gtk.CellRendererText()
col = Gtk.TreeViewColumn(colname, renderer, text=idx)
self.listing.append_column(col)
col.set_sort_column_id(idx)
for row in actions:
self.store.append(row)
self.scrollable_listing = Gtk.ScrolledWindow()
self.scrollable_listing.add(self.listing)
box.pack_start(self.scrollable_listing, True, True, 16)
self.show_all()
class ProgressDialog(Gtk.Dialog):
def __init__(self, parent):
super(ProgressDialog, self).__init__(
title='Processing...', transient_for=parent, modal=True)
box = self.get_content_area()
self.spinner = Gtk.Spinner()
self.spinner.start()
box.add(self.spinner)
self.msg = Gtk.Label()
self.msg.set_text('Processing...')
box.add(self.msg)
self.infobox = Gtk.TextView()
self.scrollable = Gtk.ScrolledWindow()
self.scrollable.add(self.infobox)
box.pack_start(self.scrollable, True, True, 16)
self.show_all()
def finish(self, success):
self.spinner.stop()
if success:
self.msg.set_text('Operations succeeded.')
else:
self.msg.set_markup('<b>Error:</b>')
self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.run()
class QubesTemplateApp(Gtk.Window):
def __init__(self):
super(QubesTemplateApp, self).__init__(title='Qubes Template Manager')
self.iconsize = Gtk.IconSize.SMALL_TOOLBAR
self.executor = concurrent.futures.ThreadPoolExecutor()
self.outerbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.__build_action_models()
self.__build_toolbar()
self.__build_listing()
self.__build_infobox()
self.add(self.outerbox)
def __build_action_models(self):
#pylint: disable=invalid-name
OPS = [ OPS = [
['Installed', 'Reinstall', 'Remove'], ['Installed', 'Reinstall', 'Remove'],
['Extra', 'Remove'], ['Extra', 'Remove'],
@ -169,96 +79,162 @@ class QubesTemplateApp(Gtk.Window):
['Downgradable', 'Downgrade', 'Remove'], ['Downgradable', 'Downgrade', 'Remove'],
['Available', 'Install'] ['Available', 'Install']
] ]
self.action_models = {}
for ops in OPS:
# First element is the default status for the certain class of
# templates
self.action_models[ops[0]] = Gtk.ListStore(str)
for oper in ops:
self.action_models[ops[0]].append([oper])
def __build_toolbar(self): def createEditor(self, parent, option, index):
self.toolbar = Gtk.Toolbar() _ = option # unused
self.btn_refresh = Gtk.ToolButton( editor = PyQt5.QtWidgets.QComboBox(parent)
icon_widget=Gtk.Image.new_from_icon_name( # Otherwise the internalPointer can be overwritten with a QComboBox
'view-refresh', self.iconsize), index = index.model().index(index.row(), index.column())
label='Refresh') kind = index.internalPointer().default_status
self.btn_refresh.connect('clicked', self.refresh) for op_list in TemplateStatusDelegate.OPS:
self.toolbar.insert(self.btn_refresh, 0) if op_list[0] == kind:
for op in op_list:
editor.addItem(op)
editor.currentIndexChanged.connect(self.currentIndexChanged)
editor.showPopup()
return editor
return None
self.btn_install = Gtk.ToolButton( def setEditorData(self, editor, index):
icon_widget=Gtk.Image.new_from_icon_name('go-down', self.iconsize), #pylint: disable=no-self-use
label='Apply') cur = index.data()
self.btn_install.connect('clicked', self.show_confirm) idx = editor.findText(cur)
self.toolbar.insert(self.btn_install, 1) if idx >= 0:
editor.setCurrentIndex(idx)
self.outerbox.pack_start(self.toolbar, False, True, 0) def setModelData(self, editor, model, index):
#pylint: disable=no-self-use
model.setData(index, editor.currentText())
def __build_listing(self): def updateEditorGeometry(self, editor, option, index):
self.store = Gtk.ListStore(*Template.TYPES) #pylint: disable=no-self-use
_ = index # unused
editor.setGeometry(option.rect)
self.listing = Gtk.TreeView(model=self.store) @PyQt5.QtCore.pyqtSlot()
self.cols = [] def currentIndexChanged(self):
for idx, colname in enumerate(Template.COL_NAMES): self.commitData.emit(self.sender())
if colname == 'Status':
renderer = Gtk.CellRendererCombo()
renderer.set_property('editable', True)
renderer.set_property('has-entry', False)
renderer.set_property('text-column', 0)
renderer.connect('edited', self.entry_edit)
col = Gtk.TreeViewColumn(
colname,
renderer,
text=idx,
weight=len(Template.TYPES) - 2,
model=len(Template.TYPES) - 1)
else:
renderer = Gtk.CellRendererText()
col = Gtk.TreeViewColumn(
colname,
renderer,
text=idx,
weight=len(Template.TYPES) - 2)
# Right-align for integers
if Template.TYPES[idx] is int:
renderer.set_property('xalign', 1.0)
self.cols.append(col)
self.listing.append_column(col)
col.set_sort_column_id(idx)
sel = self.listing.get_selection()
sel.set_mode(Gtk.SelectionMode.MULTIPLE)
sel.connect('changed', self.update_info)
self.scrollable_listing = Gtk.ScrolledWindow() class TemplateModel(PyQt5.QtCore.QAbstractItemModel):
self.scrollable_listing.add(self.listing) def __init__(self):
self.scrollable_listing.set_visible(False) super().__init__()
self.spinner = Gtk.Spinner() self.children = []
self.outerbox.pack_start(self.scrollable_listing, True, True, 0) def flags(self, index):
self.outerbox.pack_start(self.spinner, True, True, 0) if index.isValid() and index.column() == 0:
return super().flags(index) | PyQt5.QtCore.Qt.ItemIsEditable
return super().flags(index)
def __build_infobox(self): def sort(self, idx, order):
self.infobox = Gtk.TextView() rev = (order == PyQt5.QtCore.Qt.AscendingOrder)
self.outerbox.pack_start(self.infobox, True, True, 16) self.children.sort(key=lambda x: x[idx], reverse=rev)
def refresh(self, button=None): self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
# Ignore if we're already doing a refresh
#pylint: disable=no-member def index(self, row, column, parent=PyQt5.QtCore.QModelIndex()):
if self.spinner.props.active: if not self.hasIndex(row, column, parent):
return return PyQt5.QtCore.QModelIndex()
self.scrollable_listing.set_visible(False)
self.spinner.start() return self.createIndex(row, column, self.children[row])
self.spinner.set_visible(True)
self.store.clear() def parent(self, child):
def worker(): #pylint: disable=no-self-use
_ = child # unused
return PyQt5.QtCore.QModelIndex()
def rowCount(self, parent=PyQt5.QtCore.QModelIndex()):
#pylint: disable=no-self-use
_ = parent # unused
return len(self.children)
def columnCount(self, parent=PyQt5.QtCore.QModelIndex()):
#pylint: disable=no-self-use
_ = parent # unused
return len(Template.COL_NAMES)
def hasChildren(self, index=PyQt5.QtCore.QModelIndex()):
#pylint: disable=no-self-use
return index == PyQt5.QtCore.QModelIndex()
def data(self, index, role=PyQt5.QtCore.Qt.DisplayRole):
if index.isValid():
if role == PyQt5.QtCore.Qt.DisplayRole:
return self.children[index.row()][index.column()]
if role == PyQt5.QtCore.Qt.FontRole:
font = PyQt5.QtGui.QFont()
tpl = self.children[index.row()]
font.setBold(tpl.status != tpl.default_status)
return font
if role == PyQt5.QtCore.Qt.TextAlignmentRole:
if isinstance(self.children[index.row()][index.column()], int):
return PyQt5.QtCore.Qt.AlignRight
return PyQt5.QtCore.Qt.AlignLeft
return None
def setData(self, index, value, role=PyQt5.QtCore.Qt.EditRole):
if index.isValid() and role == PyQt5.QtCore.Qt.EditRole:
old_list = list(self.children[index.row()])
old_list[index.column()] = value
new_tpl = Template(*old_list)
self.children[index.row()] = new_tpl
self.dataChanged.emit(index, index)
return True
return False
def headerData(self, section, orientation,
role=PyQt5.QtCore.Qt.DisplayRole):
#pylint: disable=no-self-use
if section < len(Template.COL_NAMES) \
and orientation == PyQt5.QtCore.Qt.Horizontal \
and role == PyQt5.QtCore.Qt.DisplayRole:
return Template.COL_NAMES[section]
return None
def removeRows(self, row, count, parent=PyQt5.QtCore.QModelIndex()):
_ = parent # unused
self.beginRemoveRows(PyQt5.QtCore.QModelIndex(), row, row + count)
del self.children[row:row+count]
self.endRemoveRows()
self.dataChanged.emit(*self.row_index(row, row + count))
def row_index(self, low, high):
return self.createIndex(low, 0), \
self.createIndex(high, self.columnCount())
def set_templates(self, templates):
self.removeRows(0, self.rowCount())
cnt = sum(len(g) for _, g in templates.items())
self.beginInsertRows(PyQt5.QtCore.QModelIndex(), 0, cnt - 1)
for status, grp in templates.items():
for tpl in grp:
self.children.append(Template.build(status, tpl))
self.endInsertRows()
self.dataChanged.emit(*self.row_index(0, self.rowCount() - 1))
def get_actions(self):
actions = []
for tpl in self.children:
if tpl.status != tpl.default_status:
actions.append(Action(tpl.status, tpl.name, tpl.evr))
return actions
async def refresh(self, refresh=True):
cmd = BASE_CMD[:] cmd = BASE_CMD[:]
if button is not None: if refresh:
# Force refresh if triggered by button press # Force refresh if triggered by button press
cmd.append('--refresh') cmd.append('--refresh')
cmd.extend(['info', '--machine-readable-json', '--installed', cmd.extend(['info', '--machine-readable-json', '--installed',
'--available', '--upgrades', '--extras']) '--available', '--upgrades', '--extras'])
output = subprocess.check_output(cmd) proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)
output, stderr = await proc.communicate()
output = output.decode('ASCII')
if proc.returncode != 0:
stderr = stderr.decode('ASCII')
return False, stderr
# Default type is dict as we're going to replace the lists with # Default type is dict as we're going to replace the lists with
# dicts shortly after # dicts shortly after
tpls = collections.defaultdict(dict, json.loads(output)) tpls = collections.defaultdict(dict, json.loads(output))
@ -288,45 +264,37 @@ class QubesTemplateApp(Gtk.Window):
k: v for k, v in tpls['available'].items() k: v for k, v in tpls['available'].items()
if k not in tpls['downgradable']} if k not in tpls['downgradable']}
# Convert back to list # Convert back to list
for key in tpls: tpls = {k.title(): list(v.values()) for k, v in tpls.items()}
tpls[key] = list(tpls[key].values()) self.set_templates(tpls)
for status, seq in tpls.items(): return True, None
status_str = status.title()
for entry in seq:
self.store.append(Template.build(
status_str, entry, self.action_models[status_str]))
def finish_cb(future): class TemplateInstallConfirmDialog(
def callback(): ui_templateinstallconfirmdlg.Ui_TemplateInstallConfirmDlg,
if future.exception() is not None: PyQt5.QtWidgets.QDialog):
buf = self.infobox.get_buffer() def __init__(self, actions):
buf.set_text('Error:\n' + str(future.exception())) super().__init__()
self.spinner.set_visible(False) self.setupUi(self)
self.spinner.stop()
self.scrollable_listing.set_visible(True)
GLib.idle_add(callback)
future = self.executor.submit(worker) model = PyQt5.QtGui.QStandardItemModel()
future.add_done_callback(finish_cb) model.setHorizontalHeaderLabels(Action.COL_NAMES)
self.treeView.setModel(model)
def show_confirm(self, button=None): for act in actions:
_ = button # unused model.appendRow([PyQt5.QtGui.QStandardItem(x) for x in act])
actions = []
for row in self.store:
tpl = Template(*row)
if tpl.status != tpl.default_status:
actions.append(Action(tpl.status, tpl.name, tpl.evr))
dialog = ConfirmDialog(self, actions)
resp = dialog.run()
dialog.destroy()
if resp == Gtk.ResponseType.OK:
self.do_install(actions)
def do_install(self, actions): class TemplateInstallProgressDialog(
dialog = ProgressDialog(self) ui_templateinstallprogressdlg.Ui_TemplateInstallProgressDlg,
def worker(): PyQt5.QtWidgets.QDialog):
actions.sort() def __init__(self, actions):
for oper, grp in itertools.groupby(actions, lambda x: x[0]): super().__init__()
self.setupUi(self)
self.actions = actions
self.buttonBox.hide()
def install(self):
async def coro():
self.actions.sort()
for oper, grp in itertools.groupby(self.actions, lambda x: x[0]):
oper = oper.lower() oper = oper.lower()
# No need to specify versions for local operations # No need to specify versions for local operations
if oper in ('remove', 'purge'): if oper in ('remove', 'purge'):
@ -339,67 +307,108 @@ class QubesTemplateApp(Gtk.Window):
# the messages can be displayed in time. # the messages can be displayed in time.
envs = os.environ.copy() envs = os.environ.copy()
envs['PYTHONUNBUFFERED'] = '1' envs['PYTHONUNBUFFERED'] = '1'
proc = subprocess.Popen( proc = await asyncio.create_subprocess_exec(
BASE_CMD + [oper, '--'] + specs, *(BASE_CMD + [oper, '--'] + specs),
stdout=subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=asyncio.subprocess.STDOUT,
text=True,
bufsize=1,
env=envs) env=envs)
#pylint: disable=cell-var-from-loop #pylint: disable=cell-var-from-loop
for line in iter(proc.stdout.readline, ''): while True:
# Need to modify the buffers in the main thread line = await proc.stdout.readline()
def callback(): if line == b'':
buf = dialog.infobox.get_buffer() break
end_iter = buf.get_end_iter() line = line.decode('ASCII')
buf.insert(end_iter, line) self.textEdit.append(line.rstrip())
GLib.idle_add(callback) if await proc.wait() != 0:
if proc.wait() != 0: self.buttonBox.show()
self.progressBar.setMaximum(100)
self.progressBar.setValue(0)
return False return False
self.progressBar.setMaximum(100)
self.progressBar.setValue(100)
self.buttonBox.show()
return True return True
asyncio.create_task(coro())
def finish_cb(future): class QvmTemplateWindow(
def callback(): ui_qvmtemplate.Ui_QubesTemplateManager,
dialog.finish(future.result()) PyQt5.QtWidgets.QMainWindow):
dialog.destroy() def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
self.refresh() _ = parent # unused
GLib.idle_add(callback)
future = self.executor.submit(worker) super().__init__()
future.add_done_callback(finish_cb) self.setupUi(self)
def update_info(self, sel): self.qubes_app = qubes_app
model, treeiters = sel.get_selected_rows() self.qt_app = qt_app
if not treeiters: self.dispatcher = dispatcher
self.listing_model = TemplateModel()
self.listing_delegate = TemplateStatusDelegate(self.listing)
self.listing.setModel(self.listing_model)
self.listing.setItemDelegateForColumn(0, self.listing_delegate)
self.refresh(False)
self.listing.setItemDelegateForColumn(0, self.listing_delegate)
self.listing.selectionModel() \
.selectionChanged.connect(self.update_info)
self.actionRefresh.triggered.connect(lambda: self.refresh(True))
self.actionInstall.triggered.connect(self.do_install)
def update_info(self, selected):
_ = selected # unused
indices = [
x
for x in self.listing.selectionModel().selectedIndexes()
if x.column() == 0]
if len(indices) == 0:
return return
buf = self.infobox.get_buffer() self.infobox.clear()
if len(treeiters) > 1: cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
def row_to_spec(row): bold_fmt = PyQt5.QtGui.QTextCharFormat()
tpl = Template(*row) bold_fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
return tpl.name + '-' + tpl.evr norm_fmt = PyQt5.QtGui.QTextCharFormat()
text = '\n'.join(row_to_spec(model[it]) for it in treeiters) if len(indices) > 1:
buf.set_text('Selected templates:\n' + text) cursor.insertText('Selected templates:\n', bold_fmt)
for idx in indices:
tpl = self.listing_model.children[idx.row()]
cursor.insertText(tpl.name + '-' + tpl.evr + '\n', norm_fmt)
else: else:
itr = treeiters[0] idx = indices[0]
tpl = Template(*model[itr]) tpl = self.listing_model.children[idx.row()]
text = 'Name: %s\n\nDescription:\n%s' % (tpl.name, tpl.description) cursor.insertText('Name: ', bold_fmt)
buf.set_text(text) cursor.insertText(tpl.name + '\n', norm_fmt)
cursor.insertText('Description:\n', bold_fmt)
cursor.insertText(tpl.description + '\n', norm_fmt)
def entry_edit(self, widget, path, text): def refresh(self, refresh=True):
_ = widget # unused self.progressBar.show()
#pylint: disable=unsubscriptable-object async def coro():
tpl = Template(*self.store[path]) ok, stderr = await self.listing_model.refresh(refresh)
tpl = tpl._replace(status=text) self.infobox.clear()
if text == tpl.default_status: if not ok:
tpl = tpl._replace(weight=Pango.Weight.BOOK) cursor = PyQt5.QtGui.QTextCursor(self.infobox.document())
else: fmt = PyQt5.QtGui.QTextCharFormat()
tpl = tpl._replace(weight=Pango.Weight.BOLD) fmt.setFontWeight(PyQt5.QtGui.QFont.Bold)
#pylint: disable=unsupported-assignment-operation cursor.insertText('Failed to fetch template list:\n', fmt)
self.store[path] = tpl fmt.setFontWeight(PyQt5.QtGui.QFont.Normal)
cursor.insertText(stderr, fmt)
self.progressBar.hide()
asyncio.create_task(coro())
def do_install(self):
actions = self.listing_model.get_actions()
confirm = TemplateInstallConfirmDialog(actions)
if confirm.exec_():
progress = TemplateInstallProgressDialog(actions)
progress.install()
progress.exec_()
self.refresh()
def main():
utils.run_asynchronous(QvmTemplateWindow)
if __name__ == '__main__': if __name__ == '__main__':
main = QubesTemplateApp() main()
main.connect('destroy', Gtk.main_quit)
main.show_all()
main.refresh()
Gtk.main()

104
ui/qvmtemplate.ui Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QubesTemplateManager</class>
<widget class="QMainWindow" name="QubesTemplateManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>465</width>
<height>478</height>
</rect>
</property>
<property name="windowTitle">
<string>Qubes Template Manager</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="listing">
<property name="editTriggers">
<set>QAbstractItemView::AllEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="infobox">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>Click the 'Status' column to change the status of templates.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>465</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionRefresh"/>
<addaction name="actionInstall"/>
</widget>
<action name="actionRefresh">
<property name="icon">
<iconset theme="view-refresh">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Refresh</string>
</property>
</action>
<action name="actionInstall">
<property name="icon">
<iconset theme="go-down">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Install</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TemplateInstallConfirmDlg</class>
<widget class="QDialog" name="TemplateInstallConfirmDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>290</height>
</rect>
</property>
<property name="windowTitle">
<string>Template Install Confirmation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;WARNING: Local changes made to the following templates will be overwritten! Continue?&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="treeView">
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TemplateInstallConfirmDlg</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TemplateInstallConfirmDlg</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TemplateInstallProgressDlg</class>
<widget class="QDialog" name="TemplateInstallProgressDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Installing Templates...</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="textEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TemplateInstallProgressDlg</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TemplateInstallProgressDlg</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>