This commit is contained in:
zj
2025-11-23 22:11:53 +08:00
parent a923c1098d
commit 20c7191eb6
1105 changed files with 120938 additions and 4 deletions

View File

@@ -0,0 +1,26 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
include(CalamaresAddBrandingSubdirectory)
include(CalamaresAddLibrary)
include(CalamaresAddModuleSubdirectory)
include(CalamaresAddPlugin)
include(CalamaresAddTest)
include(CalamaresAddTranslations)
# library
add_subdirectory(libcalamares)
add_subdirectory(libcalamaresui)
add_subdirectory(qml)
# application
add_subdirectory(calamares)
# plugins
add_subdirectory(modules)
# branding components
add_subdirectory(branding)

View File

@@ -0,0 +1,10 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
# Add branding components. Since there is only one, called "default",
# add that one. For examples of other branding components, see
# the calamares-extensions repository.
calamares_add_branding_subdirectory( default )

View File

@@ -0,0 +1,225 @@
# Branding directory
<!-- SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later
-->
Branding components can go here, or they can be installed separately.
A branding component is a subdirectory with a `branding.desc` descriptor
file, containing brand-specific strings in a key-value structure, plus
brand-specific images or QML. Such a subdirectory, when placed here, is
automatically picked up by CMake and made available to Calamares.
It is recommended to package branding separately, so as to avoid
forking Calamares just for adding some files. Calamares installs
CMake support macros to help create branding packages. See the
calamares-branding repository for examples of stand-alone branding.
## Examples
There is one example of a branding component included with Calamares,
so that it can be run directly from the build directory for testing purposes:
- `default/` is a sample brand for the Generic Linux distribution. It uses
the default Calamares icons and a as start-page splash it provides a
tag-cloud view of languages. The slideshow is a basic one with a few
slides of text and a single image. Translations (done by hand, not via
the usual mechanism of Calamares translations) in English, Arabic, Dutch
and French are available.
Since the slideshow can be **any** QML, it is limited only by your designers
imagination and your QML experience. For straightforward presentations,
see the documentation below. There are more examples in the [calamares-branding][1]
repository.
[1] https://codeberg.org/Calamares/calamares-branding
## API Versions
In Calamares versions prior to 3.2.10, the QML slideshow was loaded
synchronously when the installation page is shown. This can lead to
noticeable lag when showing that page. The QML is written start when
it is loaded, by responding to the `onComplete` signal.
Calamares 3.2.10 introduces an API versioning scheme which uses different
loading mechanisms.
- **API version 1** Loads the QML slideshow synchronously, as before.
- The QML can use `onComplete` to start timers, etc. for progress
or animation.
- Translations are supported through `qsTr()` and the language that is
in use when the installation slideshow is loaded, will be used
(once the installation part is running, it can't change anyway).
- **API version 2** Loads the QML slideshow **a**synchronously, on
startup (generally during the requirements-checking phase of Calamares)
so that no compilation lag is seen.
- The QML should **not** use `onComplete`, since the QML is loaded and
instantiated at startup. Instead,
- The QML should provide functions `onActivate()` and `onLeave()` in the
root object of the slideshow. These are called when the slideshow
should start (e.g. becomes visible) and stop.
- Translations are supported through `qsTr()`. However, since the language
can change after the QML is loaded, code should count on the bindings
being re-evaluated on language change. Translation updates (e.g. change
of language) is **only supported** with Qt 5.10 and later.
The setting *slideshowAPI* in `branding.desc` indicates which one to use
for a given branding slideshow. Which API to use is really a function of
the QML. Expect the version 1 API to be deprecated in the course of Calamares 3.3.
In Calamares 3.2.13 support for activation notification to the QML
parts is improved:
- If the root object has a property *activatedInCalamares* (the examples do),
then that property is set to *true* when the slideshow becomes visible
(activated) and is set to *false* when the slideshow is hidden (e.g.
when the installation phase is done).
- The *actvatedInCalamares* property can be used to set up timers also in V1.
- The keyboard shortcuts in the example slideshow are enabled only while
the slideshow is visible.
## Translations
QML files in a branding component can be translated. Translations should
be placed in a subdirectory `lang/` of the branding component directory.
Qt translation files are supported (`.ts` sources which get compiled into
`.qm`). Inside the `lang` subdirectory all translation files must be named
according to the scheme `calamares-<component name>_<language>.ts`.
The example branding component, called *default*, therefore has translation
files names `calamares-default_nl.ts` (similar for other languages than Dutch).
Text in your `show.qml` (or whatever *slideshow* is set to in the descriptor
file) should be enclosed in this form for translations
```
text: qsTr("This is an example text.")
```
If you use CMake for preparing branding for packaging, the macro
`calamares_add_branding_subdirectory()`` (see also *Project Layout*,
below) will convert the source `.ts` files to their compiled form).
If you are packaging the branding by hand, use
```
lrelease file_en.ts [file_en_GB.ts ..]
```
with all the language suffixes to *file*.
## Presentation
The default QML classes provided by Calamares can be used for a simple
and straightforward "slideshow" presentation with static text and
pictures. To use the default slideshow classes, start with a `show.qml`
file with the following content:
```
import QtQuick 2.5;
import calamares.slideshow 1.0;
Presentation
{
id: presentation
}
```
After the *id*, set properties of the presentation as a whole. These include:
- *loopSlides* (default true) When set, clicking past the last slide
returns to the very first slide.
- *mouseNavigation*, *arrowNavigation*, *keyShortcutsEnabled* (all default
true) enable different ways to navigate the slideshow.
- *titleColor*, *textColor* change the look of the presentation.
- *fontFamily*, *codeFontFamily* change the look of text in the presentation.
After setting properties, you can add elements to the presentation.
Generally, you will add a few presentation-level elements first,
then slides.
- For visible navigation arrows, add elements of class *ForwardButton* and
*BackwardButton*. Set the *source* property of each to a suitable
image. See the `fancy/` example in the external branding-examples
repository. It is recommended to turn off other
kinds of navigation when visible navigation is used.
- To indicate where the user is, add an element of class *SlideCounter*.
This indicates in "n / total" form where the user is in the slideshow.
- To automatically advance the presentation (for a fully passive slideshow),
add a timer that calls the `goToNextSlide()` function of the presentation.
See the `default/` example -- remember to start the timer when the
presentation is completely loaded.
After setting the presentation elements, add one or more Slide elements.
The presentation framework will make a slideshow out of the Slide
elements, displaying only one at a time. Each slide is an element in itself,
so you can put whatever visual elements you like in the slide. They have
standard properties for a boring "static text" slideshow, though:
- *title* is text to show as slide title
- *centeredText* is displayed in a large-ish font
- *writeInText* is displayed by "writing it in" to the slide,
one letter at a time.
- *content* is a list of things which are displayed as a bulleted list.
The presentation classes can be used to produce a fairly dry slideshow
for the installation process; it is recommended to experiment with the
visual effects and classes available in QtQuick.
## Project Layout
A branding component that is created and installed outside of Calamares
will have a top-level `CMakeLists.txt` that includes some boilerplate
to find Calamares, and then adds a subdirectory which contains the
actual branding component.
The file layout in a typical branding component repository is:
```
/
- CMakeLists.txt
- componentname/
- show.qml
- image1.png
...
- lang/
- calamares-componentname_en.ts
- calamares-componentname_de.ts
...
```
Adding the subdirectory can be done as follows:
- If the directory contains files only, and optionally has a single
subdirectory lang/ which contains the translation files for the
component, then `calamares_add_branding_subdirectory()` can be
used, which takes only the name of the subdirectory.
- If the branding component has many files which are organized into
subdirectories, use the SUBDIRECTORIES argument to the CMake function
to additionally install files from those subdirectories. For example,
if the component places all of its images in an `img/` subdirectory,
then call `calamares_add_branding_subdirectory( ... SUBDIRECTORIES img)`.
It is a bad idea to include `lang/` in the SUBDIRECTORIES list.
- The `.ts` files from the `lang/` subdirectory need be be compiled
to `.qm` files before being installed. The CMake macro's do this
automatically. For manual packaging, use `lrelease` to compile
the files.
## Global Storage keys
The following keys from the `branding.desc` file are copied into
Global Storage under a *branding* parent key:
"productName",
"version",
"shortVersion",
"versionedName",
"shortVersionedName",
"shortProductName",
"bootloaderEntryName",
"productUrl",
"supportUrl",
"knownIssuesUrl",
"releaseNotesUrl",
"donateUrl"
<!-- see Branding::s_stringEntryStrings and Branding::setGlobals() -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -0,0 +1,239 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# Product branding information. This influences some global
# user-visible aspects of Calamares, such as the product
# name, window behavior, and the slideshow during installation.
#
# Additional styling can be done using the stylesheet.qss
# file, also in the branding directory.
---
componentName: default
### WELCOME / OVERALL WORDING
#
# These settings affect some overall phrasing and looks,
# which are most visible in the welcome page.
# This selects between different welcome texts. When false, uses
# the traditional "Welcome to the %1 installer.", and when true,
# uses "Welcome to the Calamares installer for %1." This allows
# to distinguish this installer from other installers for the
# same distribution.
welcomeStyleCalamares: false
# Should the welcome image (productWelcome, below) be scaled
# up beyond its natural size? If false, the image does not grow
# with the window but remains the same size throughout (this
# may have surprising effects on HiDPI monitors).
welcomeExpandingLogo: true
### WINDOW CONFIGURATION
#
# The settings here affect the placement of the Calamares
# window through hints to the window manager and initial
# sizing of the Calamares window.
# Size and expansion policy for Calamares.
# - "normal" or unset, expand as needed, use *windowSize*
# - "fullscreen", start as large as possible, ignore *windowSize*
# - "noexpand", don't expand automatically, use *windowSize*
windowExpanding: normal
# Size of Calamares window, expressed as w,h. Both w and h
# may be either pixels (suffix px) or font-units (suffix em).
# e.g. "800px,600px"
# "60em,480px"
# This setting is ignored if "fullscreen" is selected for
# *windowExpanding*, above. If not set, use constants defined
# in CalamaresUtilsGui, 800x520.
windowSize: 800px,520px
# Placement of Calamares window. Either "center" or "free".
# Whether "center" actually works does depend on the window
# manager in use (and only makes sense if you're not using
# *windowExpanding* set to "fullscreen").
windowPlacement: center
### PANELS CONFIGURATION
#
# Calamares has a main content area, and two panels (navigation
# and progress / sidebar). The panels can be controlled individually,
# or switched off. If both panels are switched off, the layout of
# the main content area loses its margins, on the assumption that
# you're doing something special.
# Kind of sidebar (panel on the left, showing progress).
# - "widget" or unset, use traditional sidebar (logo, items)
# - "none", hide it entirely
# - "qml", use calamares-sidebar.qml from branding folder
# In addition, you **may** specify a side, separated by a comma,
# from the kind. Valid sides are:
# - "left" (if not specified, uses this)
# - "right"
# - "top"
# - "bottom"
# For instance, "widget,right" is valid; so is "qml", which defaults
# to putting the sidebar on the left. Also valid is "qml,top".
# While "widget,top" is valid, the widgets code is **not** flexible
# and results will be terrible.
sidebar: widget
# Kind of navigation (button panel on the bottom).
# - "widget" or unset, use traditional navigation
# - "none", hide it entirely
# - "qml", use calamares-navigation.qml from branding folder
# In addition, you **may** specify a side, separated by a comma,
# from the kind. The same sides are valid as for *sidebar*,
# except the default is *bottom*.
navigation: widget
### STRINGS, IMAGES AND COLORS
#
# This section contains the "branding proper" of names
# and images, rather than global-look settings.
# These are strings shown to the user in the user interface.
# There is no provision for translating them -- since they
# are names, the string is included as-is.
#
# The four Url strings are the Urls used by the buttons in
# the welcome screen, and are not shown to the user. Clicking
# on the "Support" button, for instance, opens the link supportUrl.
# If a Url is empty, the corresponding button is not shown.
#
# bootloaderEntryName is how this installation / distro is named
# in the boot loader (e.g. in the GRUB menu).
#
# These strings support substitution from /etc/os-release
# if KDE Frameworks 5.58 are available at build-time. When
# enabled, ${varname} is replaced by the equivalent value
# from os-release. All the supported var-names are in all-caps,
# and are listed on the FreeDesktop.org site,
# https://www.freedesktop.org/software/systemd/man/os-release.html
# Note that ANSI_COLOR and CPE_NAME don't make sense here, and
# are not supported (the rest are). Remember to quote the string
# if it contains substitutions, or you'll get YAML exceptions.
#
# The *Url* entries are used on the welcome page, and they
# are visible as buttons there if the corresponding *show* keys
# are set to "true" (they can also be overridden).
strings:
productName: "${NAME}"
shortProductName: Generic
version: 2023.3 LTS
shortVersion: 2023.3
versionedName: Fancy GNU/Linux 2023.3 LTS "Venomous Vole"
shortVersionedName: FancyGL 2023.3
bootloaderEntryName: FancyGL
productUrl: https://calamares.io/
supportUrl: https://codeberg.org/Calamares/calamares/wiki
knownIssuesUrl: https://codeberg.org/Calamares/calamares/issues
releaseNotesUrl: https://calamares.io/news/
donateUrl: https://docs.codeberg.org/improving-codeberg/donate/
# These images are loaded from the branding module directory.
#
# productBanner is an optional image, which if present, will be shown
# on the welcome page of the application, above the welcome text.
# It is intended to have a width much greater than height.
# It is displayed at 64px height (also on HiDPI).
# Recommended size is 64px tall, and up to 460px wide.
# productIcon is used as the window icon, and will (usually) be used
# by the window manager to represent the application. This image
# should be square, and may be displayed by the window manager
# as small as 16x16 (but possibly larger).
# productLogo is used as the logo at the top of the left-hand column
# which shows the steps to be taken. The image should be square,
# and is displayed at 80x80 pixels (also on HiDPI).
# productWallpaper is an optional image, which if present, will replace
# the normal solid background on every page of the application.
# It can be any size and proportion,
# and will be tiled to fit the entire window.
# For a non-tiled wallpaper, the size should be the same as
# the overall window, see *windowSize* above (800x520).
# productWelcome is shown on the welcome page of the application in
# the middle of the window, below the welcome text. It can be
# any size and proportion, and will be scaled to fit inside
# the window. Use `welcomeExpandingLogo` to make it non-scaled.
# Recommended size is 320x150.
#
# These filenames can also use substitutions from os-release (see above).
images:
# productBanner: "banner.png"
productIcon: "squid.png"
productLogo: "squid.png"
# productWallpaper: "wallpaper.png"
productWelcome: "languages.png"
# Colors for text and background components.
#
# - SidebarBackground is the background of the sidebar
# - SidebarText is the (foreground) text color
# - SidebarBackgroundCurrent sets the background of the current step.
# Optional, and defaults to the application palette.
# - SidebarTextCurrent is the text color of the current step.
#
# These colors can **also** be set through the stylesheet, if the
# branding component also ships a stylesheet.qss. Then they are
# the corresponding CSS attributes of #sidebarApp.
style:
SidebarBackground: "#292F34"
SidebarText: "#FFFFFF"
SidebarTextCurrent: "#292F34"
SidebarBackgroundCurrent: "#D35400"
### SLIDESHOW
#
# The slideshow is displayed during execution steps (e.g. when the
# installer is actually writing to disk and doing other slow things).
# The slideshow can be a QML file (recommended) which can display
# arbitrary things -- text, images, animations, or even play a game --
# during the execution step. The QML **is** abruptly stopped when the
# execution step is done, though, so maybe a game isn't a great idea.
#
# The slideshow can also be a sequence of images (not recommended unless
# you don't want QML at all in your Calamares). The images are displayed
# at a rate of 1 every 2 seconds during the execution step.
#
# To configure a QML file, list a single filename:
# slideshow: "show.qml"
# To configure images, like the filenames (here, as an inline list):
# slideshow: [ "/etc/calamares/slideshow/0.png", "/etc/logo.png" ]
slideshow: "show.qml"
# There are two available APIs for a QML slideshow:
# - 1 (the default) loads the entire slideshow when the installation-
# slideshow page is shown and starts the QML then. The QML
# is never stopped (after installation is done, times etc.
# continue to fire).
# - 2 loads the slideshow on startup and calls onActivate() and
# onLeave() in the root object. After the installation is done,
# the show is stopped (first by calling onLeave(), then destroying
# the QML components).
#
# An image slideshow does not need to have the API defined.
slideshowAPI: 2
# These options are to customize online uploading of logs to pastebins:
# - type : Defines the kind of pastebin service to be used. Currently
# it accepts two values:
# - none : disables the pastebin functionality
# - fiche : use fiche pastebin server
# - url : Defines the address of pastebin service to be used.
# Takes string as input. Important bits are the host and port,
# the scheme is not used.
# - sizeLimit : Defines maximum size limit (in KiB) of log file to be pasted.
# The option must be set, to have the log option work.
# Takes integer as input. If < 0, no limit will be forced,
# else only last (approximately) 'n' KiB of log file will be pasted.
# Please note that upload size may be slightly over the limit (due
# to last minute logging), so provide a suitable value.
uploadServer :
type : "fiche"
url : "http://termbin.com:9999"
sizeLimit : -1

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ar">
<context>
<name>show</name>
<message>
<location filename="../show.qml" line="64"/>
<source>This is a second Slide element.</source>
<translation>عرض الثاني</translation>
</message>
<message>
<location filename="../show.qml" line="68"/>
<source>This is a third Slide element.</source>
<translation>عرض الثالث</translation>
</message>
</context>
</TS>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en">
<context>
<name>show</name>
<message>
<location filename="../show.qml" line="64"/>
<source>This is a second Slide element.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../show.qml" line="68"/>
<source>This is a third Slide element.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="eo">
<context>
<name>show</name>
<message>
<location filename="../show.qml" line="64"/>
<source>This is a second Slide element.</source>
<translation>Ĉi tio estas la dua gliteja.</translation>
</message>
<message>
<location filename="../show.qml" line="68"/>
<source>This is a third Slide element.</source>
<translation>Ĉi tio estas la tria gliteja.</translation>
</message>
</context>
</TS>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr">
<context>
<name>show</name>
<message>
<location filename="../show.qml" line="64"/>
<source>This is a second Slide element.</source>
<translation>Ceci est la deuxieme affiche.</translation>
</message>
<message>
<location filename="../show.qml" line="68"/>
<source>This is a third Slide element.</source>
<translation>La troisième affice ce trouve ici.</translation>
</message>
</context>
</TS>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="nl">
<context>
<name>show</name>
<message>
<location filename="../show.qml" line="64"/>
<source>This is a second Slide element.</source>
<translation>Dit is het tweede Dia element.</translation>
</message>
<message>
<location filename="../show.qml" line="68"/>
<source>This is a third Slide element.</source>
<translation>Dit is het derde Dia element.</translation>
</message>
</context>
</TS>

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -0,0 +1,77 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
import QtQuick 2.0;
import calamares.slideshow 1.0;
Presentation
{
id: presentation
function nextSlide() {
console.log("QML Component (default slideshow) Next slide");
presentation.goToNextSlide();
}
Timer {
id: advanceTimer
interval: 1000
running: presentation.activatedInCalamares
repeat: true
onTriggered: nextSlide()
}
Slide {
Image {
id: background
source: "squid.png"
width: 200; height: 200
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
}
Text {
anchors.horizontalCenter: background.horizontalCenter
anchors.top: background.bottom
text: "This is a customizable QML slideshow.<br/>"+
"Distributions should provide their own slideshow and list it in <br/>"+
"their custom branding.desc file.<br/>"+
"To create a Calamares presentation in QML, import calamares.slideshow,<br/>"+
"define a Presentation element with as many Slide elements as needed."
wrapMode: Text.WordWrap
width: presentation.width
horizontalAlignment: Text.Center
}
}
Slide {
centeredText: qsTr("This is a second Slide element.")
}
Slide {
centeredText: qsTr("This is a third Slide element.")
}
// When this slideshow is loaded as a V1 slideshow, only
// activatedInCalamares is set, which starts the timer (see above).
//
// In V2, also the onActivate() and onLeave() methods are called.
// These example functions log a message (and re-start the slides
// from the first).
function onActivate() {
console.log("QML Component (default slideshow) activated");
presentation.currentSlide = 0;
}
function onLeave() {
console.log("QML Component (default slideshow) deactivated");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: no
* SPDX-License-Identifier: CC0-1.0
*/
/*
A branding component can ship a stylesheet (like this one)
which is applied to parts of the Calamares user-interface.
In principle, all parts can be styled through CSS.
Missing parts should be filed as issues.
The IDs are based on the object names in the C++ code.
You can use the Debug Dialog to find out object names:
- Open the debug dialog
- Choose tab *Tools*
- Click *Widget Tree* button
The list of object names is printed in the log.
Documentation for styling Qt Widgets through a stylesheet
can be found at
https://doc.qt.io/qt-5/stylesheet-examples.html
https://doc.qt.io/qt-5/stylesheet-reference.html
In Calamares, styling widget classes is supported (e.g.
using `QComboBox` as a selector).
This example stylesheet has all the actual styling commented out.
The examples are not exhaustive.
*/
/*** Generic Widgets.
*
* You can style **all** widgets of a given class by selecting
* the class name. Some widgets have specialized sub-selectors.
*/
/*
QPushButton { background-color: green; }
*/
/*** Main application window.
*
* The main application window has the sidebar, which in turn
* contains a logo and a list of items -- note that the list
* can **not** be styled, since it has its own custom C++
* delegate code.
*/
/*
#mainApp { }
#sidebarApp { }
#logoApp { }
*/
/*** Welcome module.
*
* There are plenty of parts, but the buttons are the most interesting
* ones (donate, release notes, ...). The little icon image can be
* styled through *qproperty-icon*, which is a little obscure.
* URLs can reference the QRC paths of the Calamares application
* or loaded via plugins or within the filesystem. There is no
* comprehensive list of available icons, though.
*/
/*
QPushButton#aboutButton { qproperty-icon: url(:/data/images/release.svg); }
#donateButton,
#supportButton,
#releaseNotesButton,
#knownIssuesButton { qproperty-icon: url(:/data/images/help.svg); }
*/
/*** Partitioning module.
*
* Many moving parts, which you will need to experiment with.
*/
/*
#bootInfoIcon { }
#bootInfoLable { }
#deviceInfoIcon { }
#defineInfoLabel { }
#scrollAreaWidgetContents { }
#partitionBarView { }
*/
/*** Licensing module.
*
* The licensing module paints individual widgets for each of
* the licenses. The item can be collapsed or expanded.
*/
/*
#licenseItem { }
#licenseItemFullText { }
*/

View File

@@ -0,0 +1,67 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
set(calamaresSources
main.cpp
CalamaresApplication.cpp
CalamaresWindow.cpp
DebugWindow.cpp
VariantModel.cpp
progresstree/ProgressTreeDelegate.cpp
progresstree/ProgressTreeView.cpp
)
include_directories(
${CMAKE_SOURCE_DIR}/src/libcalamares
${CMAKE_SOURCE_DIR}/src/libcalamaresui
${CMAKE_BINARY_DIR}/src/libcalamares
${CMAKE_CURRENT_SOURCE_DIR}
)
### EXECUTABLE
#
# "calamares_bin" is the main application, not to be confused with
# the target "calamares" which is the non-GUI library part.
#
# The calamares-i18n.cxx file -- full path in CALAMARES_TRANSLATIONS_SOURCE --
# is created as a target in the lang/ directory. This is compiled to a
# library (it's just the result of a QRC compile).
add_executable(calamares_bin ${calamaresSources} calamares.qrc)
target_include_directories(calamares_bin PRIVATE ${CMAKE_SOURCE_DIR})
set_target_properties(calamares_bin PROPERTIES ENABLE_EXPORTS TRUE RUNTIME_OUTPUT_NAME calamares)
calamares_automoc( calamares_bin )
calamares_autouic( calamares_bin )
calamares_autorcc( calamares_bin )
target_link_libraries(
calamares_bin
PRIVATE calamares calamaresui calamares-i18n kdsingleapplication ${qtname}::Core ${qtname}::Widgets
)
target_link_libraries(calamares_bin PRIVATE ${kfname}::CoreAddons)
if(BUILD_CRASH_REPORTING)
target_link_libraries(calamares_bin PRIVATE ${kfname}::Crash)
target_compile_definitions(calamares_bin PRIVATE BUILD_CRASH_REPORTING)
endif()
install(TARGETS calamares_bin BUNDLE DESTINATION . RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(
FILES ${CMAKE_SOURCE_DIR}/data/images/squid.svg
RENAME calamares.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps
)
### TESTS
#
#
if(BUILD_TESTING)
# Don't install, these are just for enable_testing
add_executable(loadmodule testmain.cpp)
target_link_libraries(loadmodule PRIVATE ${qtname}::Core ${qtname}::Widgets calamares calamaresui)
add_executable(test_conf test_conf.cpp)
target_link_libraries(test_conf PUBLIC yamlcpp::yamlcpp ${qtname}::Core)
endif()

View File

@@ -0,0 +1,302 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "CalamaresApplication.h"
#include "CalamaresConfig.h"
#include "CalamaresVersionX.h"
#include "CalamaresWindow.h"
#include "progresstree/ProgressTreeView.h"
#include "Branding.h"
#include "JobQueue.h"
#include "Settings.h"
#include "ViewManager.h"
#include "locale/TranslationsModel.h"
#include "modulesystem/ModuleManager.h"
#include "utils/Dirs.h"
#include "utils/Gui.h"
#include "utils/Logger.h"
#include "utils/System.h"
#ifdef WITH_QML
#include "utils/Qml.h"
#endif
#include "utils/Retranslator.h"
#include "viewpages/ViewStep.h"
#include <QDir>
#include <QFileInfo>
#include <QScreen>
#include <QTimer>
/// @brief Convenience for "are the settings in debug mode"
static bool
isDebug()
{
return Calamares::Settings::instance() && Calamares::Settings::instance()->debugMode();
}
CalamaresApplication::CalamaresApplication( int& argc, char* argv[] )
: QApplication( argc, argv )
, m_mainwindow( nullptr )
, m_moduleManager( nullptr )
{
// Setting the organization name makes the default cache
// directory -- where Calamares stores logs, for instance --
// <org>/<app>/, so we end up with ~/.cache/Calamares/calamares/
// which is excessively squidly.
//
// setOrganizationName( QStringLiteral( CALAMARES_ORGANIZATION_NAME ) );
setOrganizationDomain( QStringLiteral( CALAMARES_ORGANIZATION_DOMAIN ) );
setApplicationName( QStringLiteral( CALAMARES_APPLICATION_NAME ) );
setApplicationVersion( QStringLiteral( CALAMARES_VERSION ) );
QFont f = font();
Calamares::setDefaultFontSize( f.pointSize() );
}
void
CalamaresApplication::init()
{
Logger::setupLogfile();
cDebug() << "Calamares version:" << CALAMARES_VERSION;
cDebug() << Logger::SubEntry << "Using Qt version:" << qVersion();
cDebug() << Logger::SubEntry << "Build type:" << CMAKE_BUILD_TYPE;
#ifdef WITH_PYBIND11
cDebug() << Logger::SubEntry << "Using PyBind11";
#endif
#ifdef WITH_BOOST_PYTHON
cDebug() << Logger::SubEntry << "Using Boost Python";
#endif
cDebug() << Logger::SubEntry << "Using settings:" << Calamares::Settings::instance()->path();
cDebug() << Logger::SubEntry << "Using log file:" << Logger::logFile();
cDebug() << Logger::SubEntry << "Languages:" << Calamares::Locale::availableLanguages();
if ( !Calamares::Settings::instance() )
{
cError() << "Must create Calamares::Settings before the application.";
::exit( 1 );
}
initQmlPath();
initBranding();
Calamares::installTranslator();
setQuitOnLastWindowClosed( false );
setWindowIcon( QIcon( Calamares::Branding::instance()->imagePath( Calamares::Branding::ProductIcon ) ) );
cDebug() << Logger::SubEntry << "STARTUP: initSettings, initQmlPath, initBranding done";
initModuleManager(); //also shows main window
cDebug() << Logger::SubEntry << "STARTUP: initModuleManager: module init started";
}
CalamaresApplication::~CalamaresApplication()
{
Logger::CDebug( Logger::LOGVERBOSE ) << "Shutting down Calamares...";
Logger::CDebug( Logger::LOGVERBOSE ) << Logger::SubEntry << "Finished shutdown.";
}
CalamaresApplication*
CalamaresApplication::instance()
{
return qobject_cast< CalamaresApplication* >( QApplication::instance() );
}
CalamaresWindow*
CalamaresApplication::mainWindow()
{
return m_mainwindow;
}
static QStringList
brandingFileCandidates( bool assumeBuilddir, const QString& brandingFilename )
{
QStringList brandingPaths;
if ( Calamares::isAppDataDirOverridden() )
{
brandingPaths << Calamares::appDataDir().absoluteFilePath( brandingFilename );
}
else
{
if ( assumeBuilddir )
{
brandingPaths << ( QDir::currentPath() + QStringLiteral( "/src/" ) + brandingFilename );
}
if ( Calamares::haveExtraDirs() )
{
for ( auto s : Calamares::extraDataDirs() )
{
brandingPaths << ( s + brandingFilename );
}
}
brandingPaths << QDir( CMAKE_INSTALL_FULL_SYSCONFDIR "/calamares/" ).absoluteFilePath( brandingFilename );
brandingPaths << Calamares::appDataDir().absoluteFilePath( brandingFilename );
}
return brandingPaths;
}
void
CalamaresApplication::initQmlPath()
{
#ifdef WITH_QML
if ( !Calamares::initQmlModulesDir() )
{
::exit( EXIT_FAILURE );
}
#endif
}
void
CalamaresApplication::initBranding()
{
QString brandingComponentName = Calamares::Settings::instance()->brandingComponentName();
if ( brandingComponentName.simplified().isEmpty() )
{
cError() << "FATAL: branding component not set in settings.conf";
::exit( EXIT_FAILURE );
}
QString brandingDescriptorSubpath = QString( "branding/%1/branding.desc" ).arg( brandingComponentName );
QStringList brandingFileCandidatesByPriority = brandingFileCandidates( isDebug(), brandingDescriptorSubpath );
QFileInfo brandingFile;
bool found = false;
foreach ( const QString& path, brandingFileCandidatesByPriority )
{
QFileInfo pathFi( path );
if ( pathFi.exists() && pathFi.isReadable() )
{
brandingFile = pathFi;
found = true;
break;
}
}
if ( !found || !brandingFile.exists() || !brandingFile.isReadable() )
{
cError() << "Cowardly refusing to continue startup without branding."
<< Logger::DebugList( brandingFileCandidatesByPriority );
if ( Calamares::isAppDataDirOverridden() )
{
cError() << "FATAL: explicitly configured application data directory is missing" << brandingComponentName;
}
else
{
cError() << "FATAL: none of the expected branding descriptor file paths exist.";
}
::exit( EXIT_FAILURE );
}
new Calamares::Branding( brandingFile.absoluteFilePath(), this, devicePixelRatio() );
}
void
CalamaresApplication::initModuleManager()
{
m_moduleManager = new Calamares::ModuleManager( Calamares::Settings::instance()->modulesSearchPaths(), this );
connect( m_moduleManager, &Calamares::ModuleManager::initDone, this, &CalamaresApplication::initView );
m_moduleManager->init();
}
/** @brief centers the widget @p w on (a) screen
*
* This tries to duplicate the (deprecated) qApp->desktop()->availableGeometry()
* placement by iterating over screens and putting Calamares in the first
* one where it fits; this is *generally* the primary screen.
*
* With debugging, it would look something like this (2 screens attached,
* primary at +1080+240 because I have a very strange X setup). Before
* being mapped, the Calamares window is at +0+0 but does have a size.
* The first screen's geometry includes the offset from the origin in
* screen coordinates.
*
* Proposed window size: 1024 520
* Window QRect(0,0 1024x520)
* Screen QRect(1080,240 2560x1440)
* Moving QPoint(1848,700)
* Screen QRect(0,0 1080x1920)
*
*/
static void
centerWindowOnScreen( QWidget* w )
{
QList< QScreen* > screens = qApp->screens();
QPoint windowCenter = w->rect().center();
QSize windowSize = w->rect().size();
for ( const auto* screen : screens )
{
QSize screenSize = screen->availableGeometry().size();
if ( ( screenSize.width() >= windowSize.width() ) && ( screenSize.height() >= windowSize.height() ) )
{
w->move( screen->availableGeometry().center() - windowCenter );
break;
}
}
}
void
CalamaresApplication::initView()
{
cDebug() << "STARTUP: initModuleManager: all modules init done";
initJobQueue();
cDebug() << "STARTUP: initJobQueue done";
m_mainwindow = new CalamaresWindow(); //also creates ViewManager
connect( m_moduleManager, &Calamares::ModuleManager::modulesLoaded, this, &CalamaresApplication::initViewSteps );
connect( m_moduleManager, &Calamares::ModuleManager::modulesFailed, this, &CalamaresApplication::initFailed );
QTimer::singleShot( 0, m_moduleManager, &Calamares::ModuleManager::loadModules );
if ( Calamares::Branding::instance() && Calamares::Branding::instance()->windowPlacementCentered() )
{
centerWindowOnScreen( m_mainwindow );
}
cDebug() << "STARTUP: CalamaresWindow created; loadModules started";
}
void
CalamaresApplication::initViewSteps()
{
cDebug() << "STARTUP: loadModules for all modules done";
m_moduleManager->checkRequirements();
if ( Calamares::Branding::instance()->windowMaximize() )
{
m_mainwindow->setWindowFlag( Qt::FramelessWindowHint );
m_mainwindow->showMaximized();
}
else
{
m_mainwindow->show();
}
cDebug() << "STARTUP: Window now visible and ProgressTreeView populated";
cDebug() << Logger::SubEntry << Calamares::ViewManager::instance()->viewSteps().count() << "view steps loaded.";
Calamares::ViewManager::instance()->onInitComplete();
}
void
CalamaresApplication::initFailed( const QStringList& l )
{
cError() << "STARTUP: failed modules are" << l;
m_mainwindow->show();
}
void
CalamaresApplication::initJobQueue()
{
Calamares::JobQueue* jobQueue = new Calamares::JobQueue( this );
new Calamares::System( Calamares::Settings::instance()->doChroot(), this );
Calamares::Branding::instance()->setGlobals( jobQueue->globalStorage() );
}

View File

@@ -0,0 +1,64 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARESAPPLICATION_H
#define CALAMARESAPPLICATION_H
#include <QApplication>
class CalamaresWindow;
namespace Calamares
{
class ModuleManager;
} // namespace Calamares
/**
* @brief The CalamaresApplication class extends QApplication to handle
* Calamares startup and lifetime of main components.
*/
class CalamaresApplication : public QApplication
{
Q_OBJECT
public:
CalamaresApplication( int& argc, char* argv[] );
~CalamaresApplication() override;
/**
* @brief init handles the first part of Calamares application startup.
* After the main window shows up, the latter part of the startup sequence
* (including modules loading) happens asynchronously.
*/
void init();
static CalamaresApplication* instance();
/**
* @brief mainWindow returns the Calamares application main window.
*/
CalamaresWindow* mainWindow();
private slots:
void initView();
void initViewSteps();
void initFailed( const QStringList& l );
private:
// Initialization steps happen in this order
void initQmlPath();
void initBranding();
void initModuleManager();
void initJobQueue();
CalamaresWindow* m_mainwindow;
Calamares::ModuleManager* m_moduleManager;
};
#endif // CALAMARESAPPLICATION_H

View File

@@ -0,0 +1,552 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2018 Raul Rodrigo Segura (raurodse)
* SPDX-FileCopyrightText: 2019 Collabora Ltd <arnaud.ferraris@collabora.com>
* SPDX-FileCopyrightText: 2020 Anubhav Choudhary <ac.10edu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "CalamaresWindow.h"
#include "Branding.h"
#include "CalamaresConfig.h"
#include "DebugWindow.h"
#include "Settings.h"
#include "ViewManager.h"
#include "progresstree/ProgressTreeView.h"
#include "utils/Gui.h"
#include "utils/Logger.h"
#include "utils/Qml.h"
#include "utils/Retranslator.h"
#include <QApplication>
#include <QBoxLayout>
#include <QCloseEvent>
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
#include <QDesktopWidget>
#endif
#include <QFile>
#include <QFileInfo>
#include <QLabel>
#ifdef WITH_QML
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickWidget>
#endif
#include <QTreeView>
static QSize
desktopSize( QWidget* w )
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
return qApp->desktop()->availableGeometry( w ).size();
#else
return w->screen()->availableGeometry().size();
#endif
}
static inline int
windowDimensionToPixels( const Calamares::Branding::WindowDimension& u )
{
if ( !u.isValid() )
{
return 0;
}
if ( u.unit() == Calamares::Branding::WindowDimensionUnit::Pixies )
{
return static_cast< int >( u.value() );
}
if ( u.unit() == Calamares::Branding::WindowDimensionUnit::Fonties )
{
return static_cast< int >( u.value() * Calamares::defaultFontHeight() );
}
return 0;
}
/** @brief Expected orientation of the panels, based on their side
*
* Panels on the left and right are expected to be "vertical" style,
* top and bottom should be "horizontal bars". This function maps
* the sides to expected orientation.
*/
static inline Qt::Orientation
orientation( const Calamares::Branding::PanelSide s )
{
using Side = Calamares::Branding::PanelSide;
return ( s == Side::Left || s == Side::Right ) ? Qt::Orientation::Vertical : Qt::Orientation::Horizontal;
}
/** @brief Get a button-sized icon. */
static inline QPixmap
getButtonIcon( const QString& name )
{
return Calamares::Branding::instance()->image( name, QSize( 22, 22 ) );
}
static inline void
setButtonIcon( QPushButton* button, const QString& name )
{
auto icon = getButtonIcon( name );
if ( button && !icon.isNull() )
{
button->setIcon( icon );
}
}
static QWidget*
getWidgetSidebar( Calamares::DebugWindowManager* debug,
Calamares::ViewManager* viewManager,
QWidget* parent,
Qt::Orientation,
int desiredWidth )
{
const Calamares::Branding* const branding = Calamares::Branding::instance();
QWidget* sideBox = new QWidget( parent );
sideBox->setObjectName( "sidebarApp" );
QBoxLayout* sideLayout = new QVBoxLayout;
sideBox->setLayout( sideLayout );
// Set this attribute into qss file
sideBox->setFixedWidth( desiredWidth );
sideBox->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
QHBoxLayout* logoLayout = new QHBoxLayout;
sideLayout->addLayout( logoLayout );
logoLayout->addStretch();
QLabel* logoLabel = new QLabel( sideBox );
logoLabel->setObjectName( "logoApp" );
//Define all values into qss file
{
QPalette plt = sideBox->palette();
sideBox->setAutoFillBackground( true );
plt.setColor( sideBox->backgroundRole(), branding->styleString( Calamares::Branding::SidebarBackground ) );
plt.setColor( sideBox->foregroundRole(), branding->styleString( Calamares::Branding::SidebarText ) );
sideBox->setPalette( plt );
logoLabel->setPalette( plt );
}
logoLabel->setAlignment( Qt::AlignCenter );
logoLabel->setFixedSize( 80, 80 );
logoLabel->setPixmap( branding->image( Calamares::Branding::ProductLogo, logoLabel->size() ) );
logoLayout->addWidget( logoLabel );
logoLayout->addStretch();
ProgressTreeView* tv = new ProgressTreeView( sideBox );
tv->setModel( viewManager );
tv->setFocusPolicy( Qt::NoFocus );
sideLayout->addWidget( tv );
QHBoxLayout* extraButtons = new QHBoxLayout;
sideLayout->addLayout( extraButtons );
const int defaultFontHeight = Calamares::defaultFontHeight();
if ( /* About-Calamares Button enabled */ true )
{
QPushButton* aboutDialog = new QPushButton;
aboutDialog->setObjectName( "aboutButton" );
aboutDialog->setIcon( Calamares::defaultPixmap(
Calamares::Information, Calamares::Original, 2 * QSize( defaultFontHeight, defaultFontHeight ) ) );
CALAMARES_RETRANSLATE_FOR(
aboutDialog, aboutDialog->setText( QCoreApplication::translate( "calamares-sidebar", "About", "@button" ) );
aboutDialog->setToolTip(
QCoreApplication::translate( "calamares-sidebar", "Show information about Calamares", "@tooltip" ) ); );
extraButtons->addWidget( aboutDialog );
aboutDialog->setFlat( true );
aboutDialog->setCheckable( true );
QObject::connect( aboutDialog, &QPushButton::clicked, debug, &Calamares::DebugWindowManager::about );
}
if ( debug && debug->enabled() )
{
QPushButton* debugWindowBtn = new QPushButton;
debugWindowBtn->setObjectName( "debugButton" );
debugWindowBtn->setIcon( Calamares::defaultPixmap(
Calamares::Bugs, Calamares::Original, 2 * QSize( defaultFontHeight, defaultFontHeight ) ) );
CALAMARES_RETRANSLATE_FOR(
debugWindowBtn,
debugWindowBtn->setText( QCoreApplication::translate( "calamares-sidebar", "Debug", "@button" ) );
debugWindowBtn->setToolTip(
QCoreApplication::translate( "calamares-sidebar", "Show debug information", "@tooltip" ) ); );
extraButtons->addWidget( debugWindowBtn );
debugWindowBtn->setFlat( true );
debugWindowBtn->setCheckable( true );
QObject::connect( debugWindowBtn, &QPushButton::clicked, debug, &Calamares::DebugWindowManager::show );
QObject::connect(
debug, &Calamares::DebugWindowManager::visibleChanged, debugWindowBtn, &QPushButton::setChecked );
}
Calamares::unmarginLayout( sideLayout );
return sideBox;
}
static QWidget*
getWidgetNavigation( Calamares::DebugWindowManager*,
Calamares::ViewManager* viewManager,
QWidget* parent,
Qt::Orientation,
int )
{
QWidget* navigation = new QWidget( parent );
QBoxLayout* bottomLayout = new QHBoxLayout;
bottomLayout->addStretch();
// Create buttons and sets an initial icon; the icons may change
{
auto* back = new QPushButton(
getButtonIcon( QStringLiteral( "go-previous" ) ),
QCoreApplication::translate( CalamaresWindow::staticMetaObject.className(), "&Back", "@button" ),
navigation );
back->setObjectName( "view-button-back" );
back->setEnabled( viewManager->backEnabled() );
QObject::connect( back, &QPushButton::clicked, viewManager, &Calamares::ViewManager::back );
QObject::connect( viewManager, &Calamares::ViewManager::backEnabledChanged, back, &QPushButton::setEnabled );
QObject::connect( viewManager, &Calamares::ViewManager::backLabelChanged, back, &QPushButton::setText );
QObject::connect(
viewManager, &Calamares::ViewManager::backIconChanged, [ = ]( QString n ) { setButtonIcon( back, n ); } );
QObject::connect(
viewManager, &Calamares::ViewManager::backAndNextVisibleChanged, back, &QPushButton::setVisible );
bottomLayout->addWidget( back );
}
{
auto* next = new QPushButton(
getButtonIcon( QStringLiteral( "go-next" ) ),
QCoreApplication::translate( CalamaresWindow::staticMetaObject.className(), "&Next", "@button" ),
navigation );
next->setObjectName( "view-button-next" );
next->setEnabled( viewManager->nextEnabled() );
QObject::connect( next, &QPushButton::clicked, viewManager, &Calamares::ViewManager::next );
QObject::connect( viewManager, &Calamares::ViewManager::nextEnabledChanged, next, &QPushButton::setEnabled );
QObject::connect( viewManager, &Calamares::ViewManager::nextLabelChanged, next, &QPushButton::setText );
QObject::connect(
viewManager, &Calamares::ViewManager::nextIconChanged, [ = ]( QString n ) { setButtonIcon( next, n ); } );
QObject::connect(
viewManager, &Calamares::ViewManager::backAndNextVisibleChanged, next, &QPushButton::setVisible );
bottomLayout->addWidget( next );
}
bottomLayout->addSpacing( 12 );
{
auto* quit = new QPushButton(
getButtonIcon( QStringLiteral( "dialog-cancel" ) ),
QCoreApplication::translate( CalamaresWindow::staticMetaObject.className(), "&Cancel", "@button" ),
navigation );
quit->setObjectName( "view-button-cancel" );
QObject::connect( quit, &QPushButton::clicked, viewManager, &Calamares::ViewManager::quit );
QObject::connect( viewManager, &Calamares::ViewManager::quitEnabledChanged, quit, &QPushButton::setEnabled );
QObject::connect( viewManager, &Calamares::ViewManager::quitLabelChanged, quit, &QPushButton::setText );
QObject::connect(
viewManager, &Calamares::ViewManager::quitIconChanged, [ = ]( QString n ) { setButtonIcon( quit, n ); } );
QObject::connect( viewManager, &Calamares::ViewManager::quitTooltipChanged, quit, &QPushButton::setToolTip );
QObject::connect( viewManager, &Calamares::ViewManager::quitVisibleChanged, quit, &QPushButton::setVisible );
bottomLayout->addWidget( quit );
}
bottomLayout->setContentsMargins( 0, 0, 6, 6 );
navigation->setLayout( bottomLayout );
return navigation;
}
#ifdef WITH_QML
static inline void
setDimension( QQuickWidget* w, Qt::Orientation o, int desiredWidth )
{
w->setSizePolicy( o == Qt::Orientation::Vertical ? QSizePolicy::MinimumExpanding : QSizePolicy::Expanding,
o == Qt::Orientation::Horizontal ? QSizePolicy::MinimumExpanding : QSizePolicy::Expanding );
if ( o == Qt::Orientation::Vertical )
{
w->setFixedWidth( desiredWidth );
}
else
{
// If the QML itself sets a height, use that, otherwise go to 48 pixels
// which seems to match what the widget navigation would use for height
// (with *my* specific screen, style, etc. so YMMV).
//
// Bound between (16, 64) with a default of 48.
qreal minimumHeight = qBound( qreal( 16 ), w->rootObject() ? w->rootObject()->height() : 48, qreal( 64 ) );
w->setMinimumHeight( int( minimumHeight ) );
w->setFixedHeight( int( minimumHeight ) );
}
w->setResizeMode( QQuickWidget::SizeRootObjectToView );
}
static QWidget*
getQmlSidebar( Calamares::DebugWindowManager* debug,
Calamares::ViewManager*,
QWidget* parent,
Qt::Orientation o,
int desiredWidth )
{
Calamares::registerQmlModels();
QQuickWidget* w = new QQuickWidget( parent );
if ( debug )
{
w->engine()->rootContext()->setContextProperty( "debug", debug );
}
w->setSource(
QUrl( Calamares::searchQmlFile( Calamares::QmlSearch::Both, QStringLiteral( "calamares-sidebar" ) ) ) );
setDimension( w, o, desiredWidth );
return w;
}
static QWidget*
getQmlNavigation( Calamares::DebugWindowManager* debug,
Calamares::ViewManager*,
QWidget* parent,
Qt::Orientation o,
int desiredWidth )
{
Calamares::registerQmlModels();
QQuickWidget* w = new QQuickWidget( parent );
if ( debug )
{
w->engine()->rootContext()->setContextProperty( "debug", debug );
}
w->setSource(
QUrl( Calamares::searchQmlFile( Calamares::QmlSearch::Both, QStringLiteral( "calamares-navigation" ) ) ) );
setDimension( w, o, desiredWidth );
return w;
}
#else
// Bogus to keep the linker happy
//
// Calls to flavoredWidget() still refer to these *names*
// even if they are subsequently not used.
static QWidget*
getQmlSidebar( Calamares::DebugWindowManager*,
Calamares::ViewManager*,
QWidget* parent,
Qt::Orientation,
int desiredWidth )
{
return nullptr;
}
static QWidget*
getQmlNavigation( Calamares::DebugWindowManager*,
Calamares::ViewManager*,
QWidget* parent,
Qt::Orientation,
int desiredWidth )
{
return nullptr;
}
#endif
/**@brief Picks one of two methods to call
*
* Calls method (member function) @p widget or @p qml with arguments @p a
* on the given window, based on the flavor.
*/
template < typename widgetMaker, typename... args >
QWidget*
flavoredWidget( Calamares::Branding::PanelFlavor flavor,
Qt::Orientation o,
Calamares::DebugWindowManager* w,
QWidget* parent,
widgetMaker widget,
widgetMaker qml, // Only if WITH_QML is on
args... a )
{
#ifndef WITH_QML
Q_UNUSED( qml )
#endif
auto* viewManager = Calamares::ViewManager::instance();
switch ( flavor )
{
case Calamares::Branding::PanelFlavor::Widget:
return widget( w, viewManager, parent, o, a... );
#ifdef WITH_QML
case Calamares::Branding::PanelFlavor::Qml:
return qml( w, viewManager, parent, o, a... );
#endif
case Calamares::Branding::PanelFlavor::None:
return nullptr;
}
__builtin_unreachable();
}
/** @brief Adds widgets to @p layout if they belong on this @p side
*/
static inline void
insertIf( QBoxLayout* layout,
Calamares::Branding::PanelSide side,
QWidget* first,
Calamares::Branding::PanelSide firstSide )
{
if ( first && side == firstSide )
{
layout->addWidget( first );
}
}
CalamaresWindow::CalamaresWindow( QWidget* parent )
: QWidget( parent )
, m_debugManager( new Calamares::DebugWindowManager( this ) )
, m_viewManager( nullptr )
{
installEventFilter( Calamares::Retranslator::instance() );
// If we can never cancel, don't show the window-close button
if ( Calamares::Settings::instance()->disableCancel() )
{
setWindowFlag( Qt::WindowCloseButtonHint, false );
}
// %1 is the distribution name
CALAMARES_RETRANSLATE( const auto* branding = Calamares::Branding::instance();
setWindowTitle( Calamares::Settings::instance()->isSetupMode()
? tr( "%1 Setup Program" ).arg( branding->productName() )
: tr( "%1 Installer" ).arg( branding->productName() ) ); );
const Calamares::Branding* const branding = Calamares::Branding::instance();
using ImageEntry = Calamares::Branding::ImageEntry;
using Calamares::windowMinimumHeight;
using Calamares::windowMinimumWidth;
using Calamares::windowPreferredHeight;
using Calamares::windowPreferredWidth;
using PanelSide = Calamares::Branding::PanelSide;
// Needs to match what's checked in DebugWindow
this->setObjectName( "mainApp" );
QSize availableSize = desktopSize( this );
QSize minimumSize( qBound( windowMinimumWidth, availableSize.width(), windowPreferredWidth ),
qBound( windowMinimumHeight, availableSize.height(), windowPreferredHeight ) );
setMinimumSize( minimumSize );
cDebug() << "Available desktop" << availableSize << "minimum size" << minimumSize;
auto brandingSizes = branding->windowSize();
int w = qBound( minimumSize.width(), windowDimensionToPixels( brandingSizes.first ), availableSize.width() );
int h = qBound( minimumSize.height(), windowDimensionToPixels( brandingSizes.second ), availableSize.height() );
cDebug() << Logger::SubEntry << "Proposed window size:" << w << h;
resize( w, h );
QWidget* baseWidget = this;
if ( !( branding->imagePath( ImageEntry::ProductWallpaper ).isEmpty() ) )
{
QWidget* label = new QWidget( this );
QVBoxLayout* l = new QVBoxLayout;
Calamares::unmarginLayout( l );
l->addWidget( label );
setLayout( l );
label->setObjectName( "backgroundWidget" );
label->setStyleSheet(
QStringLiteral( "#backgroundWidget { background-image: url(%1); background-repeat: repeat-xy; }" )
.arg( branding->imagePath( ImageEntry::ProductWallpaper ) ) );
baseWidget = label;
}
m_viewManager = Calamares::ViewManager::instance( baseWidget );
if ( branding->windowExpands() )
{
connect( m_viewManager, &Calamares::ViewManager::ensureSize, this, &CalamaresWindow::ensureSize );
}
// NOTE: Although the ViewManager has a signal cancelEnabled() that
// signals when the state of the cancel button changes (in
// particular, to disable cancel during the exec phase),
// we don't connect to it here. Changing the window flag
// for the close button causes uncomfortable window flashing
// and requires an extra show() (at least with KWin/X11) which
// is too annoying. Instead, leave it up to ignoring-the-quit-
// event, which is also the ViewManager's responsibility.
QBoxLayout* mainLayout = new QHBoxLayout;
QBoxLayout* contentsLayout = new QVBoxLayout;
contentsLayout->setSpacing( 0 );
QWidget* sideBox
= flavoredWidget( branding->sidebarFlavor(),
::orientation( branding->sidebarSide() ),
m_debugManager,
baseWidget,
::getWidgetSidebar,
::getQmlSidebar,
qBound( 100, Calamares::defaultFontHeight() * 12, w < windowPreferredWidth ? 100 : 190 ) );
QWidget* navigation = flavoredWidget( branding->navigationFlavor(),
::orientation( branding->navigationSide() ),
m_debugManager,
baseWidget,
::getWidgetNavigation,
::getQmlNavigation,
64 );
// Build up the contentsLayout (a VBox) top-to-bottom
// .. note that the bottom is mirrored wrt. the top
insertIf( contentsLayout, PanelSide::Top, sideBox, branding->sidebarSide() );
insertIf( contentsLayout, PanelSide::Top, navigation, branding->navigationSide() );
contentsLayout->addWidget( m_viewManager->centralWidget() );
insertIf( contentsLayout, PanelSide::Bottom, navigation, branding->navigationSide() );
insertIf( contentsLayout, PanelSide::Bottom, sideBox, branding->sidebarSide() );
// .. and then the mainLayout left-to-right
insertIf( mainLayout, PanelSide::Left, sideBox, branding->sidebarSide() );
insertIf( mainLayout, PanelSide::Left, navigation, branding->navigationSide() );
mainLayout->addLayout( contentsLayout );
insertIf( mainLayout, PanelSide::Right, navigation, branding->navigationSide() );
insertIf( mainLayout, PanelSide::Right, sideBox, branding->sidebarSide() );
// layout->count() returns number of things in it; above we have put
// at **least** the central widget, which comes from the view manager,
// both vertically and horizontally -- so if there's a panel along
// either axis, the count in that axis will be > 1.
m_viewManager->setPanelSides(
( contentsLayout->count() > 1 ? Qt::Orientations( Qt::Horizontal ) : Qt::Orientations() )
| ( mainLayout->count() > 1 ? Qt::Orientations( Qt::Vertical ) : Qt::Orientations() ) );
Calamares::unmarginLayout( mainLayout );
Calamares::unmarginLayout( contentsLayout );
baseWidget->setLayout( mainLayout );
setStyleSheet( Calamares::Branding::instance()->stylesheet() );
}
void
CalamaresWindow::ensureSize( QSize size )
{
auto mainGeometry = this->geometry();
QSize availableSize = desktopSize( this );
// We only care about vertical sizes that are big enough
int embiggenment = qMax( 0, size.height() - m_viewManager->centralWidget()->size().height() );
if ( embiggenment < 6 )
{
return;
}
auto h = qBound( 0, mainGeometry.height() + embiggenment, availableSize.height() );
auto w = this->size().width();
resize( w, h );
}
void
CalamaresWindow::closeEvent( QCloseEvent* event )
{
if ( m_viewManager )
{
m_viewManager->quit();
// If it didn't actually exit, eat the event to ignore close
event->ignore();
}
else
{
event->accept();
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
QApplication::quit();
#else
QApplication::exit( EXIT_SUCCESS );
#endif
}
}

View File

@@ -0,0 +1,50 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARESWINDOW_H
#define CALAMARESWINDOW_H
#include <QWidget>
#include <memory>
namespace Calamares
{
class DebugWindowManager;
class ViewManager;
} // namespace Calamares
/**
* @brief The CalamaresWindow class represents the main window of the Calamares UI.
*/
class CalamaresWindow : public QWidget
{
Q_OBJECT
public:
CalamaresWindow( QWidget* parent = nullptr );
~CalamaresWindow() override {}
public Q_SLOTS:
/**
* This asks the main window to grow to accomodate @p size pixels, to accomodate
* larger-than-expected window contents. The enlargement may be silently
* ignored.
*/
void ensureSize( QSize size );
protected:
virtual void closeEvent( QCloseEvent* e ) override;
private:
Calamares::DebugWindowManager* m_debugManager = nullptr;
Calamares::ViewManager* m_viewManager = nullptr;
};
#endif // CALAMARESWINDOW_H

View File

@@ -0,0 +1,257 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2015-2016 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "DebugWindow.h"
#include "ui_DebugWindow.h"
#include "Branding.h"
#include "CalamaresAbout.h"
#include "CalamaresVersion.h"
#include "GlobalStorage.h"
#include "Job.h"
#include "JobQueue.h"
#include "Settings.h"
#include "VariantModel.h"
#include "modulesystem/Module.h"
#include "modulesystem/ModuleManager.h"
#include "utils/Gui.h"
#include "utils/Logger.h"
#include "utils/Paste.h"
#include "utils/Retranslator.h"
#include "widgets/TranslationFix.h"
#include <QMessageBox>
#include <QSplitter>
#include <QStringListModel>
#include <QTreeView>
#include <QWidget>
#include <signal.h>
#include <unistd.h>
/**
* @brief crash makes Calamares crash immediately.
*/
static void
crash()
{
kill( getpid(), SIGTRAP );
}
/// @brief Print out the widget tree (names) in indented form.
static void
dumpWidgetTree( QDebug& deb, const QWidget* widget, int depth )
{
if ( !widget )
{
return;
}
deb << Logger::Continuation;
for ( int i = 0; i < depth; ++i )
{
deb << ' ';
}
deb << widget->metaObject()->className() << widget->objectName();
for ( const auto* w : widget->findChildren< QWidget* >( QString(), Qt::FindDirectChildrenOnly ) )
{
dumpWidgetTree( deb, w, depth + 1 );
}
}
namespace Calamares
{
DebugWindow::DebugWindow()
: QWidget( nullptr )
, m_ui( new Ui::DebugWindow )
, m_globals( JobQueue::instance()->globalStorage()->data() )
, m_globals_model( std::make_unique< VariantModel >( &m_globals ) )
, m_module_model( std::make_unique< VariantModel >( &m_module ) )
{
GlobalStorage* gs = JobQueue::instance()->globalStorage();
m_ui->setupUi( this );
m_ui->globalStorageView->setModel( m_globals_model.get() );
m_ui->globalStorageView->expandAll();
// Do above when the GS changes, too
connect( gs,
&GlobalStorage::changed,
this,
[ = ]
{
m_globals = JobQueue::instance()->globalStorage()->data();
m_globals_model->reload();
m_ui->globalStorageView->expandAll();
} );
// JobQueue page
m_ui->jobQueueText->setReadOnly( true );
connect( JobQueue::instance(),
&JobQueue::queueChanged,
this,
[ this ]( const QStringList& jobs ) { m_ui->jobQueueText->setText( jobs.join( '\n' ) ); } );
// Modules page
QStringList modulesKeys;
for ( const auto& m : ModuleManager::instance()->loadedInstanceKeys() )
{
modulesKeys << m.toString();
}
QStringListModel* modulesModel = new QStringListModel( modulesKeys );
m_ui->modulesListView->setModel( modulesModel );
m_ui->modulesListView->setSelectionMode( QAbstractItemView::SingleSelection );
m_ui->moduleConfigView->setModel( m_module_model.get() );
connect( m_ui->modulesListView->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
[ this ]
{
QString moduleName = m_ui->modulesListView->currentIndex().data().toString();
Module* module
= ModuleManager::instance()->moduleInstance( ModuleSystem::InstanceKey::fromString( moduleName ) );
if ( module )
{
m_module = module->configurationMap();
m_module_model->reload();
m_ui->moduleConfigView->expandAll();
m_ui->moduleTypeLabel->setText( module->typeString() );
m_ui->moduleInterfaceLabel->setText( module->interfaceString() );
}
} );
// Tools page
connect( m_ui->crashButton, &QPushButton::clicked, this, [] { ::crash(); } );
connect( m_ui->reloadStylesheetButton,
&QPushButton::clicked,
[]()
{
for ( auto* w : qApp->topLevelWidgets() )
{
// Needs to match what's set in CalamaresWindow
if ( w->objectName() == QStringLiteral( "mainApp" ) )
{
w->setStyleSheet( Calamares::Branding::instance()->stylesheet() );
}
}
} );
connect( m_ui->widgetTreeButton,
&QPushButton::clicked,
[]()
{
for ( auto* w : qApp->topLevelWidgets() )
{
Logger::CDebug deb;
dumpWidgetTree( deb, w, 0 );
}
} );
// Send Log button only if it would be useful
m_ui->sendLogButton->setVisible( Calamares::Paste::isEnabled() );
connect( m_ui->sendLogButton, &QPushButton::clicked, [ this ]() { Calamares::Paste::doLogUploadUI( this ); } );
CALAMARES_RETRANSLATE( m_ui->retranslateUi( this ); setWindowTitle( tr( "Debug Information", "@title" ) ); );
}
void
DebugWindow::closeEvent( QCloseEvent* e )
{
Q_UNUSED( e )
emit closed();
}
DebugWindowManager::DebugWindowManager( QObject* parent )
: QObject( parent )
{
}
bool
DebugWindowManager::enabled() const
{
const auto* s = Settings::instance();
return ( Logger::logLevel() >= Logger::LOGVERBOSE ) || ( s ? s->debugMode() : false );
}
void
DebugWindowManager::show( bool visible )
{
if ( !enabled() )
{
visible = false;
}
if ( m_visible == visible )
{
return;
}
if ( visible )
{
m_debugWindow = new Calamares::DebugWindow();
m_debugWindow->show();
connect( m_debugWindow.data(),
&Calamares::DebugWindow::closed,
this,
[ = ]()
{
m_debugWindow->deleteLater();
m_visible = false;
emit visibleChanged( false );
} );
m_visible = true;
emit visibleChanged( true );
}
else
{
if ( m_debugWindow )
{
m_debugWindow->deleteLater();
}
m_visible = false;
emit visibleChanged( false );
}
}
void
DebugWindowManager::toggle()
{
show( !m_visible );
}
void
DebugWindowManager::about()
{
QString title = Calamares::Settings::instance()->isSetupMode()
? QCoreApplication::translate( "WelcomePage", "About %1 Setup", "@title" )
: QCoreApplication::translate( "WelcomePage", "About %1 Installer", "@title" );
QMessageBox mb( QMessageBox::Information,
title.arg( CALAMARES_APPLICATION_NAME ),
Calamares::aboutString().arg( Calamares::Branding::instance()->versionedName() ),
QMessageBox::Ok,
nullptr );
Calamares::fixButtonLabels( &mb );
mb.setIconPixmap(
Calamares::defaultPixmap( Calamares::Squid,
Calamares::Original,
QSize( Calamares::defaultFontHeight() * 6, Calamares::defaultFontHeight() * 6 ) ) );
QGridLayout* layout = reinterpret_cast< QGridLayout* >( mb.layout() );
if ( layout )
{
layout->setColumnMinimumWidth( 2, Calamares::defaultFontHeight() * 24 );
}
mb.exec();
}
} // namespace Calamares

View File

@@ -0,0 +1,96 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_DEBUGWINDOW_H
#define CALAMARES_DEBUGWINDOW_H
#include "VariantModel.h"
#include <QPointer>
#include <QVariant>
#include <QWidget>
#include <memory>
namespace Calamares
{
// From the .ui file
namespace Ui
{
class DebugWindow;
} // namespace Ui
class DebugWindow : public QWidget
{
Q_OBJECT
public:
explicit DebugWindow();
signals:
void closed();
protected:
void closeEvent( QCloseEvent* e ) override;
private:
Ui::DebugWindow* m_ui;
QVariant m_globals;
QVariant m_module;
std::unique_ptr< VariantModel > m_globals_model;
std::unique_ptr< VariantModel > m_module_model;
};
/** @brief Manager for meta-windows (Debug and About windows)
*
* Only one DebugWindow is expected to be around. This class manages
* (exactly one) DebugWindow and can create and destroy it as needed.
* It is available to the Calamares panels as object `DebugWindow`.
*
* The about() method shows a modal pop-up about Calamares.
*/
class DebugWindowManager : public QObject
{
Q_OBJECT
/// @brief Proxy to Settings::debugMode() default @c false
Q_PROPERTY( bool enabled READ enabled CONSTANT FINAL )
/** @brief Is the debug window visible?
*
* Writing @c true to this **may** make the debug window visible to
* the user; only if debugMode() is on.
*/
Q_PROPERTY( bool visible READ visible WRITE show NOTIFY visibleChanged )
public:
DebugWindowManager( QObject* parent = nullptr );
virtual ~DebugWindowManager() override = default;
public Q_SLOTS:
bool enabled() const;
bool visible() const { return m_visible; }
void show( bool visible );
void toggle();
void about();
signals:
void visibleChanged( bool visible );
private:
QPointer< DebugWindow > m_debugWindow;
bool m_visible = false;
};
} // namespace Calamares
#endif

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<author>
SPDX-FileCopyrightText: 2015 Teo Mrnjavac &lt;teo@kde.org&gt;
SPDX-License-Identifier: GPL-3.0-or-later
</author>
<class>Calamares::DebugWindow</class>
<widget class="QWidget" name="DebugWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>962</width>
<height>651</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="globalStorageTab">
<attribute name="title">
<string>GlobalStorage</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTreeView" name="globalStorageView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="jobQueueTab">
<attribute name="title">
<string>JobQueue</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="jobQueueText"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="modulesTab">
<attribute name="title">
<string>Modules</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListView" name="modulesListView"/>
</item>
<item>
<layout class="QVBoxLayout" name="modulesVerticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="moduleTypeLabel">
<property name="text">
<string>none</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="interfaceLabel">
<property name="text">
<string>Interface:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="moduleInterfaceLabel">
<property name="text">
<string>none</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="moduleConfigView"/>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="crashButton">
<property name="toolTip">
<string>Crashes Calamares, so that Dr. Konqi can look at it.</string>
</property>
<property name="text">
<string notr="true">Crash now</string>
</property>
<property name="icon">
<iconset theme="data-error"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reloadStylesheetButton">
<property name="toolTip">
<string>Reloads the stylesheet from the branding directory.</string>
</property>
<property name="text">
<string>Reload Stylesheet</string>
</property>
<property name="icon">
<iconset theme="preferences-web-browser-stylesheets"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="widgetTreeButton">
<property name="toolTip">
<string>Displays the tree of widget names in the log (for stylesheet debugging).</string>
</property>
<property name="text">
<string>Widget Tree</string>
</property>
<property name="icon">
<iconset theme="view-list-tree"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sendLogButton">
<property name="toolTip">
<string>Uploads the session log to the configured pastebin.</string>
</property>
<property name="text">
<string>Send Session Log</string>
</property>
<property name="icon">
<iconset theme="upload-media"/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,285 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "VariantModel.h"
#include "compat/Variant.h"
static bool
isMapLike( const QVariant& item )
{
return item.canConvert< QVariantMap >();
}
static bool
isListLike( const QVariant& item )
{
return item.canConvert< QVariantList >() && !( Calamares::typeOf( item ) == Calamares::StringVariantType );
}
static void
overallLength( const QVariant& item, quintptr& c, quintptr parent, VariantModel::IndexVector* skiplist )
{
if ( skiplist )
{
skiplist->append( parent );
}
parent = c++;
if ( isMapLike( item ) )
{
for ( const auto& subitem : item.toMap() )
{
overallLength( subitem, c, parent, skiplist );
}
}
else if ( isListLike( item ) )
{
for ( const auto& subitem : item.toList() )
{
overallLength( subitem, c, parent, skiplist );
}
}
}
static quintptr
findNth( const VariantModel::IndexVector& skiplist, quintptr value, int n )
{
constexpr const quintptr invalid_index = static_cast< quintptr >( -1 );
if ( n < 0 )
{
return invalid_index;
}
int index = static_cast< int >( value );
while ( ( n >= 0 ) && ( index < skiplist.count() ) )
{
if ( skiplist[ index ] == value )
{
if ( --n < 0 )
{
// It's bigger than 0
return static_cast< quintptr >( index );
}
}
index++;
}
return invalid_index;
}
VariantModel::VariantModel( const QVariant* p )
: m_p( p )
{
reload();
}
VariantModel::~VariantModel() {}
void
VariantModel::reload()
{
constexpr const quintptr invalid_index = static_cast< quintptr >( -1 );
quintptr x = 0;
m_rows.clear(); // Start over
if ( m_rows.capacity() < 64 )
{
m_rows.reserve( 64 ); // Start reasonably-sized
}
overallLength( *m_p, x, invalid_index, &m_rows );
}
int
VariantModel::columnCount( const QModelIndex& ) const
{
return 2;
}
int
VariantModel::rowCount( const QModelIndex& index ) const
{
quintptr p = index.isValid() ? index.internalId() : 0;
return m_rows.count( p );
}
QModelIndex
VariantModel::index( int row, int column, const QModelIndex& parent ) const
{
quintptr p = 0;
if ( parent.isValid() )
{
if ( inRange( parent ) )
{
p = parent.internalId();
}
}
return createIndex( row, column, findNth( m_rows, p, row ) );
}
static inline quintptr
deref( const VariantModel::IndexVector& v, quintptr i )
{
return v[ static_cast< int >( i ) ];
}
QModelIndex
VariantModel::parent( const QModelIndex& index ) const
{
if ( !index.isValid() || !inRange( index ) )
{
return QModelIndex();
}
quintptr p = deref( m_rows, index.internalId() );
if ( p == 0 )
{
return QModelIndex();
}
if ( !inRange( p ) )
{
return QModelIndex();
}
quintptr p_pid = deref( m_rows, p );
int row = 0;
for ( int i = static_cast< int >( p_pid ); i < static_cast< int >( p ); ++i )
{
if ( m_rows[ i ] == p_pid )
{
row++;
}
}
return createIndex( row, index.column(), p );
}
QVariant
VariantModel::data( const QModelIndex& index, int role ) const
{
if ( role != Qt::DisplayRole )
{
return QVariant();
}
if ( !index.isValid() )
{
return QVariant();
}
if ( ( index.column() < 0 ) || ( index.column() > 1 ) )
{
return QVariant();
}
if ( !inRange( index ) )
{
return QVariant();
}
const QVariant thing = underlying( parent( index ) );
if ( !thing.isValid() )
{
return QVariant();
}
if ( isMapLike( thing ) )
{
QVariantMap the_map = thing.toMap();
const auto key = the_map.keys().at( index.row() );
if ( index.column() == 0 )
{
return key;
}
else
{
return the_map[ key ];
}
}
else if ( isListLike( thing ) )
{
if ( index.column() == 0 )
{
return index.row();
}
else
{
QVariantList the_list = thing.toList();
return the_list.at( index.row() );
}
}
else
{
if ( index.column() == 0 )
{
return QVariant();
}
else
{
return thing;
}
}
}
QVariant
VariantModel::headerData( int section, Qt::Orientation orientation, int role ) const
{
if ( role != Qt::DisplayRole )
{
return QVariant();
}
if ( orientation == Qt::Horizontal )
{
if ( section == 0 )
{
return tr( "Key", "Column header for key/value" );
}
else if ( section == 1 )
{
return tr( "Value", "Column header for key/value" );
}
else
{
return QVariant();
}
}
else
{
return QVariant();
}
}
const QVariant
VariantModel::underlying( const QModelIndex& index ) const
{
if ( !index.isValid() )
{
return *m_p;
}
const auto& thing = underlying( parent( index ) );
if ( isMapLike( thing ) )
{
const auto& the_map = thing.toMap();
return the_map[ the_map.keys()[ index.row() ] ];
}
else if ( isListLike( thing ) )
{
return thing.toList()[ index.row() ];
}
else
{
return thing;
}
}

View File

@@ -0,0 +1,104 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef VARIANTMODEL_H
#define VARIANTMODEL_H
#include <QAbstractItemModel>
#include <QVariantMap>
#include <QVector>
/** @brief A model that operates directly on a QVariant
*
* A VariantModel operates directly on an underlying
* QVariant, treating QVariantMap and QVariantList as
* nodes with multiple children. In general, putting
* a QVariantMap into a QVariant and passing that into
* the model will get you a tree-like model of the
* VariantMap's data structure.
*
* Take care of object lifetimes and that the underlying
* QVariant does not change during use. If the QVariant
* **does** change, call reload() to re-build the internal
* representation of the tree.
*/
class VariantModel : public QAbstractItemModel
{
Q_OBJECT
public:
/** @brief Auxiliary data
*
* The nodes of the tree are enumerated into a vector
* (of length equal to the number of nodes in the tree + 1)
* which are used to do index and parent calculations.
*/
using IndexVector = QVector< quintptr >;
/** @brief Constructor
*
* The QVariant's lifetime is **not** affected by the model,
* so take care that the QVariant lives at least as long as
* the model). Also, don't change the QVariant underneath the model.
*/
VariantModel( const QVariant* p );
~VariantModel() override;
/** @brief Re-build the internal tree
*
* Call this when the underlying variant is changed, which
* might impact how the tree is laid out.
*/
void reload();
int columnCount( const QModelIndex& index ) const override;
int rowCount( const QModelIndex& index ) const override;
QModelIndex index( int row, int column, const QModelIndex& parent ) const override;
QModelIndex parent( const QModelIndex& index ) const override;
QVariant data( const QModelIndex& index, int role ) const override;
QVariant headerData( int section, Qt::Orientation orientation, int role ) const override;
private:
const QVariant* const m_p;
/** @brief Tree representation of the variant.
*
* At index 0 in the vector , we store -1 to indicate the root.
*
* Then we enumerate all the elements in the tree (by traversing
* the variant and using QVariantMap and QVariantList as having
* children, and everything else being a leaf node) and at the index
* for a child, store the index of its parent. This means that direct
* children of the root store a 0 in their indexes, children of the first
* child of the root store a 1, and we can "pointer chase" from an index
* through parents back to index 0.
*
* Because of this structure, the value stored at index i must be
* less than i (except for index 0, which is special). This makes it
* slightly easier to search for a given value *p*, because we can start
* at index *p* (or even *p+1*).
*
* Given an index *i* into the vector corresponding to a child, we know the
* parent, but can also count which row this child should have, by counting
* *other* indexes before *i* with the same parent (and by the ordering
* of values, we can start counting at index *parent-index*).
*
*/
IndexVector m_rows;
/// @brief Implementation of walking an index through the variant-tree
const QVariant underlying( const QModelIndex& index ) const;
/// @brief Helpers for range-checking
inline bool inRange( quintptr p ) const { return p < static_cast< quintptr >( m_rows.count() ); }
inline bool inRange( const QModelIndex& index ) const { return inRange( index.internalId() ); }
};
#endif

View File

@@ -0,0 +1,83 @@
/* Sample of QML navigation.
SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later
The navigation panel is generally "horizontal" in layout, with
buttons for next and previous; this particular one copies
the layout and size of the widgets panel.
*/
import io.calamares.ui 1.0
import io.calamares.core 1.0
import QtQuick 2.3
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
Rectangle {
id: navigationBar;
color: Branding.styleString( Branding.SidebarBackground );
height: 48;
RowLayout {
id: buttonBar
anchors.fill: parent;
Item
{
Layout.fillWidth: true;
}
Button
{
text: ViewManager.backLabel;
icon.name: ViewManager.backIcon;
enabled: ViewManager.backEnabled;
visible: ViewManager.backAndNextVisible;
onClicked: { ViewManager.back(); }
}
Button
{
text: ViewManager.nextLabel;
icon.name: ViewManager.nextIcon;
enabled: ViewManager.nextEnabled;
visible: ViewManager.backAndNextVisible;
onClicked: { ViewManager.next(); }
// This margin goes in the "next" button, because the "quit"
// button can vanish and we want to keep the margin to
// the next-thing-in-the-navigation-panel around.
Layout.rightMargin: 3 * buttonBar.spacing;
}
Button
{
Layout.rightMargin: 2 * buttonBar.spacing
text: ViewManager.quitLabel;
icon.name: ViewManager.quitIcon;
ToolTip.visible: hovered
ToolTip.timeout: 5000
ToolTip.delay: 1000
ToolTip.text: ViewManager.quitTooltip;
/*
* The ViewManager has settings -- user-controlled via the
* branding component, and party based on program state --
* whether the quit button should be enabled and visible.
*
* QML navigation *should* follow this pattern, but can also
* add other qualifications. For instance, you may have a
* "finished" module that handles quit in its own way, and
* want to hide the quit button then. The ViewManager has a
* current step and a total count, so compare them:
*
* visible: ViewManager.quitVisible && ( ViewManager.currentStepIndex < ViewManager.rowCount()-1);
*/
enabled: ViewManager.quitEnabled;
visible: ViewManager.quitVisible;
onClicked: { ViewManager.quit(); }
}
}
}

View File

@@ -0,0 +1,125 @@
/* Sample of QML progress tree.
SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
SPDX-License-Identifier: GPL-3.0-or-later
The progress tree (actually a list) is generally "vertical" in layout,
with the steps going "down", but it could also be a more compact
horizontal layout with suitable branding settings.
This example emulates the layout and size of the widgets progress tree.
*/
import io.calamares.ui 1.0
import io.calamares.core 1.0
import QtQuick 2.3
import QtQuick.Layouts 1.3
Rectangle {
id: sideBar;
color: Branding.styleString( Branding.SidebarBackground );
anchors.fill: parent;
ColumnLayout {
anchors.fill: parent;
spacing: 0;
Image {
Layout.topMargin: 12;
Layout.bottomMargin: 12;
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
id: logo;
width: 80;
height: width; // square
source: "file:/" + Branding.imagePath(Branding.ProductLogo);
sourceSize.width: width;
sourceSize.height: height;
}
Repeater {
model: ViewManager
Rectangle {
Layout.leftMargin: 6;
Layout.rightMargin: 6;
Layout.fillWidth: true;
height: 35;
radius: 6;
color: Branding.styleString( index == ViewManager.currentStepIndex ? Branding.SidebarBackgroundCurrent : Branding.SidebarBackground );
Text {
anchors.verticalCenter: parent.verticalCenter;
anchors.horizontalCenter: parent.horizontalCenter;
color: Branding.styleString( index == ViewManager.currentStepIndex ? Branding.SidebarTextCurrent : Branding.SidebarText );
text: display;
}
}
}
Item {
Layout.fillHeight: true;
}
Rectangle {
id: metaArea
Layout.fillWidth: true;
height: 35
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
color: Branding.styleString( Branding.SidebarBackground );
visible: true;
Rectangle {
id: aboutArea
height: 35
width: parent.width / 2;
anchors.left: parent.left
color: Branding.styleString( Branding.SidebarBackgroundCurrent );
visible: true;
MouseArea {
id: mouseAreaAbout
anchors.fill: parent;
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
Text {
anchors.verticalCenter: parent.verticalCenter;
anchors.horizontalCenter: parent.horizontalCenter;
x: parent.x + 4;
text: qsTr("About")
color: Branding.styleString( Branding.SidebarTextCurrent );
font.pointSize : 9
}
onClicked: debug.about()
}
}
Rectangle {
id: debugArea
height: 35
width: parent.width / 2;
anchors.right: parent.right
color: Branding.styleString( Branding.SidebarBackgroundCurrent );
visible: debug.enabled
MouseArea {
id: mouseAreaDebug
anchors.fill: parent;
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
Text {
anchors.verticalCenter: parent.verticalCenter;
anchors.horizontalCenter: parent.horizontalCenter;
x: parent.x + 4;
text: qsTr("Debug")
color: Branding.styleString( Branding.SidebarTextCurrent );
font.pointSize : 9
}
onClicked: debug.toggle()
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE RCC>
<!-- SPDX-FileCopyrightText: no
SPDX-License-Identifier: CC0-1.0
-->
<RCC version="1.0">
<qresource>
<file alias="calamares-sidebar.qml">calamares-sidebar.qml</file>
<file alias="calamares-navigation.qml">calamares-navigation.qml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,154 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "CalamaresApplication.h"
#include "Settings.h"
#include "utils/Dirs.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
// From 3rdparty/
#include "kdsingleapplication.h"
#include <KAboutData>
#ifdef BUILD_CRASH_REPORTING
#include <KCrash>
#endif
#include <QCommandLineParser>
#include <QDebug>
#include <QDir>
#include <memory>
/** @brief Gets debug-level from -D command-line-option
*
* If unset, use LOGERROR (corresponding to -D1), although
* effectively -D2 is the lowest level you can set for
* logging-to-the-console, and everything always gets
* logged to the session file).
*/
static unsigned int
debug_level( QCommandLineParser& parser, QCommandLineOption& levelOption )
{
if ( !parser.isSet( levelOption ) )
{
return Logger::LOGERROR;
}
bool ok = true;
int l = parser.value( levelOption ).toInt( &ok );
if ( !ok || ( l < 0 ) )
{
return Logger::LOGVERBOSE;
}
else
{
return static_cast< unsigned int >( l ); // l >= 0
}
}
/** @brief Handles the command-line arguments
*
* Sets up internals for Calamares based on command-line arguments like `-D`,
* `-d`, etc. Returns @c true if this is a *debug* run, i.e. if the `-d`
* command-line flag is given, @c false otherwise.
*/
static bool
handle_args( CalamaresApplication& a )
{
QCommandLineOption debugOption( QStringList { "d", "debug" },
"Also look in current directory for configuration. Implies -D8." );
QCommandLineOption debugLevelOption(
QStringLiteral( "D" ), "Verbose output for debugging purposes (0-8).", "level" );
QCommandLineOption debugTxOption( QStringList { "T", "debug-translation" },
"Also look in the current directory for translation." );
QCommandLineOption configOption(
QStringList { "c", "config" }, "Configuration directory to use, for testing purposes.", "config" );
QCommandLineOption xdgOption( QStringList { "X", "xdg-config" }, "Use XDG_{CONFIG,DATA}_DIRS as well." );
QCommandLineParser parser;
parser.setApplicationDescription( "Distribution-independent installer framework" );
parser.addHelpOption();
parser.addVersionOption();
parser.addOption( debugOption );
parser.addOption( debugLevelOption );
parser.addOption( configOption );
parser.addOption( xdgOption );
parser.addOption( debugTxOption );
parser.process( a );
Logger::setupLogLevel( parser.isSet( debugOption ) ? Logger::LOGVERBOSE : debug_level( parser, debugLevelOption ) );
if ( parser.isSet( configOption ) )
{
Calamares::setAppDataDir( QDir( parser.value( configOption ) ) );
}
if ( parser.isSet( xdgOption ) )
{
Calamares::setXdgDirs();
}
Calamares::setAllowLocalTranslation( parser.isSet( debugOption ) || parser.isSet( debugTxOption ) );
return parser.isSet( debugOption );
}
int
main( int argc, char* argv[] )
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
// Not needed in Qt6
QApplication::setAttribute( Qt::AA_EnableHighDpiScaling );
#endif
CalamaresApplication a( argc, argv );
KAboutData aboutData( "calamares",
"Calamares",
a.applicationVersion(),
"The universal system installer",
KAboutLicense::GPL_V3,
QString(),
QString(),
"https://calamares.io",
"https://codeberg.org/Calamares/calamares/issues" );
KAboutData::setApplicationData( aboutData );
a.setApplicationDisplayName( QString() ); // To avoid putting an extra "Calamares/" into the log-file
#ifdef BUILD_CRASH_REPORTING
KCrash::initialize();
// KCrash::setCrashHandler();
KCrash::setDrKonqiEnabled( true );
KCrash::setFlags( KCrash::SaferDialog | KCrash::AlwaysDirectly );
#endif
std::unique_ptr< KDSingleApplication > possiblyUnique;
const bool is_debug = handle_args( a );
if ( !is_debug )
{
possiblyUnique = std::make_unique< KDSingleApplication >();
if ( !possiblyUnique->isPrimaryInstance() )
{
qCritical() << "Calamares is already running.";
return 87; // EUSERS on Linux
}
}
Calamares::Settings::init( is_debug );
if ( !Calamares::Settings::instance() || !Calamares::Settings::instance()->isValid() )
{
qCritical() << "Calamares has invalid settings, shutting down.";
return 78; // EX_CONFIG on FreeBSD
}
a.init();
return a.exec();
}

View File

@@ -0,0 +1,119 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "ProgressTreeDelegate.h"
#include "Branding.h"
#include "CalamaresApplication.h"
#include "CalamaresWindow.h"
#include "ViewManager.h"
#include "utils/Gui.h"
#include <QPainter>
static constexpr int const item_margin = 8;
static inline int
item_fontsize()
{
return Calamares::defaultFontSize() + 4;
}
static void
paintViewStep( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index )
{
QRect textRect = option.rect.adjusted( item_margin, item_margin, -item_margin, -item_margin );
QFont font = qApp->font();
font.setPointSize( item_fontsize() );
font.setBold( false );
painter->setFont( font );
if ( index.row() == index.data( Calamares::ViewManager::ProgressTreeItemCurrentIndex ).toInt() )
{
painter->setPen( Calamares::Branding::instance()->styleString( Calamares::Branding::SidebarTextCurrent ) );
QString textHighlight
= Calamares::Branding::instance()->styleString( Calamares::Branding::SidebarBackgroundCurrent );
if ( textHighlight.isEmpty() )
{
painter->setBrush( CalamaresApplication::instance()->mainWindow()->palette().window() );
}
else
{
painter->setBrush( QColor( textHighlight ) );
}
}
// Draw the text at least once. If it doesn't fit, then shrink the font
// being used by 1 pt on each iteration, up to a maximum of maximumShrink
// times. On each loop, we'll have to blank out the rectangle again, so this
// is an expensive (in terms of drawing operations) thing to do.
//
// (The loop uses <= because the counter is incremented at the start).
static constexpr int const maximumShrink = 4;
int shrinkSteps = 0;
do
{
painter->fillRect( option.rect, painter->brush().color() );
shrinkSteps++;
QRectF boundingBox;
painter->drawText(
textRect, Qt::AlignHCenter | Qt::AlignVCenter | Qt::TextSingleLine, index.data().toString(), &boundingBox );
// The extra check here is to avoid the changing-font-size if we're not going to use
// it in the next iteration of the loop anyway.
if ( ( shrinkSteps <= maximumShrink ) && ( boundingBox.width() > textRect.width() ) )
{
font.setPointSize( item_fontsize() - shrinkSteps );
painter->setFont( font );
}
else
{
break; // It fits
}
} while ( shrinkSteps <= maximumShrink );
}
QSize
ProgressTreeDelegate::sizeHint( const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
if ( !index.isValid() )
{
return option.rect.size();
}
QFont font = qApp->font();
font.setPointSize( item_fontsize() );
QFontMetrics fm( font );
int height = fm.height();
height += 2 * item_margin;
return QSize( option.rect.width(), height );
}
void
ProgressTreeDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
QStyleOptionViewItem opt = option;
painter->save();
initStyleOption( &opt, index );
opt.text.clear();
painter->setBrush(
QColor( Calamares::Branding::instance()->styleString( Calamares::Branding::SidebarBackground ) ) );
painter->setPen( QColor( Calamares::Branding::instance()->styleString( Calamares::Branding::SidebarText ) ) );
paintViewStep( painter, opt, index );
painter->restore();
}

View File

@@ -0,0 +1,31 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef PROGRESSTREEDELEGATE_H
#define PROGRESSTREEDELEGATE_H
#include <QStyledItemDelegate>
/**
* @brief The ProgressTreeDelegate class customizes the look and feel of the
* ProgressTreeView elements.
* @see ProgressTreeView
*/
class ProgressTreeDelegate : public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
QSize sizeHint( const QStyleOptionViewItem& option, const QModelIndex& index ) const override;
void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const override;
};
#endif // PROGRESSTREEDELEGATE_H

View File

@@ -0,0 +1,61 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "ProgressTreeView.h"
#include "ProgressTreeDelegate.h"
#include "Branding.h"
#include "ViewManager.h"
ProgressTreeView::ProgressTreeView( QWidget* parent )
: QListView( parent )
{
this->setObjectName( "sidebarMenuApp" );
setFrameShape( QFrame::NoFrame );
setContentsMargins( 0, 0, 0, 0 );
setSelectionMode( QAbstractItemView::NoSelection );
setDragDropMode( QAbstractItemView::NoDragDrop );
setAcceptDrops( false );
setItemDelegate( new ProgressTreeDelegate( this ) );
QPalette plt = palette();
plt.setColor( QPalette::Base,
Calamares::Branding::instance()->styleString( Calamares::Branding::SidebarBackground ) );
setPalette( plt );
}
ProgressTreeView::~ProgressTreeView() {}
void
ProgressTreeView::setModel( QAbstractItemModel* model )
{
if ( ProgressTreeView::model() )
{
return;
}
QListView::setModel( model );
connect( Calamares::ViewManager::instance(),
&Calamares::ViewManager::currentStepChanged,
this,
&ProgressTreeView::update,
Qt::UniqueConnection );
}
void
ProgressTreeView::update()
{
viewport()->update();
}

View File

@@ -0,0 +1,39 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef PROGRESSTREEVIEW_H
#define PROGRESSTREEVIEW_H
#include <QListView>
/**
* @brief Displays progress through the list of (visible) steps
*
* The ProgressTreeView class is a modified QListView which displays the
* available view steps and the user's progress through them.
* Since Calamares doesn't support "sub steps", it isn't really a tree.
*/
class ProgressTreeView : public QListView
{
Q_OBJECT
public:
explicit ProgressTreeView( QWidget* parent = nullptr );
~ProgressTreeView() override;
/**
* @brief setModel assigns a model to this view.
*/
void setModel( QAbstractItemModel* model ) override;
public Q_SLOTS:
void update();
};
#endif // PROGRESSTREEVIEW_H

View File

@@ -0,0 +1,109 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
/**
* This is a test-application that just checks the YAML config-file
* shipped with each module for correctness -- well, for parseability.
*/
#include "utils/Yaml.h"
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <QByteArray>
#include <QFile>
using std::cerr;
static const char usage[] = "Usage: test_conf [-v] [-b] <file> ...\n";
int
main( int argc, char** argv )
{
bool verbose = false;
bool bytes = false;
int opt;
while ( ( opt = getopt( argc, argv, "vb" ) ) != -1 )
{
switch ( opt )
{
case 'v':
verbose = true;
break;
case 'b':
bytes = true;
break;
default: /* '?' */
cerr << usage;
return 1;
}
}
if ( optind >= argc )
{
cerr << usage;
return 1;
}
const char* filename = argv[ optind ];
try
{
YAML::Node doc;
if ( bytes )
{
QFile f( filename );
if ( f.open( QFile::ReadOnly | QFile::Text ) )
{
doc = YAML::Load( f.readAll().constData() );
}
}
else
{
doc = YAML::LoadFile( filename );
}
if ( doc.IsNull() )
{
// Special case: empty config files are valid,
// but aren't a map. For the example configs,
// this is still an error.
cerr << "WARNING:" << filename << '\n';
cerr << "WARNING: empty YAML\n";
return 1;
}
if ( !doc.IsMap() )
{
cerr << "WARNING:" << filename << '\n';
cerr << "WARNING: not-a-YAML-map (type=" << doc.Type() << ")\n";
return 1;
}
if ( verbose )
{
cerr << "Keys:\n";
for ( auto i = doc.begin(); i != doc.end(); ++i )
{
cerr << i->first.as< std::string >() << '\n';
}
}
}
catch ( YAML::Exception& e )
{
cerr << "WARNING:" << filename << '\n';
cerr << "WARNING: YAML parser error " << e.what() << '\n';
return 1;
}
return 0;
}

View File

@@ -0,0 +1,573 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
/*
* This executable loads and runs a Calamares Python module
* within a C++ application, in order to test the different
* bindings.
*/
#include "Branding.h"
#include "CppJob.h"
#include "GlobalStorage.h"
#include "Job.h"
#include "JobQueue.h"
#include "Settings.h"
#include "ViewManager.h"
#include "modulesystem/Module.h"
#include "modulesystem/ModuleManager.h"
#include "modulesystem/ViewModule.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
#include "utils/System.h"
#include "utils/Yaml.h"
#include "viewpages/ExecutionViewStep.h"
// Optional features of Calamares
// - Python support with pybind11
// - Python support with older Boost implementation
// - QML support
#ifdef WITH_PYTHON
#ifdef WITH_PYBIND11
#include "pybind11/PythonJob.h"
#else
#include "pyboost/PythonJob.h"
#endif
#endif
#ifdef WITH_QML
#include "utils/Qml.h"
#endif
#include <QApplication>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QFileInfo>
#include <QLabel>
#include <QMainWindow>
#include <QThread>
#include <QTimer>
#include <memory>
struct ModuleConfig
{
QString moduleName() const { return m_module; }
QString configFile() const { return m_jobConfig; }
QString language() const { return m_language; }
QString globalConfigFile() const { return m_globalConfig; }
QString m_module;
QString m_jobConfig;
QString m_globalConfig;
QString m_settingsConfig;
QString m_language;
QString m_branding;
bool m_ui;
bool m_pythonInjection;
};
static ModuleConfig
handle_args( QCoreApplication& a )
{
QCommandLineOption debugLevelOption(
QStringLiteral( "D" ), "Verbose output for debugging purposes (0-8), ignored.", "level" );
QCommandLineOption settingsOption( { QStringLiteral( "S" ), QStringLiteral( "settings" ) },
QStringLiteral( "Settings.conf document" ),
QString( "settings.conf" ) );
QCommandLineOption globalOption( { QStringLiteral( "g" ), QStringLiteral( "global" ) },
QStringLiteral( "Global storage settings document" ),
"global.yaml" );
QCommandLineOption jobOption(
{ QStringLiteral( "j" ), QStringLiteral( "job" ) }, QStringLiteral( "Job settings document" ), "job.yaml" );
QCommandLineOption langOption( { QStringLiteral( "l" ), QStringLiteral( "language" ) },
QStringLiteral( "Language (global)" ),
"languagecode" );
QCommandLineOption brandOption( { QStringLiteral( "b" ), QStringLiteral( "branding" ) },
QStringLiteral( "Branding directory" ),
"path/to/branding.desc",
"src/branding/default/branding.desc" );
QCommandLineOption uiOption( { QStringLiteral( "U" ), QStringLiteral( "ui" ) }, QStringLiteral( "Enable UI" ) );
QCommandLineOption slideshowOption( { QStringLiteral( "s" ), QStringLiteral( "slideshow" ) },
QStringLiteral( "Run slideshow module" ) );
QCommandLineParser parser;
parser.setApplicationDescription( "Calamares module tester" );
parser.addHelpOption();
parser.addVersionOption();
parser.addOption( debugLevelOption );
parser.addOption( settingsOption );
parser.addOption( globalOption );
parser.addOption( jobOption );
parser.addOption( langOption );
parser.addOption( brandOption );
parser.addOption( uiOption );
parser.addOption( slideshowOption );
#ifdef WITH_PYTHON
QCommandLineOption pythonOption( { QStringLiteral( "P" ), QStringLiteral( "no-injected-python" ) },
QStringLiteral( "Do not disable potentially-harmful Python commands" ) );
parser.addOption( pythonOption );
#endif
parser.addPositionalArgument( "module", "Path or name of module to run." );
parser.addPositionalArgument( "job.yaml", "Path of job settings document to use.", "[job.yaml]" );
parser.process( a );
const QStringList args = parser.positionalArguments();
if ( args.isEmpty() && !parser.isSet( slideshowOption ) )
{
cError() << "Missing <module> path.\n";
parser.showHelp();
}
else if ( args.size() > 2 )
{
cError() << "More than one <module> path.\n";
parser.showHelp();
}
else
{
QString jobSettings( parser.value( jobOption ) );
if ( jobSettings.isEmpty() && ( args.size() == 2 ) )
{
jobSettings = args.at( 1 );
}
bool pythonInjection = true;
#ifdef WITH_PYTHON
if ( parser.isSet( pythonOption ) )
{
pythonInjection = false;
}
#endif
return ModuleConfig { parser.isSet( slideshowOption ) ? QStringLiteral( "-" ) : args.first(),
jobSettings,
parser.value( globalOption ),
parser.value( settingsOption ),
parser.value( langOption ),
parser.value( brandOption ),
parser.isSet( slideshowOption ) || parser.isSet( uiOption ),
pythonInjection };
}
}
/** @brief Bogus Job for --slideshow option
*
* Generally one would use DummyCppJob for this kind of dummy
* job, but that class lives in a module so isn't available
* in this test application.
*
* This bogus job just sleeps for 3.
*/
class ExecViewJob : public Calamares::CppJob
{
public:
explicit ExecViewJob( const QString& name, unsigned long t = 3 )
: m_name( name )
, m_delay( t )
{
}
~ExecViewJob() override;
QString prettyName() const override { return m_name; }
Calamares::JobResult exec() override
{
QThread::sleep( m_delay );
return Calamares::JobResult::ok();
}
void setConfigurationMap( const QVariantMap& ) override {}
private:
QString m_name;
unsigned long m_delay;
};
ExecViewJob::~ExecViewJob() {}
/** @brief Bogus module for --slideshow option
*
* Normally the slideshow -- displayed by ExecutionViewStep -- is not
* associated with any particular module in the Calamares configuration.
* It is added internally by the module manager. For the module-loader
* testing application, we need something that pretends to be the
* module for the ExecutionViewStep.
*/
class ExecViewModule : public Calamares::Module
{
public:
ExecViewModule();
~ExecViewModule() override;
void loadSelf() override;
virtual Calamares::ModuleSystem::Type type() const override;
virtual Calamares::ModuleSystem::Interface interface() const override;
virtual Calamares::JobList jobs() const override;
protected:
void initFrom( const Calamares::ModuleSystem::Descriptor& ) override;
};
ExecViewModule::ExecViewModule()
: Calamares::Module()
{
// Normally the module-loader gives the module an instance key
// (out of the settings file, or the descriptor of the module).
// We don't have one, so build one -- this gives us "execView@execView".
QVariantMap m;
const QString execView = QStringLiteral( "execView" );
m.insert( "name", execView );
Calamares::Module::initFrom( Calamares::ModuleSystem::Descriptor::fromDescriptorData( m, execView ), execView );
}
ExecViewModule::~ExecViewModule() {}
void
ExecViewModule::initFrom( const Calamares::ModuleSystem::Descriptor& )
{
}
void
ExecViewModule::loadSelf()
{
auto* viewStep = new Calamares::ExecutionViewStep();
viewStep->setModuleInstanceKey( instanceKey() );
viewStep->setConfigurationMap( m_configurationMap );
viewStep->appendJobModuleInstanceKey( instanceKey() );
Calamares::ViewManager::instance()->addViewStep( viewStep );
m_loaded = true;
}
Calamares::Module::Type
ExecViewModule::type() const
{
return Module::Type::View;
}
Calamares::Module::Interface
ExecViewModule::interface() const
{
return Module::Interface::QtPlugin;
}
Calamares::JobList
ExecViewModule::jobs() const
{
Calamares::JobList l;
const auto* gs = Calamares::JobQueue::instance()->globalStorage();
if ( gs && gs->contains( "jobs" ) )
{
QVariantList joblist = gs->value( "jobs" ).toList();
for ( const auto& jd : joblist )
{
QVariantMap jobdescription = jd.toMap();
if ( jobdescription.contains( "name" ) && jobdescription.contains( "delay" ) )
{
l.append( Calamares::job_ptr( new ExecViewJob( jobdescription.value( "name" ).toString(),
jobdescription.value( "delay" ).toULongLong() ) ) );
}
}
}
if ( l.count() > 0 )
{
return l;
}
l.append( Calamares::job_ptr( new ExecViewJob( QStringLiteral( "step 1" ) ) ) );
l.append( Calamares::job_ptr( new ExecViewJob( QStringLiteral( "step two" ) ) ) );
l.append( Calamares::job_ptr( new ExecViewJob( QStringLiteral( "locking mutexes" ), 20 ) ) );
l.append( Calamares::job_ptr( new ExecViewJob( QStringLiteral( "unlocking mutexes" ), 1 ) ) );
for ( const QString& s : QStringList { "Harder", "Better", "Faster", "Stronger" } )
{
l.append( Calamares::job_ptr( new ExecViewJob( s, 0 ) ) );
}
l.append( Calamares::job_ptr( new ExecViewJob( QStringLiteral( "cleaning up" ), 20 ) ) );
return l;
}
static Calamares::Module*
load_module( const ModuleConfig& moduleConfig )
{
QString moduleName = moduleConfig.moduleName();
if ( moduleName == "-" )
{
return new ExecViewModule;
}
QFileInfo fi; // This is kept around to hold the path of the module descriptor
bool ok = false;
QVariantMap descriptor;
QStringList moduleDirectories { "./", "src/modules/", "modules/", CMAKE_INSTALL_FULL_LIBDIR "/calamares/modules/" };
for ( const QString& prefix : std::as_const( moduleDirectories ) )
{
// Could be a complete path, eg. src/modules/dummycpp/module.desc
fi = QFileInfo( prefix + moduleName );
if ( fi.exists() && fi.isFile() )
{
descriptor = Calamares::YAML::load( fi, &ok );
}
if ( ok )
{
break;
}
// Could be a path without module.desc
fi = QFileInfo( prefix + moduleName );
if ( fi.exists() && fi.isDir() )
{
fi = QFileInfo( prefix + moduleName + "/module.desc" );
if ( fi.exists() && fi.isFile() )
{
descriptor = Calamares::YAML::load( fi, &ok );
}
if ( ok )
{
break;
}
else
{
if ( !fi.exists() )
{
cDebug() << "Expected a descriptor file" << fi.path();
}
else
{
cDebug() << "Read descriptor" << fi.path() << "and it was empty.";
}
}
}
}
if ( !ok )
{
cWarning() << "No suitable module descriptor found in" << Logger::DebugList( moduleDirectories );
return nullptr;
}
QString name = descriptor.value( "name" ).toString();
if ( name.isEmpty() )
{
cWarning() << "No name found in module descriptor" << fi.absoluteFilePath();
return nullptr;
}
QString moduleDirectory = fi.absolutePath();
QString configFile( moduleConfig.configFile().isEmpty() ? moduleDirectory + '/' + name + ".conf"
: moduleConfig.configFile() );
cDebug() << Logger::SubEntry << "Module" << moduleName << "job-configuration:" << configFile;
Calamares::Module* module = Calamares::moduleFromDescriptor(
Calamares::ModuleSystem::Descriptor::fromDescriptorData( descriptor, fi.absoluteFilePath() ),
name,
configFile,
moduleDirectory );
return module;
}
static bool
is_ui_option( const char* s )
{
return !qstrcmp( s, "--ui" ) || !qstrcmp( s, "-U" );
}
static bool
is_slideshow_option( const char* s )
{
return !qstrcmp( s, "--slideshow" ) || !qstrcmp( s, "-s" );
}
/** @brief Create the right kind of QApplication
*
* Does primitive parsing of argv[] to find the --ui option and returns
* a UI-enabled application if it does.
*
* @p argc must be a reference (to main's argc) because the QCoreApplication
* constructors take a reference as well, and that would otherwise be a
* reference to a temporary.
*/
QCoreApplication*
createApplication( int& argc, char* argv[] )
{
for ( int i = 1; i < argc; ++i )
{
if ( is_slideshow_option( argv[ i ] ) || is_ui_option( argv[ i ] ) )
{
auto* aw = new QApplication( argc, argv );
aw->setQuitOnLastWindowClosed( true );
return aw;
}
}
return new QCoreApplication( argc, argv );
}
#ifdef WITH_PYTHON
static const char pythonPreScript[] = R"%(
# This is Python code executed by Python modules *before* the
# script file (e.g. main.py) is executed.
#
# Calls to suprocess methods that execute something are
# suppressed and logged -- scripts should really be using libcalamares
# methods instead.
_calamares_subprocess = __import__("subprocess", globals(), locals(), [], 0)
import sys
import libcalamares
class fake_subprocess(object):
PIPE = object()
STDOUT = object()
STDERR = object()
class CompletedProcess(object):
returncode = 0
stdout = ""
stderr = ""
@staticmethod
def call(*args, **kwargs):
libcalamares.utils.debug("subprocess.call(%r,%r) X ignored" % (args, kwargs))
return 0
@staticmethod
def check_call(*args, **kwargs):
libcalamares.utils.debug("subprocess.check_call(%r,%r) X ignored" % (args, kwargs))
return 0
# This is a 3.5-and-later method, is supposed to return a CompletedProcess
@staticmethod
def run(*args, **kwargs):
libcalamares.utils.debug("subprocess.run(%r,%r) X ignored" % (args, kwargs))
return fake_subprocess.CompletedProcess()
for attr in ("CalledProcessError",):
setattr(fake_subprocess,attr,getattr(_calamares_subprocess,attr))
sys.modules["subprocess"] = fake_subprocess
libcalamares.utils.debug('pre-script for testing purposes injected')
)%";
#endif
int
main( int argc, char* argv[] )
{
QCoreApplication* application = createApplication( argc, argv );
Logger::setupLogLevel( Logger::LOGVERBOSE );
ModuleConfig module = handle_args( *application );
if ( module.moduleName().isEmpty() )
{
return 1;
}
std::unique_ptr< Calamares::Settings > settings_p( Calamares::Settings::init( module.m_settingsConfig ) );
std::unique_ptr< Calamares::JobQueue > jobqueue_p( new Calamares::JobQueue( nullptr ) );
std::unique_ptr< Calamares::System > system_p( new Calamares::System( settings_p->doChroot() ) );
QMainWindow* mainWindow = nullptr;
auto* gs = jobqueue_p->globalStorage();
if ( !module.globalConfigFile().isEmpty() )
{
gs->loadYaml( module.globalConfigFile() );
}
if ( !module.language().isEmpty() )
{
QVariantMap vm;
vm.insert( "LANG", module.language() );
gs->insert( "localeConf", vm );
}
#ifdef WITH_PYTHON
if ( module.m_pythonInjection )
{
#ifdef WITH_PYBIND11
Calamares::Python::Job::setInjectedPreScript( pythonPreScript );
#else
// Old Boost approach
Calamares::PythonJob::setInjectedPreScript( pythonPreScript );
#endif
}
#endif
#ifdef WITH_QML
Calamares::initQmlModulesDir(); // don't care if failed
#endif
cDebug() << "Calamares module-loader testing" << module.moduleName();
std::unique_ptr<Calamares::Module> m( load_module( module ) );
std::unique_ptr<Calamares::ModuleManager> modulemanager;
if ( !m )
{
cError() << "Could not load module" << module.moduleName();
return 1;
}
cDebug() << Logger::SubEntry << "got" << m->name() << m->typeString() << m->interfaceString();
if ( m->type() == Calamares::Module::Type::View )
{
// If we forgot the --ui, any ViewModule will core dump as it
// tries to create the widget **which won't be used anyway**.
//
// To avoid that crash, re-create the QApplication, now with GUI
if ( !qobject_cast< QApplication* >( application ) )
{
auto* replace_app = new QApplication( argc, argv );
replace_app->setQuitOnLastWindowClosed( true );
application = replace_app;
}
mainWindow = module.m_ui ? new QMainWindow() : nullptr;
if ( mainWindow )
{
mainWindow->installEventFilter( Calamares::Retranslator::instance() );
}
(void)new Calamares::Branding( module.m_branding );
modulemanager = std::make_unique<Calamares::ModuleManager>( QStringList(), nullptr );
(void)Calamares::ViewManager::instance( mainWindow );
modulemanager->addModule( m.release() ); // Transfers ownership
}
if ( !m->isLoaded() )
{
m->loadSelf();
}
if ( !m->isLoaded() )
{
cError() << "Module" << module.moduleName() << "could not be loaded.";
return 1;
}
if ( mainWindow )
{
auto* vm = Calamares::ViewManager::instance();
vm->onInitComplete();
QWidget* w = vm->currentStep()->widget();
w->setParent( mainWindow );
mainWindow->setCentralWidget( w );
w->show();
mainWindow->show();
return application->exec();
}
using TR = Logger::DebugRow< const char*, const QString >;
cDebug() << Logger::SubEntry << "Module metadata" << TR( "name", m->name() ) << TR( "type", m->typeString() )
<< TR( "interface", m->interfaceString() );
Calamares::JobQueue::instance()->enqueue( 100, m->jobs() );
QObject::connect( Calamares::JobQueue::instance(),
&Calamares::JobQueue::finished,
[ application ]()
{ QTimer::singleShot( std::chrono::seconds( 3 ), application, &QApplication::quit ); } );
QTimer::singleShot( 0, []() { Calamares::JobQueue::instance()->start(); } );
return application->exec();
}

View File

@@ -0,0 +1,255 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
#
# libcalamares is the non-GUI part of Calamares, which includes handling
# translations, configurations, logging, utilities, global storage, and
# (non-GUI) jobs.
#
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CalamaresConfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresConfig.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersion.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersion.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersionX.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersionX.h)
# Map the available translations names into a suitable constexpr list
# of names in C++. This gets us Calamares::Locale::availableLanguages,
# a QStringList of names.
set(_names_tu
"
#ifndef CALAMARES_TRANSLATIONS_H
#define CALAMARES_TRANSLATIONS_H
#include <QStringList>
namespace {
static const QStringList availableLanguageList{
"
)
foreach(l ${CALAMARES_TRANSLATION_LANGUAGES})
string(APPEND _names_tu "\"${l}\",\n")
endforeach()
string(APPEND _names_tu "};\n} // namespace\n#endif\n\n")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/CalamaresTranslations.cc "${_names_tu}")
# Some implementation details are compiled twice, because
# we want them in the library with visibility("hidden")
# but also need them in tests.
set(_geoip_src
geoip/GeoIPFixed.cpp
geoip/GeoIPJSON.cpp
)
add_library(
calamares
SHARED
CalamaresAbout.cpp
CppJob.cpp
GlobalStorage.cpp
Job.cpp
JobExample.cpp
JobQueue.cpp
ProcessJob.cpp
Settings.cpp
# GeoIP services
geoip/Interface.cpp
${_geoip_src}
geoip/Handler.cpp
# Locale-data service
locale/Global.cpp
locale/Lookup.cpp
locale/TimeZone.cpp
locale/TranslatableConfiguration.cpp
locale/TranslatableString.cpp
locale/Translation.cpp
locale/TranslationsModel.cpp
# Modules
modulesystem/Config.cpp
modulesystem/Descriptor.cpp
modulesystem/InstanceKey.cpp
modulesystem/Module.cpp
modulesystem/Preset.cpp
modulesystem/RequirementsChecker.cpp
modulesystem/RequirementsModel.cpp
# Network service
network/Manager.cpp
# Packages service
packages/Globals.cpp
# Partition service
partition/Global.cpp
partition/Mount.cpp
partition/PartitionSize.cpp
partition/Sync.cpp
# Utility service
utils/CommandList.cpp
utils/Dirs.cpp
utils/Entropy.cpp
utils/Logger.cpp
utils/Permissions.cpp
utils/PluginFactory.cpp
utils/Retranslator.cpp
utils/Runner.cpp
utils/String.cpp
utils/StringExpander.cpp
utils/System.cpp
utils/UMask.cpp
utils/Variant.cpp
utils/Yaml.cpp
)
set_target_properties(
calamares
PROPERTIES
VERSION ${CALAMARES_VERSION_SHORT}
SOVERSION ${CALAMARES_SOVERSION}
CXX_VISIBILITY_PRESET hidden
)
target_link_libraries(calamares LINK_PUBLIC yamlcpp::yamlcpp ${qtname}::Core ${qtname}::Network)
target_link_libraries(calamares LINK_PUBLIC ${kfname}::CoreAddons)
target_compile_definitions(calamares PRIVATE DLLEXPORT_PRO)
target_include_directories(calamares PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(calamares PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include/libcalamares>
)
target_include_directories(calamares PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)
### OPTIONAL Automount support (requires dbus)
#
#
if(TARGET ${qtname}::DBus)
target_sources(calamares PRIVATE partition/AutoMount.cpp)
target_link_libraries(calamares PRIVATE ${qtname}::DBus)
endif()
### OPTIONAL Python support
#
#
if(WITH_PYTHON)
if(WITH_PYBIND11)
target_include_directories(calamares PRIVATE pybind11)
target_sources(calamares PRIVATE pybind11/Api.cpp pybind11/PythonJob.cpp)
target_link_libraries(calamares PRIVATE Python::Python pybind11::headers)
else()
target_include_directories(calamares PRIVATE pyboost)
target_sources(calamares PRIVATE pyboost/PythonHelper.cpp pyboost/PythonJob.cpp pyboost/PythonJobApi.cpp)
target_link_libraries(calamares PRIVATE Python::Python Boost::python)
endif()
target_sources(calamares PRIVATE python/Api.cpp python/Variant.cpp)
endif()
### OPTIONAL GeoIP XML support
#
#
find_package(${qtname} ${QT_VERSION} COMPONENTS Xml)
if(TARGET ${qtname}::Xml)
target_sources(calamares PRIVATE geoip/GeoIPXML.cpp)
target_link_libraries(calamares PRIVATE ${qtname}::Network ${qtname}::Xml)
endif()
### OPTIONAL KPMcore support
#
#
include(KPMcoreHelper)
if(KPMcore_FOUND)
target_sources(
calamares
PRIVATE
partition/FileSystem.cpp
partition/KPMManager.cpp
partition/PartitionIterator.cpp
partition/PartitionQuery.cpp
)
endif()
# Always, since this also handles the no-KPMcore case; we don't
# call it calamares::kpmcore because that name exists only
# when KPMcore is actually found.
target_link_libraries(calamares PRIVATE calapmcore)
### LIBRARY
#
#
calamares_automoc( calamares )
add_library(Calamares::calamares ALIAS calamares)
### Installation
#
#
install(
TARGETS calamares
EXPORT Calamares
RUNTIME
DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY
DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
# Make symlink lib/calamares/libcalamares.so to lib/libcalamares.so.VERSION so
# lib/calamares can be used as module path for the Python interpreter.
install(
CODE
"
file( MAKE_DIRECTORY \"\$ENV{DESTDIR}/${CMAKE_INSTALL_FULL_LIBDIR}/calamares\" )
execute_process( COMMAND \"${CMAKE_COMMAND}\" -E create_symlink ../libcalamares.so.${CALAMARES_VERSION_SHORT} libcalamares.so WORKING_DIRECTORY \"\$ENV{DESTDIR}/${CMAKE_INSTALL_FULL_LIBDIR}/calamares\" )
"
)
# Install header files
file(GLOB rootHeaders "*.h")
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/CalamaresConfig.h ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersion.h ${rootHeaders}
DESTINATION include/libcalamares
)
# Install each subdir-worth of header files
foreach(subdir geoip locale modulesystem network partition utils compat packages)
file(GLOB subdir_headers "${subdir}/*.h")
install(FILES ${subdir_headers} DESTINATION include/libcalamares/${subdir})
endforeach()
### TRANSLATION TESTING
#
calamares_qrc_translations( localetest OUTPUT_VARIABLE localetest_qrc SUBDIRECTORY testdata LANGUAGES nl )
### TESTING
#
#
calamares_add_test(libcalamarestest SOURCES Tests.cpp)
calamares_add_test(libcalamaresgeoiptest SOURCES geoip/GeoIPTests.cpp ${_geoip_src})
calamares_add_test(libcalamareslocaletest SOURCES locale/Tests.cpp ${localetest_qrc})
calamares_add_test(libcalamaresmodulesystemtest SOURCES modulesystem/Tests.cpp)
calamares_add_test(libcalamaresnetworktest SOURCES network/Tests.cpp)
calamares_add_test(libcalamarespackagestest SOURCES packages/Tests.cpp)
if(KPMcore_FOUND)
calamares_add_test(
libcalamarespartitiontest
SOURCES partition/Global.cpp partition/Tests.cpp
LIBRARIES calamares::kpmcore
)
calamares_add_test(libcalamarespartitionkpmtest SOURCES partition/KPMTests.cpp LIBRARIES calamares::kpmcore)
endif()
calamares_add_test(libcalamaresutilstest SOURCES utils/Tests.cpp utils/Permissions.cpp utils/Runner.cpp)
calamares_add_test(libcalamaresutilspathstest SOURCES utils/TestPaths.cpp)
# This is not an actual test, it's a test / demo application
# for experimenting with GeoIP.
add_executable(test_geoip geoip/test_geoip.cpp ${_geoip_src})
target_link_libraries(test_geoip Calamares::calamares ${qtname}::Network yamlcpp::yamlcpp)
calamares_automoc( test_geoip )
if(TARGET ${qtname}::DBus)
add_executable(test_automount partition/calautomount.cpp)
target_link_libraries(test_automount Calamares::calamares ${qtname}::DBus)
endif()

View File

@@ -0,0 +1,76 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2022 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "CalamaresAbout.h"
#include "CalamaresVersionX.h"
#include <QCoreApplication>
static const char s_header[]
= QT_TRANSLATE_NOOP( "AboutData", "<h1>%1</h1><br/><strong>%2<br/> for %3</strong><br/><br/>" );
static const char s_footer[]
= QT_TRANSLATE_NOOP( "AboutData",
"Thanks to <a href=\"https://calamares.io/team/\">the Calamares team</a> "
"and the <a href=\"https://app.transifex.com/calamares/calamares/\">Calamares "
"translators team</a>." );
struct Maintainer
{
unsigned int start;
unsigned int end;
const char* name;
const char* email;
QString text() const
{
//: Copyright year-year Name <email-address>
return QCoreApplication::translate( "AboutData", "Copyright %1-%2 %3 &lt;%4&gt;<br/>" )
.arg( start )
.arg( end )
.arg( name )
.arg( email );
}
};
static constexpr const Maintainer maintainers[] = {
{ 2014, 2017, "Teo Mrnjavac", "teo@kde.org" },
{ 2017, 2022, "Adriaan de Groot", "groot@kde.org" },
{ 2022, 2024, "Adriaan de Groot (community)", "groot@kde.org" },
};
static QString
aboutMaintainers()
{
QStringList s;
for ( const auto& m : maintainers )
{
s.append( m.text() );
}
return s.join( QString() );
}
static QString
substituteVersions( const QString& s )
{
return s.arg( CALAMARES_APPLICATION_NAME ).arg( CALAMARES_VERSION );
}
const QString
Calamares::aboutString()
{
return substituteVersions( QCoreApplication::translate( "AboutData", s_header ) ) + aboutMaintainers()
+ QCoreApplication::translate( "AboutData", s_footer );
}
const QString
Calamares::aboutStringUntranslated()
{
return substituteVersions( QString( s_header ) ) + aboutMaintainers() + QString( s_footer );
}

View File

@@ -0,0 +1,31 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2022 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_CALAMARESABOUT_H
#define CALAMARES_CALAMARESABOUT_H
#include "DllMacro.h"
#include <QString>
namespace Calamares
{
/** @brief Returns an about string for the application
*
* The about string includes a header-statement, a list of maintainer
* addresses, and a thank-you to Blue Systems. There is on %-substitution
* left, where you can fill in the name of the product (e.g. to say
* "Calamares for Netrunner" or ".. for Manjaro").
*/
DLLEXPORT const QString aboutStringUntranslated();
/// @brief As above, but translated in the current Calamares language
DLLEXPORT const QString aboutString();
} // namespace Calamares
#endif

View File

@@ -0,0 +1,34 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARESCONFIG_H
#define CALAMARESCONFIG_H
#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}"
#define CMAKE_INSTALL_FULL_LIBEXECDIR "${CMAKE_INSTALL_FULL_LIBEXECDIR}"
#define CMAKE_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}"
#define CMAKE_INSTALL_FULL_LIBDIR "${CMAKE_INSTALL_FULL_LIBDIR}"
#define CMAKE_INSTALL_FULL_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}/calamares"
#define CMAKE_INSTALL_FULL_SYSCONFDIR "${CMAKE_INSTALL_FULL_SYSCONFDIR}"
#define CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}"
/*
* These are feature-settings that affect consumers of Calamares
* libraries as well; without Python-support in the libs, for instance,
* there's no point in having a Python plugin.
*
* This list should match the one in CalamaresConfig.cmake
* which is the CMake-time side of the same configuration.
*/
#cmakedefine WITH_PYTHON
#cmakedefine WITH_PYBIND11
#cmakedefine WITH_BOOST_PYTHON
#cmakedefine WITH_QML
#cmakedefine WITH_QT6
#endif // CALAMARESCONFIG_H

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: no
// SPDX-License-Identifier: CC0-1.0
#ifndef CALAMARES_VERSION_H
#define CALAMARES_VERSION_H
#cmakedefine CALAMARES_ORGANIZATION_NAME "${CALAMARES_ORGANIZATION_NAME}"
#cmakedefine CALAMARES_ORGANIZATION_DOMAIN "${CALAMARES_ORGANIZATION_DOMAIN}"
#cmakedefine CALAMARES_APPLICATION_NAME "${CALAMARES_APPLICATION_NAME}"
#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION_SHORT}"
#cmakedefine CALAMARES_VERSION_SHORT "${CALAMARES_VERSION_SHORT}"
#cmakedefine CALAMARES_VERSION_MAJOR "${CALAMARES_VERSION_MAJOR}"
#cmakedefine CALAMARES_VERSION_MINOR "${CALAMARES_VERSION_MINOR}"
#cmakedefine CALAMARES_VERSION_PATCH "${CALAMARES_VERSION_PATCH}"
#cmakedefine CALAMARES_VERSION_RC "${CALAMARES_VERSION_RC}"
#endif // CALAMARES_VERSION_H

View File

@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: no
// SPDX-License-Identifier: CC0-1.0
//
// Same as CalamaresVersion.h, but with a full-git-extended VERSION
// rather than the short (vM.m.p) semantic version.
#ifndef CALAMARES_VERSION_H
// On purpose, do not define the guard, but let CalamaresVersion.h do it
// #define CALAMARES_VERSION_H
#include "CalamaresVersion.h"
#undef CALAMARES_VERSION
#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION_LONG}"
#endif // CALAMARES_VERSION_H

View File

@@ -0,0 +1,38 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2016 Kevin Kofler <kevin.kofler@chello.at>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "CppJob.h"
namespace Calamares
{
CppJob::CppJob( QObject* parent )
: Job( parent )
{
}
CppJob::~CppJob() {}
void
CppJob::setModuleInstanceKey( const Calamares::ModuleSystem::InstanceKey& instanceKey )
{
m_instanceKey = instanceKey;
}
void
CppJob::setConfigurationMap( const QVariantMap& configurationMap )
{
Q_UNUSED( configurationMap )
}
} // namespace Calamares

View File

@@ -0,0 +1,44 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2016 Kevin Kofler <kevin.kofler@chello.at>
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_CPPJOB_H
#define CALAMARES_CPPJOB_H
#include "DllMacro.h"
#include "Job.h"
#include "modulesystem/InstanceKey.h"
#include <QObject>
#include <QVariant>
namespace Calamares
{
class DLLEXPORT CppJob : public Job
{
Q_OBJECT
public:
explicit CppJob( QObject* parent = nullptr );
~CppJob() override;
void setModuleInstanceKey( const Calamares::ModuleSystem::InstanceKey& instanceKey );
Calamares::ModuleSystem::InstanceKey moduleInstanceKey() const { return m_instanceKey; }
virtual void setConfigurationMap( const QVariantMap& configurationMap );
protected:
Calamares::ModuleSystem::InstanceKey m_instanceKey;
};
} // namespace Calamares
#endif // CALAMARES_CPPJOB_H

View File

@@ -0,0 +1,68 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef DLLMACRO_H
#define DLLMACRO_H
#ifndef CALAMARES_EXPORT
#define CALAMARES_EXPORT __attribute__( ( visibility( "default" ) ) )
#endif
/*
* Mark symbols exported from Calamares non-GUI library with DLLEXPORT.
* These are the public API of libcalamares.
*/
#ifndef DLLEXPORT
#if defined( DLLEXPORT_PRO )
#define DLLEXPORT CALAMARES_EXPORT
#else
#define DLLEXPORT
#endif
#endif
/*
* Mark symbols exported from Calamares GUI library with DLLEXPORT.
* These are the public API of libcalamaresui.
*/
#ifndef UIDLLEXPORT
#if defined( UIDLLEXPORT_PRO )
#define UIDLLEXPORT CALAMARES_EXPORT
#else
#define UIDLLEXPORT
#endif
#endif
/*
* Mark symbols exported from Calamares C++ plugins with PLUGINDLLEXPORT.
* These are the public API of the libraries (generally, the plugin
* entry point)
*/
#ifndef PLUGINDLLEXPORT
#if defined( PLUGINDLLEXPORT_PRO )
#define PLUGINDLLEXPORT CALAMARES_EXPORT
#else
#define PLUGINDLLEXPORT
#endif
#endif
/*
* For functions that should be static in production but also need to
* be tested, use STATICTEST as linkage specifier. When built as part
* of a test, the function will be given normal linkage.
*/
#ifndef STATICTEST
#if defined( BUILD_AS_TEST )
#define STATICTEST
#else
#define STATICTEST static
#endif
#endif
#endif

View File

@@ -0,0 +1,244 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "GlobalStorage.h"
#include "compat/Mutex.h"
#include "utils/Logger.h"
#include "utils/Units.h"
#include "utils/Yaml.h"
#include <QFile>
#include <QJsonDocument>
using namespace Calamares::Units;
namespace Calamares
{
class GlobalStorage::ReadLock : public MutexLocker
{
public:
ReadLock( const GlobalStorage* gs )
: MutexLocker( &gs->m_mutex )
{
}
};
class GlobalStorage::WriteLock : public MutexLocker
{
public:
WriteLock( GlobalStorage* gs )
: MutexLocker( &gs->m_mutex )
, m_gs( gs )
{
}
~WriteLock() { m_gs->changed(); }
GlobalStorage* m_gs;
};
GlobalStorage::GlobalStorage( QObject* parent )
: QObject( parent )
{
}
bool
GlobalStorage::contains( const QString& key ) const
{
ReadLock l( this );
return m.contains( key );
}
int
GlobalStorage::count() const
{
ReadLock l( this );
return m.count();
}
void
GlobalStorage::insert( const QString& key, const QVariant& value )
{
WriteLock l( this );
m.insert( key, value );
}
QStringList
GlobalStorage::keys() const
{
ReadLock l( this );
return m.keys();
}
int
GlobalStorage::remove( const QString& key )
{
WriteLock l( this );
int nItems = m.remove( key );
return nItems;
}
void
GlobalStorage::clear()
{
WriteLock l( this );
m.clear();
}
QVariant
GlobalStorage::value( const QString& key ) const
{
ReadLock l( this );
return m.value( key );
}
void
GlobalStorage::debugDump() const
{
ReadLock l( this );
cDebug() << "GlobalStorage" << Logger::Pointer( this ) << m.count() << "items";
for ( auto it = m.cbegin(); it != m.cend(); ++it )
{
cDebug() << Logger::SubEntry << it.key() << '\t' << it.value();
}
}
bool
GlobalStorage::saveJson( const QString& filename ) const
{
ReadLock l( this );
QFile f( filename );
if ( !f.open( QFile::WriteOnly ) )
{
return false;
}
f.write( QJsonDocument::fromVariant( m ).toJson() );
f.close();
return true;
}
bool
GlobalStorage::loadJson( const QString& filename )
{
QFile f( filename );
if ( !f.open( QFile::ReadOnly ) )
{
return false;
}
QJsonParseError e;
QJsonDocument d = QJsonDocument::fromJson( f.read( 1_MiB ), &e );
if ( d.isNull() )
{
cWarning() << filename << e.errorString();
}
else if ( !d.isObject() )
{
cWarning() << filename << "Not suitable JSON.";
}
else
{
WriteLock l( this );
// Do **not** use method insert() here, because it would
// recursively lock the mutex, leading to deadlock. Also,
// that would emit changed() for each key.
auto map = d.toVariant().toMap();
for ( auto i = map.constBegin(); i != map.constEnd(); ++i )
{
m.insert( i.key(), *i );
}
return true;
}
return false;
}
bool
GlobalStorage::saveYaml( const QString& filename ) const
{
ReadLock l( this );
return Calamares::YAML::save( filename, m );
}
bool
GlobalStorage::loadYaml( const QString& filename )
{
bool ok = false;
auto map = Calamares::YAML::load( filename, &ok );
if ( ok )
{
WriteLock l( this );
// Do **not** use method insert() here, because it would
// recursively lock the mutex, leading to deadlock. Also,
// that would emit changed() for each key.
for ( auto i = map.constBegin(); i != map.constEnd(); ++i )
{
m.insert( i.key(), *i );
}
return true;
}
return false;
}
///@brief Implementation for recursively looking up dotted selector parts.
static QVariant
lookup( const QStringList& nestedKey, int index, const QVariant& v, bool& ok )
{
if ( !v.canConvert< QVariantMap >() )
{
// Mismatch: we're still looking for keys, but v is not a submap
ok = false;
return {};
}
if ( index >= nestedKey.length() )
{
cError() << "Recursion error looking at index" << index << "of" << nestedKey;
ok = false;
return {};
}
const QVariantMap map = v.toMap();
const QString& key = nestedKey.at( index );
if ( index == nestedKey.length() - 1 )
{
ok = map.contains( key );
return ok ? map.value( key ) : QVariant();
}
else
{
return lookup( nestedKey, index + 1, map.value( key ), ok );
}
}
QVariant
lookup( const GlobalStorage* storage, const QString& nestedKey, bool& ok )
{
ok = false;
if ( !storage )
{
return {};
}
if ( nestedKey.contains( '.' ) )
{
QStringList steps = nestedKey.split( '.' );
return lookup( steps, 1, storage->value( steps.first() ), ok );
}
else
{
ok = storage->contains( nestedKey );
return ok ? storage->value( nestedKey ) : QVariant();
}
}
} // namespace Calamares

View File

@@ -0,0 +1,192 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_GLOBALSTORAGE_H
#define CALAMARES_GLOBALSTORAGE_H
#include "DllMacro.h"
#include <QMutex>
#include <QObject>
#include <QString>
#include <QVariantMap>
namespace Calamares
{
/** @brief Storage for data that passes between Calamares modules.
*
* The Global Storage is global to the Calamares JobQueue and
* everything that depends on that: all of its modules use the
* same instance of the JobQueue, and so of the Global Storage.
*
* GS is used to pass data between modules; there is only convention
* about what keys are used, and individual modules should document
* what they put in to GS or what they expect to find in it.
*
* GS behaves as a basic key-value store, with a QVariantMap behind
* it. Any QVariant can be put into the storage, and the signal
* changed() is emitted when any data is modified.
*
* In general, see QVariantMap (possibly after calling data()) for details.
*
* This class is thread-safe -- most accesses go through JobQueue, which
* handles threading itself, but because modules load in parallel and can
* have asynchronous tasks like GeoIP lookups, the storage itself also
* has locking. All methods are thread-safe, use data() to make a snapshot
* copy for use outside of the thread-safe API.
*/
class DLLEXPORT GlobalStorage : public QObject
{
Q_OBJECT
public:
/** @brief Create a GS object
*
* **Generally** there is only one GS object (hence, "global") which
* is owned by the JobQueue instance (which is a singleton). However,
* it is possible to create more GS objects.
*/
explicit GlobalStorage( QObject* parent = nullptr );
/** @brief Insert a key and value into the store
*
* The @p value is added to the store with key @p key. If @p key
* already exists in the store, its existing value is overwritten.
* The changed() signal is emitted regardless.
*/
void insert( const QString& key, const QVariant& value );
/** @brief Removes a key and its value
*
* The @p key is removed from the store. If the @p key does not
* exist, nothing happens. changed() is emitted regardless.
*
* @return the number of keys remaining
*/
int remove( const QString& key );
/// @brief Clears all keys in this GS object
void clear();
/** @brief dump keys and values to the debug log
*
* All the keys and their values are written to the debug log.
* See save() for caveats: this can leak sensitive information.
*/
void debugDump() const;
/** @brief write as JSON to the given filename
*
* The file named @p filename is overwritten with a JSON representation
* of the entire global storage (this may be structured, for instance
* if maps or lists have been inserted).
*
* No tidying, sanitization, or censoring is done -- for instance,
* the user module sets a slightly-obscured password in global storage,
* and this JSON file will contain that password in-the-only-slightly-
* obscured form.
*/
bool saveJson( const QString& filename ) const;
/** @brief Adds the keys from the given JSON file
*
* No tidying, sanitization, or censoring is done.
* The JSON file is read and each key is added as a value to
* the global storage. The storage is not cleared first: existing
* keys will remain; keys that also occur in the JSON file are overwritten.
*/
bool loadJson( const QString& filename );
/** @brief write as YAML to the given filename
*
* See also save(), above.
*/
bool saveYaml( const QString& filename ) const;
/** @brief reads settings from the given filename
*
* See also load(), above.
*/
bool loadYaml( const QString& filename );
/** @brief Make a complete copy of the data
*
* Provides a snapshot of the data at a given time.
*/
QVariantMap data() const { return m; }
public Q_SLOTS:
/** @brief Does the store contain the given key?
*
* This can distinguish an explicitly-inserted QVariant() from
* a no-value-exists QVariant. See value() for details.
*/
bool contains( const QString& key ) const;
/** @brief The number of keys in the store
*
* This should be unsigned, but the underlying QMap uses signed as well.
* Equal to keys().length(), in theory.
*/
int count() const;
/** @brief The keys in the store
*
* This makes a copy of all the keys currently in the store, which
* could be used for iterating over all the values in the store.
*/
QStringList keys() const;
/** @brief Gets a value from the store
*
* If a value has been previously inserted, returns that value.
* If @p key does not exist in the store, returns a QVariant()
* (an invalid QVariant, which boolean-converts to false). Since
* QVariant() van also be inserted explicitly, use contains()
* to check for the presence of a key if you need that.
*/
QVariant value( const QString& key ) const;
signals:
/** @brief Emitted any time the store changes
*
* Also emitted sometimes when the store does not change, e.g.
* when removing a non-existent key or inserting a value that
* is already present.
*/
void changed();
private:
class ReadLock;
class WriteLock;
QVariantMap m;
mutable QMutex m_mutex;
};
/** @brief Gets a value from the store
*
* When @p nestedKey contains no '.' characters, equivalent
* to `gs->value(nestedKey)`. Otherwise recursively looks up
* the '.'-separated parts of @p nestedKey in successive sub-maps
* of the store, returning the value in the innermost one.
*
* Example: `lookup(gs, "branding.name")` finds the value of the
* 'name' key in the 'branding' submap of the store.
*
* Sets @p ok to @c true if a value was found. Returns the value
* as a variant. If no value is found (e.g. the key is missing
* or some prefix submap is missing) sets @p ok to @c false
* and returns an invalid QVariant.
*
* @see GlobalStorage::value
*/
DLLEXPORT QVariant lookup( const GlobalStorage* gs, const QString& nestedKey, bool& ok );
} // namespace Calamares
#endif // CALAMARES_GLOBALSTORAGE_H

View File

@@ -0,0 +1,112 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "Job.h"
namespace Calamares
{
JobResult::JobResult( JobResult&& rhs )
: m_message( std::move( rhs.m_message ) )
, m_details( std::move( rhs.m_details ) )
, m_number( rhs.m_number )
{
}
JobResult::operator bool() const
{
return m_number == 0;
}
QString
JobResult::message() const
{
return m_message;
}
void
JobResult::setMessage( const QString& message )
{
m_message = message;
}
QString
JobResult::details() const
{
return m_details;
}
void
JobResult::setDetails( const QString& details )
{
m_details = details;
}
JobResult
JobResult::ok()
{
return JobResult( QString(), QString(), NoError );
}
JobResult
JobResult::error( const QString& message, const QString& details )
{
return JobResult( message, details, GenericError );
}
JobResult
JobResult::internalError( const QString& message, const QString& details, int number )
{
return JobResult( message, details, number ? number : GenericError );
}
JobResult::JobResult( const QString& message, const QString& details, int number )
: m_message( message )
, m_details( details )
, m_number( number )
{
}
Job::Job( QObject* parent )
: QObject( parent )
{
}
Job::~Job() {}
int
Job::getJobWeight() const
{
return 1;
}
QString
Job::prettyDescription() const
{
return QString();
}
QString
Job::prettyStatusMessage() const
{
return QString();
}
} // namespace Calamares

View File

@@ -0,0 +1,175 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_JOB_H
#define CALAMARES_JOB_H
#include "DllMacro.h"
#include <QList>
#include <QObject>
#include <QSharedPointer>
namespace Calamares
{
class DLLEXPORT JobResult
{
public:
/** @brief Distinguish classes of errors
*
* All "ok result" have errorCode 0 (NoError).
* Errors returned from job execution have values < 0.
* Errors before job execution, or not returned by the job execution
* itself, have values > 0.
*/
enum
{
NoError = 0,
GenericError = -1,
PythonUncaughtException = 1,
InvalidConfiguration = 2,
MissingRequirements = 3,
};
// Can't copy, but you can keep a temporary
JobResult( const JobResult& rhs ) = delete;
JobResult( JobResult&& rhs );
virtual ~JobResult() {}
/** @brief Is this JobResult a success?
*
* Equivalent to errorCode() == 0, see succeeded().
*/
virtual operator bool() const;
virtual QString message() const;
virtual void setMessage( const QString& message );
virtual QString details() const;
virtual void setDetails( const QString& details );
int errorCode() const { return m_number; }
/** @brief Is this JobResult a success?
*
* Equivalent to errorCode() == 0.
*/
bool succeeded() const { return this->operator bool(); }
/// @brief an "ok status" result
static JobResult ok();
/** @brief an "error" result resulting from the execution of the job
*
* The error code is set to GenericError.
*/
static JobResult error( const QString& message, const QString& details = QString() );
/** @brief an "internal error" meaning the job itself has a problem (usually for python)
*
* Pass in a suitable error code; using 0 (which would normally mean "ok") instead
* gives you a GenericError code.
*/
static JobResult internalError( const QString& message, const QString& details, int errorCode );
protected:
explicit JobResult( const QString& message, const QString& details, int errorCode );
private:
QString m_message;
QString m_details;
int m_number;
};
class DLLEXPORT Job : public QObject
{
Q_OBJECT
public:
explicit Job( QObject* parent = nullptr );
~Job() override;
/** @brief The job's (relative) weight.
*
* The default implementation returns 1, which gives all jobs
* the same weight, so they advance the overall progress the same
* amount. This is nonsense, since some jobs take much longer than
* others; it's up to the individual jobs to say something about
* how much work is (relatively) done.
*
* Since jobs are caused by **modules** from the sequence, the
* overall weight of the module is taken into account: its weight
* is divided among the jobs based on each jobs relative weight.
* This can be used in a module that runs a bunch of jobs to indicate
* which of the jobs is "heavy" and which is not.
*/
virtual int getJobWeight() const;
/** @brief The human-readable name of this job
*
* This should be a very short statement of what the job does.
* For status and state information, see prettyStatusMessage().
*
* The job's name may be similar to the status message, but this is
* a name, and should not be an active verb phrase. The translation
* should use context @c \@label .
*
* The name of the job is used as a **fallback** when the status
* or descriptions are empty. If a job has no implementation of
* those methods, it is OK to use other contexts, but it may look
* strange in some places in the UI.
*/
virtual QString prettyName() const = 0;
/** @brief a longer human-readable description of what the job will do
*
* This **may** be used by view steps to fill in the summary
* messages for the summary page; at present, only the *partition*
* module does so.
*
* The default implementation returns an empty string.
*
* The translation should use context @c \@title .
*/
virtual QString prettyDescription() const;
/** @brief A human-readable status for progress reporting
*
* This is called from the JobQueue when progress is made, and should
* return a not-too-long description of the job's status. This
* is made visible in the progress bar of the execution view step.
*
* The job's status should say **what** the job is doing. It should be in
* present active tense. Typically the translation uses tr() context
* @c \@status . See prettyName() for examples.
*/
virtual QString prettyStatusMessage() const;
virtual JobResult exec() = 0;
bool isEmergency() const { return m_emergency; }
void setEmergency( bool e ) { m_emergency = e; }
signals:
/** @brief Signals that the job has made progress
*
* The parameter @p percent should be between 0 (0%) and 1 (100%).
* Values outside of this range will be clamped.
*/
void progress( qreal percent );
private:
bool m_emergency = false;
};
using job_ptr = QSharedPointer< Job >;
using JobList = QList< job_ptr >;
} // namespace Calamares
#endif // CALAMARES_JOB_H

View File

@@ -0,0 +1,33 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "JobExample.h"
namespace Calamares
{
QString
NamedJob::prettyName() const
{
return tr( "Example job (%1)" ).arg( m_name );
}
JobResult
GoodJob::exec()
{
return JobResult::ok();
}
JobResult
FailJob::exec()
{
return JobResult::error( tr( "Job failed (%1)" ).arg( m_name ),
tr( "Programmed job failure was explicitly requested." ) );
}
} // namespace Calamares

View File

@@ -0,0 +1,69 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_JOB_EXAMPLE_H
#define CALAMARES_JOB_EXAMPLE_H
#include "Job.h"
namespace Calamares
{
/** @brief A Job with a name
*
* This includes a default implementation of prettyName(),
* but is only used as a base for FailJob and GoodJob,
* which are support / bogus classes.
*/
class DLLEXPORT NamedJob : public Job
{
Q_OBJECT
public:
explicit NamedJob( const QString& name, QObject* parent = nullptr )
: Job( parent )
, m_name( name )
{
}
virtual QString prettyName() const override;
protected:
const QString m_name;
};
/// @brief Job does nothing, always succeeds
class DLLEXPORT GoodJob : public NamedJob
{
Q_OBJECT
public:
explicit GoodJob( const QString& name, QObject* parent = nullptr )
: NamedJob( name, parent )
{
}
virtual JobResult exec() override;
};
/// @brief Job does nothing, always fails
class DLLEXPORT FailJob : public NamedJob
{
Q_OBJECT
public:
explicit FailJob( const QString& name, QObject* parent = nullptr )
: NamedJob( name, parent )
{
}
virtual JobResult exec() override;
};
} // namespace Calamares
#endif // CALAMARES_JOB_EXAMPLE_H

View File

@@ -0,0 +1,618 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "JobQueue.h"
#include "CalamaresConfig.h"
#include "GlobalStorage.h"
#include "Job.h"
#include "compat/Mutex.h"
#include "utils/Logger.h"
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QDBusPendingCall>
#include <QDBusPendingReply>
#include <QMutex>
#include <QThread>
#include <memory>
#include <optional>
#include <unistd.h> // for close()
namespace
{
// This power-management code is largely cribbed from KDE Discover,
// https://invent.kde.org/plasma/discover/-/blob/master/discover/PowerManagementInterface.cpp
//
// Upstream license text says:
//
// SPDX-FileCopyrightText: 2019 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
// SPDX-License-Identifier: LGPL-3.0-or-later
/** @brief Class to manage sleep / suspend on inactivity (via fd.o Inhibit service on the user bus)
*
* Create an object of this class on the heap. Call inhibitSleep()
* to (try to) stop system sleep / suspend. Call uninhibitSleep()
* when the object is no longer needed. The object self-deletes
* after uninhibitSleep() completes.
*/
class PowerManagementInterface : public QObject
{
Q_OBJECT
public:
PowerManagementInterface( QObject* parent = nullptr );
~PowerManagementInterface() override;
public Q_SLOTS:
void inhibitSleep();
void uninhibitSleep();
private Q_SLOTS:
void hostSleepInhibitChanged();
void inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher );
void uninhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher );
private:
uint m_inhibitSleepCookie = 0;
bool m_inhibitedSleep = false;
};
PowerManagementInterface::PowerManagementInterface( QObject* parent )
: QObject( parent )
{
auto sessionBus = QDBusConnection::sessionBus();
sessionBus.connect( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ),
QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "HasInhibitChanged" ),
this,
SLOT( hostSleepInhibitChanged() ) );
}
PowerManagementInterface::~PowerManagementInterface() = default;
void
PowerManagementInterface::hostSleepInhibitChanged()
{
// We don't actually care
}
void
PowerManagementInterface::inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher )
{
QDBusPendingReply< uint > reply = *aWatcher;
if ( reply.isError() )
{
cError() << "Could not inhibit sleep:" << reply.error();
// m_inhibitedSleep = false; // unchanged
}
else
{
m_inhibitSleepCookie = reply.argumentAt< 0 >();
m_inhibitedSleep = true;
cDebug() << "Sleep inhibited, cookie" << m_inhibitSleepCookie;
}
aWatcher->deleteLater();
}
void
PowerManagementInterface::uninhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher )
{
QDBusPendingReply<> reply = *aWatcher;
if ( reply.isError() )
{
cError() << "Could not uninhibit sleep:" << reply.error();
}
else
{
m_inhibitedSleep = false;
m_inhibitSleepCookie = 0;
cDebug() << "Sleep uninhibited.";
}
aWatcher->deleteLater();
this->deleteLater();
}
void
PowerManagementInterface::inhibitSleep()
{
if ( m_inhibitedSleep )
{
cDebug() << "Sleep is already inhibited.";
return;
}
auto sessionBus = QDBusConnection::sessionBus();
auto inhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ),
QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "Inhibit" ) );
inhibitCall.setArguments( { { tr( "Calamares" ) }, { tr( "Installation in progress", "@status" ) } } );
auto asyncReply = sessionBus.asyncCall( inhibitCall );
auto* replyWatcher = new QDBusPendingCallWatcher( asyncReply, this );
QObject::connect(
replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::inhibitDBusCallFinished );
}
void
PowerManagementInterface::uninhibitSleep()
{
if ( !m_inhibitedSleep )
{
cDebug() << "Sleep was never inhibited.";
this->deleteLater();
return;
}
auto sessionBus = QDBusConnection::sessionBus();
auto uninhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ),
QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ),
QStringLiteral( "UnInhibit" ) );
uninhibitCall.setArguments( { { m_inhibitSleepCookie } } );
auto asyncReply = sessionBus.asyncCall( uninhibitCall );
auto replyWatcher = new QDBusPendingCallWatcher( asyncReply, this );
QObject::connect(
replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::uninhibitDBusCallFinished );
}
/** @brief Class to manage sleep / suspend on inactivity (via logind/CK2 service on the system bus)
*
* Create an object of this class on the heap. Call inhibitSleep()
* to (try to) stop system sleep / suspend. Call uninhibitSleep()
* when the object is no longer needed. The object self-deletes
* after uninhibitSleep() completes.
*/
class LoginManagerInterface : public QObject
{
Q_OBJECT
public:
static LoginManagerInterface* makeForRegisteredService( QObject* parent = nullptr );
~LoginManagerInterface() override;
public Q_SLOTS:
void inhibitSleep();
void uninhibitSleep();
private Q_SLOTS:
void inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher );
private:
enum class Service
{
Logind,
ConsoleKit,
};
LoginManagerInterface( Service service, QObject* parent = nullptr );
int m_inhibitFd = -1;
Service m_service;
};
LoginManagerInterface*
LoginManagerInterface::makeForRegisteredService( QObject* parent )
{
if ( QDBusConnection::systemBus().interface()->isServiceRegistered( QStringLiteral( "org.freedesktop.login1" ) ) )
{
return new LoginManagerInterface( Service::Logind, parent );
}
else if ( QDBusConnection::systemBus().interface()->isServiceRegistered(
QStringLiteral( "org.freedesktop.ConsoleKit" ) ) )
{
return new LoginManagerInterface( Service::ConsoleKit, parent );
}
else
{
return nullptr;
}
}
LoginManagerInterface::LoginManagerInterface( Service service, QObject* parent )
: QObject( parent )
, m_service( service )
{
}
LoginManagerInterface::~LoginManagerInterface() = default;
void
LoginManagerInterface::inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher )
{
QDBusPendingReply< uint > reply = *aWatcher;
if ( reply.isError() )
{
cError() << "Could not inhibit sleep:" << reply.error();
// m_inhibitFd = -1; // unchanged
}
else
{
m_inhibitFd = reply.argumentAt< 0 >();
cDebug() << "Sleep inhibited, file descriptor" << m_inhibitFd;
}
aWatcher->deleteLater();
}
void
LoginManagerInterface::inhibitSleep()
{
if ( m_inhibitFd == -1 )
{
cDebug() << "Sleep is already inhibited.";
return;
}
auto systemBus = QDBusConnection::systemBus();
QDBusMessage inhibitCall;
if ( m_service == Service::Logind )
{
inhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.login1" ),
QStringLiteral( "/org/freedesktop/login1" ),
QStringLiteral( "org.freedesktop.login1.Manager" ),
QStringLiteral( "Inhibit" ) );
}
else if ( m_service == Service::ConsoleKit )
{
inhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.ConsoleKit" ),
QStringLiteral( "/org/freedesktop/ConsoleKit/Manager" ),
QStringLiteral( "org.freedesktop.ConsoleKit.Manager" ),
QStringLiteral( "Inhibit" ) );
}
else
{
cError() << "System sleep interface not supported.";
return;
}
inhibitCall.setArguments(
{ { "sleep:shutdown" }, { tr( "Calamares" ) }, { tr( "Installation in progress", "@status" ) }, { "block" } } );
auto asyncReply = systemBus.asyncCall( inhibitCall );
auto* replyWatcher = new QDBusPendingCallWatcher( asyncReply, this );
QObject::connect(
replyWatcher, &QDBusPendingCallWatcher::finished, this, &LoginManagerInterface::inhibitDBusCallFinished );
}
void
LoginManagerInterface::uninhibitSleep()
{
if ( m_inhibitFd == -1 )
{
cDebug() << "Sleep was never inhibited.";
this->deleteLater();
return;
}
if ( close( m_inhibitFd ) != 0 )
{
cError() << "Could not uninhibit sleep:" << strerror( errno );
}
this->deleteLater();
}
} // namespace
namespace Calamares
{
SleepInhibitor::SleepInhibitor()
{
// Create a LoginManagerInterface object with intentionally no parent
// so it is not destroyed along with this. Instead, when this
// is destroyed, **start** the uninhibit-sleep call which will (later)
// destroy the LoginManagerInterface object.
if ( auto* l = LoginManagerInterface::makeForRegisteredService( nullptr ) )
{
l->inhibitSleep();
connect( this, &QObject::destroyed, l, &LoginManagerInterface::uninhibitSleep );
}
// If no login manager service was present, try the same thing
// with PowerManagementInterface.
else
{
auto* p = new PowerManagementInterface( nullptr );
p->inhibitSleep();
connect( this, &QObject::destroyed, p, &PowerManagementInterface::uninhibitSleep );
}
}
SleepInhibitor::~SleepInhibitor() = default;
struct WeightedJob
{
/** @brief Cumulative weight **before** this job starts
*
* This is calculated as jobs come in.
*/
qreal cumulative = 0.0;
/** @brief Weight of the job within the module's jobs
*
* When a list of jobs is added from a particular module,
* the jobs are weighted relative to that module's overall weight
* **and** the other jobs in the list, so that each job
* gets its share:
* ( job-weight / total-job-weight ) * module-weight
*/
qreal weight = 0.0;
job_ptr job;
};
using WeightedJobList = QList< WeightedJob >;
class JobThread : public QThread
{
Q_OBJECT
public:
JobThread( JobQueue* queue )
: QThread( queue )
, m_queue( queue )
, m_jobIndex( 0 )
{
}
~JobThread() override;
void finalize()
{
Q_ASSERT( m_runningJobs->isEmpty() );
Calamares::MutexLocker qlock( &m_enqueMutex );
Calamares::MutexLocker rlock( &m_runMutex );
std::swap( m_runningJobs, m_queuedJobs );
m_overallQueueWeight
= m_runningJobs->isEmpty() ? 0.0 : ( m_runningJobs->last().cumulative + m_runningJobs->last().weight );
if ( m_overallQueueWeight < 1 )
{
m_overallQueueWeight = 1.0;
}
cDebug() << "There are" << m_runningJobs->count() << "jobs, total weight" << m_overallQueueWeight;
int c = 0;
for ( const auto& j : *m_runningJobs )
{
cDebug() << Logger::SubEntry << "Job" << ( c + 1 ) << j.job->prettyName() << "+wt" << j.weight << "tot.wt"
<< ( j.cumulative + j.weight );
c++;
}
}
void enqueue( int moduleWeight, const JobList& jobs )
{
Calamares::MutexLocker qlock( &m_enqueMutex );
qreal cumulative
= m_queuedJobs->isEmpty() ? 0.0 : ( m_queuedJobs->last().cumulative + m_queuedJobs->last().weight );
qreal totalJobWeight
= std::accumulate( jobs.cbegin(),
jobs.cend(),
qreal( 0.0 ),
[]( qreal total, const job_ptr& j ) { return total + j->getJobWeight(); } );
if ( totalJobWeight < 1 )
{
totalJobWeight = 1.0;
}
for ( const auto& j : jobs )
{
qreal jobContribution = ( j->getJobWeight() / totalJobWeight ) * moduleWeight;
m_queuedJobs->append( WeightedJob { cumulative, jobContribution, j } );
cumulative += jobContribution;
}
}
void run() override
{
Calamares::MutexLocker rlock( &m_runMutex );
bool failureEncountered = false;
QString message; ///< Filled in with errors
QString details;
Logger::Once o;
m_jobIndex = 0;
for ( const auto& jobitem : *m_runningJobs )
{
if ( failureEncountered && !jobitem.job->isEmergency() )
{
cDebug() << o << "Skipping non-emergency job" << jobitem.job->prettyName();
}
else
{
cDebug() << o << "Starting" << ( failureEncountered ? "EMERGENCY JOB" : "job" )
<< jobitem.job->prettyName() << '(' << ( m_jobIndex + 1 ) << '/' << m_runningJobs->count()
<< ')';
o.refresh(); // So next time it shows the function header again
emitProgress( 0.0 ); // 0% for *this job*
connect( jobitem.job.data(), &Job::progress, this, &JobThread::emitProgress );
auto result = jobitem.job->exec();
if ( !failureEncountered && !result )
{
// so this is the first failure
failureEncountered = true;
message = result.message();
details = result.details();
}
QThread::msleep( 16 ); // Very brief rest before reporting the job as complete
emitProgress( 1.0 ); // 100% for *this job*
}
m_jobIndex++;
}
if ( failureEncountered )
{
QMetaObject::invokeMethod(
m_queue, "failed", Qt::QueuedConnection, Q_ARG( QString, message ), Q_ARG( QString, details ) );
}
else
{
emitProgress( 1.0 );
}
m_runningJobs->clear();
QMetaObject::invokeMethod( m_queue, "finish", Qt::QueuedConnection );
}
/** @brief The names of the queued (not running!) jobs.
*/
QStringList queuedJobs() const
{
Calamares::MutexLocker qlock( &m_enqueMutex );
QStringList l;
l.reserve( m_queuedJobs->count() );
for ( const auto& j : *m_queuedJobs )
{
l << j.job->prettyName();
}
return l;
}
private:
/* This is called **only** from run(), while m_runMutex is
* already locked, so we can use the m_runningJobs member safely.
*/
void emitProgress( qreal percentage ) const
{
percentage = qBound( 0.0, percentage, 1.0 );
QString message;
qreal progress = 0.0;
if ( m_jobIndex < m_runningJobs->count() )
{
const auto& jobitem = m_runningJobs->at( m_jobIndex );
progress = ( jobitem.cumulative + jobitem.weight * percentage ) / m_overallQueueWeight;
message = jobitem.job->prettyStatusMessage();
// In progress reports at the start of a job (e.g. when the queue
// starts the job, or if the job itself reports 0.0) be more
// accepting in what gets reported: jobs with no status fall
// back to description and name, whichever is non-empty.
//
// Later calls (e.g. when percentage > 0) use the status unchanged.
// It may be empty, but the ExecutionViewStep knows about empty
// status messages and does not update the text in that case.
//
// This means that a Job can implement just prettyName() and get
// a reasonable "status" message which will update only once.
if ( percentage == 0.0 && message.isEmpty() )
{
message = jobitem.job->prettyDescription();
if ( message.isEmpty() )
{
message = jobitem.job->prettyName();
}
}
}
else
{
progress = 1.0;
message = tr( "Done" );
}
QMetaObject::invokeMethod(
m_queue, "progress", Qt::QueuedConnection, Q_ARG( qreal, progress ), Q_ARG( QString, message ) );
}
mutable QMutex m_runMutex;
mutable QMutex m_enqueMutex;
std::unique_ptr< WeightedJobList > m_runningJobs = std::make_unique< WeightedJobList >();
std::unique_ptr< WeightedJobList > m_queuedJobs = std::make_unique< WeightedJobList >();
JobQueue* m_queue;
int m_jobIndex = 0; ///< Index into m_runningJobs
qreal m_overallQueueWeight = 0.0; ///< cumulation when **all** the jobs are done
};
JobThread::~JobThread() {}
JobQueue* JobQueue::s_instance = nullptr;
JobQueue*
JobQueue::instance()
{
if ( !s_instance )
{
cWarning() << "Getting nullptr JobQueue instance.";
}
return s_instance;
}
JobQueue::JobQueue( QObject* parent )
: QObject( parent )
, m_thread( new JobThread( this ) )
, m_storage( new GlobalStorage( this ) )
{
Q_ASSERT( !s_instance );
s_instance = this;
}
JobQueue::~JobQueue()
{
if ( m_thread->isRunning() )
{
m_thread->terminate();
if ( !m_thread->wait( 300 ) )
{
cError() << "Could not terminate job thread (expect a crash now).";
}
delete m_thread;
}
delete m_storage;
s_instance = nullptr;
}
void
JobQueue::start()
{
Q_ASSERT( !m_thread->isRunning() );
m_thread->finalize();
m_finished = false;
m_thread->start();
auto* inhibitor = new PowerManagementInterface( this );
inhibitor->inhibitSleep();
connect( this, &JobQueue::finished, inhibitor, &PowerManagementInterface::uninhibitSleep );
}
void
JobQueue::enqueue( int moduleWeight, const JobList& jobs )
{
Q_ASSERT( !m_thread->isRunning() );
m_thread->enqueue( moduleWeight, jobs );
emit queueChanged( m_thread->queuedJobs() );
}
void
JobQueue::finish()
{
m_finished = true;
emit finished();
emit queueChanged( m_thread->queuedJobs() );
}
GlobalStorage*
JobQueue::globalStorage() const
{
return m_storage;
}
} // namespace Calamares
#include "utils/moc-warnings.h"
#include "JobQueue.moc"

View File

@@ -0,0 +1,122 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_JOBQUEUE_H
#define CALAMARES_JOBQUEUE_H
#include "DllMacro.h"
#include "Job.h"
#include <QObject>
namespace Calamares
{
class GlobalStorage;
class JobThread;
///@brief RAII class to suppress sleep / suspend during its lifetime
class DLLEXPORT SleepInhibitor : public QObject
{
Q_OBJECT
public:
SleepInhibitor();
~SleepInhibitor() override;
};
class DLLEXPORT JobQueue : public QObject
{
Q_OBJECT
public:
explicit JobQueue( QObject* parent = nullptr );
~JobQueue() override;
/** @brief Returns the most-recently-created instance.
*
* It is possible for instance() to be @c nullptr, since you must
* call the constructor explicitly first.
*/
static JobQueue* instance();
/* @brief Returns the GlobalStorage object for the instance.
*
* It is possible for instanceGlobalStorage() to be @c nullptr,
* since there might not be an instance to begin with.
*/
static GlobalStorage* instanceGlobalStorage()
{
auto* jq = instance();
return jq ? jq->globalStorage() : nullptr;
}
GlobalStorage* globalStorage() const;
/** @brief Queues up jobs from a single module source
*
* The total weight of the jobs is spread out to fill the weight
* of the module.
*/
void enqueue( int moduleWeight, const JobList& jobs );
/** @brief Starts all the jobs that are enqueued.
*
* After this, isRunning() returns @c true until
* finished() is emitted.
*/
void start();
bool isRunning() const { return !m_finished; }
signals:
/** @brief Report progress of the whole queue, with a status message
*
* The @p percent is a value between 0.0 and 1.0 (100%) of the
* overall queue progress (not of the current job), while
* @p prettyName is the status message from the job -- often
* just the name of the job, but some jobs include more information.
*/
void progress( qreal percent, const QString& prettyName );
/** @brief Indicate that the queue is empty, after calling start()
*
* Emitted when the queue empties. The queue may also emit
* failed(), if something went wrong, but finished() is always
* the last one.
*/
void finished();
/** @brief A job in the queue failed.
*
* Contains the (already-translated) text from the job describing
* the failure.
*/
void failed( const QString& message, const QString& details );
/** @brief Reports the names of jobs in the queue.
*
* When jobs are added via enqueue(), or when the queue otherwise
* changes, the **names** of the jobs are reported. This is
* primarily for debugging purposes.
*/
void queueChanged( const QStringList& jobNames );
public Q_SLOTS:
/** @brief Implementation detail
*
* This is a private implementation detail for the job thread,
* which should not be called by other core.
*/
void finish();
private:
static JobQueue* s_instance;
JobThread* m_thread;
GlobalStorage* m_storage;
bool m_finished = true; ///< Initially, not running
};
} // namespace Calamares
#endif // CALAMARES_JOBQUEUE_H

View File

@@ -0,0 +1,65 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "ProcessJob.h"
#include "utils/CommandList.h"
#include "utils/Logger.h"
#include <QDir>
namespace Calamares
{
ProcessJob::ProcessJob( const QString& command,
const QString& workingPath,
bool runInChroot,
std::chrono::seconds secondsTimeout,
QObject* parent )
: Job( parent )
, m_command( command )
, m_workingPath( workingPath )
, m_runInChroot( runInChroot )
, m_timeoutSec( secondsTimeout )
{
}
ProcessJob::~ProcessJob() = default;
QString
ProcessJob::prettyName() const
{
return ( m_runInChroot ? QStringLiteral( "Run command '%1' in target system" )
: QStringLiteral( "Run command '%1'" ) )
.arg( m_command );
}
QString
ProcessJob::prettyStatusMessage() const
{
if ( m_runInChroot )
{
return tr( "Running command %1 in target system…", "@status" ).arg( m_command );
}
else
{
return tr( "Running command %1…", "@status" ).arg( m_command );
}
}
JobResult
ProcessJob::exec()
{
Calamares::CommandList l( m_runInChroot, m_timeoutSec );
l.push_back( Calamares::CommandLine { m_command } );
return l.run();
}
} // namespace Calamares

View File

@@ -0,0 +1,46 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_PROCESSJOB_H
#define CALAMARES_PROCESSJOB_H
#include "DllMacro.h"
#include "Job.h"
#include <chrono>
namespace Calamares
{
class DLLEXPORT ProcessJob : public Job
{
Q_OBJECT
public:
explicit ProcessJob( const QString& command,
const QString& workingPath,
bool runInChroot = false,
std::chrono::seconds secondsTimeout = std::chrono::seconds( 30 ),
QObject* parent = nullptr );
~ProcessJob() override;
QString prettyName() const override;
QString prettyStatusMessage() const override;
JobResult exec() override;
private:
QString m_command;
QString m_workingPath;
bool m_runInChroot;
std::chrono::seconds m_timeoutSec;
};
} // namespace Calamares
#endif // CALAMARES_PROCESSJOB_H

View File

@@ -0,0 +1,483 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Gabriel Craciunescu <crazy@frugalware.org>
* SPDX-FileCopyrightText: 2019 Dominic Hayes <ferenosdev@outlook.com>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "Settings.h"
#include "CalamaresConfig.h"
#include "compat/Variant.h"
#include "utils/Dirs.h"
#include "utils/Logger.h"
#include "utils/Yaml.h"
#include <QDir>
#include <QFile>
#include <QPair>
static bool
hasValue( const YAML::Node& v )
{
return v.IsDefined() && !v.IsNull();
}
/** @brief Helper function to grab a QString out of the config, and to warn if not present. */
static QString
requireString( const ::YAML::Node& config, const char* key )
{
auto v = config[ key ];
if ( hasValue( v ) )
{
return QString::fromStdString( v.as< std::string >() );
}
else
{
cWarning() << Logger::SubEntry << "Required settings.conf key" << key << "is missing.";
return QString();
}
}
/** @brief Helper function to grab a bool out of the config, and to warn if not present. */
static bool
requireBool( const ::YAML::Node& config, const char* key, bool d )
{
auto v = config[ key ];
if ( hasValue( v ) )
{
return v.as< bool >();
}
else
{
cWarning() << Logger::SubEntry << "Required settings.conf key" << key << "is missing.";
return d;
}
}
namespace Calamares
{
InstanceDescription::InstanceDescription( const Calamares::ModuleSystem::InstanceKey& key )
: m_instanceKey( key )
, m_weight( -1 )
{
if ( !isValid() )
{
m_weight = 0;
}
else
{
m_configFileName = key.module() + QStringLiteral( ".conf" );
}
}
InstanceDescription
InstanceDescription::fromSettings( const QVariantMap& m )
{
InstanceDescription r(
Calamares::ModuleSystem::InstanceKey( m.value( "module" ).toString(), m.value( "id" ).toString() ) );
if ( r.isValid() )
{
if ( m.value( "weight" ).isValid() )
{
int w = qBound( 1, m.value( "weight" ).toInt(), 100 );
r.m_weight = w;
}
QString c = m.value( "config" ).toString();
if ( !c.isEmpty() )
{
r.m_configFileName = c;
}
}
return r;
}
Settings* Settings::s_instance = nullptr;
Settings*
Settings::instance()
{
if ( !s_instance )
{
cWarning() << "Getting nullptr Settings instance.";
}
return s_instance;
}
static void
interpretModulesSearch( const bool debugMode, const QStringList& rawPaths, QStringList& output )
{
for ( const auto& path : rawPaths )
{
if ( path == "local" )
{
// If we're running in debug mode, we assume we might also be
// running from the build dir, so we add a maximum priority
// module search path in the build dir.
if ( debugMode )
{
QString buildDirModules
= QDir::current().absolutePath() + QDir::separator() + "src" + QDir::separator() + "modules";
if ( QDir( buildDirModules ).exists() )
{
output.append( buildDirModules );
}
}
// Install path is set in CalamaresAddPlugin.cmake
output.append( Calamares::systemLibDir().absolutePath() + QDir::separator() + "calamares"
+ QDir::separator() + "modules" );
}
else
{
QDir d( path );
if ( d.exists() && d.isReadable() )
{
output.append( d.absolutePath() );
}
else
{
cDebug() << Logger::SubEntry << "module-search entry non-existent" << path;
}
}
}
}
static void
interpretInstances( const ::YAML::Node& node, Settings::InstanceDescriptionList& customInstances )
{
// Parse the custom instances section
if ( node )
{
QVariant instancesV = Calamares::YAML::toVariant( node ).toList();
if ( typeOf( instancesV ) == ListVariantType )
{
const auto instances = instancesV.toList();
for ( const QVariant& instancesVListItem : instances )
{
if ( typeOf( instancesVListItem ) != MapVariantType )
{
continue;
}
auto description = InstanceDescription::fromSettings( instancesVListItem.toMap() );
if ( !description.isValid() )
{
cWarning() << "Invalid entry in *instances*" << instancesVListItem;
}
// Append it **anyway**, since this will bail out after Settings is constructed
customInstances.append( description );
}
}
}
}
static void
interpretSequence( const ::YAML::Node& node, Settings::ModuleSequence& moduleSequence )
{
// Parse the modules sequence section
if ( node )
{
QVariant sequenceV = Calamares::YAML::toVariant( node );
if ( typeOf( sequenceV ) != ListVariantType )
{
throw ::YAML::Exception( ::YAML::Mark(), "sequence key does not have a list-value" );
}
const auto sequence = sequenceV.toList();
for ( const QVariant& sequenceVListItem : sequence )
{
if ( typeOf( sequenceVListItem ) != MapVariantType )
{
continue;
}
QString thisActionS = sequenceVListItem.toMap().firstKey();
ModuleSystem::Action thisAction;
if ( thisActionS == "show" )
{
thisAction = ModuleSystem::Action::Show;
}
else if ( thisActionS == "exec" )
{
thisAction = ModuleSystem::Action::Exec;
}
else
{
cDebug() << "Unknown action in *sequence*" << thisActionS;
continue;
}
QStringList thisActionRoster = sequenceVListItem.toMap().value( thisActionS ).toStringList();
Calamares::ModuleSystem::InstanceKeyList roster;
roster.reserve( thisActionRoster.count() );
for ( const auto& s : thisActionRoster )
{
auto instanceKey = Calamares::ModuleSystem::InstanceKey::fromString( s );
if ( !instanceKey.isValid() )
{
cWarning() << "Invalid instance in *sequence*" << s;
}
roster.append( instanceKey );
}
moduleSequence.append( qMakePair( thisAction, roster ) );
}
}
else
{
throw ::YAML::Exception( ::YAML::Mark(), "sequence key is missing" );
}
}
Settings::Settings( bool debugMode )
: QObject()
, m_debug( debugMode )
, m_doChroot( true )
, m_promptInstall( false )
, m_disableCancel( false )
, m_disableCancelDuringExec( false )
{
cWarning() << "Using bogus Calamares settings in"
<< ( debugMode ? QStringLiteral( "debug" ) : QStringLiteral( "regular" ) ) << "mode";
s_instance = this;
}
Settings::Settings( const QString& settingsFilePath, bool debugMode )
: QObject()
, m_settingsPath( settingsFilePath )
, m_debug( debugMode )
, m_doChroot( true )
, m_promptInstall( false )
, m_disableCancel( false )
, m_disableCancelDuringExec( false )
{
cDebug() << "Using Calamares settings file at" << settingsFilePath;
QFile file( settingsFilePath );
if ( file.exists() && file.open( QFile::ReadOnly | QFile::Text ) )
{
setConfiguration( file.readAll(), file.fileName() );
}
else
{
cWarning() << "Cannot read settings file" << file.fileName();
}
s_instance = this;
}
bool
Settings::isModuleEnabled( const QString& module ) const
{
// Iterate over the list of modules searching for a match
for ( const auto& moduleInstance : std::as_const( m_moduleInstances ) )
{
if ( moduleInstance.key().module() == module )
{
return true;
}
}
return false;
}
void
Settings::reconcileInstancesAndSequence()
{
// Since moduleFinder captures targetKey by reference, we can
// update targetKey to change what the finder lambda looks for.
Calamares::ModuleSystem::InstanceKey targetKey;
auto moduleFinder = [ &targetKey ]( const InstanceDescription& d ) { return d.isValid() && d.key() == targetKey; };
// Check the sequence against the existing instances (which so far are only custom)
for ( const auto& step : m_modulesSequence )
{
for ( const auto& instanceKey : step.second )
{
targetKey = instanceKey;
const auto it = std::find_if( m_moduleInstances.constBegin(), m_moduleInstances.constEnd(), moduleFinder );
if ( it == m_moduleInstances.constEnd() )
{
if ( instanceKey.isCustom() )
{
cWarning() << "Custom instance key" << instanceKey << "is not listed in the *instances*";
}
m_moduleInstances.append( InstanceDescription( instanceKey ) );
}
}
}
}
void
Settings::setConfiguration( const QByteArray& ba, const QString& explainName )
{
try
{
// Not using Calamares::YAML:: convenience methods because we **want** the exception here
auto config = ::YAML::Load( ba.constData() );
Q_ASSERT( config.IsMap() );
interpretModulesSearch(
debugMode(), Calamares::YAML::toStringList( config[ "modules-search" ] ), m_modulesSearchPaths );
interpretInstances( config[ "instances" ], m_moduleInstances );
interpretSequence( config[ "sequence" ], m_modulesSequence );
m_brandingComponentName = requireString( config, "branding" );
m_promptInstall = requireBool( config, "prompt-install", false );
m_doChroot = !requireBool( config, "dont-chroot", false );
m_isSetupMode = requireBool( config, "oem-setup", !m_doChroot );
m_disableCancel = requireBool( config, "disable-cancel", false );
m_disableCancelDuringExec = requireBool( config, "disable-cancel-during-exec", false );
m_hideBackAndNextDuringExec = requireBool( config, "hide-back-and-next-during-exec", false );
m_quitAtEnd = requireBool( config, "quit-at-end", false );
reconcileInstancesAndSequence();
}
catch ( ::YAML::Exception& e )
{
Calamares::YAML::explainException( e, ba, explainName );
}
}
QStringList
Settings::modulesSearchPaths() const
{
return m_modulesSearchPaths;
}
Settings::InstanceDescriptionList
Settings::moduleInstances() const
{
return m_moduleInstances;
}
Settings::ModuleSequence
Settings::modulesSequence() const
{
return m_modulesSequence;
}
QString
Settings::brandingComponentName() const
{
return m_brandingComponentName;
}
static QStringList
settingsFileCandidates( bool assumeBuilddir )
{
static const char settings[] = "settings.conf";
QStringList settingsPaths;
if ( Calamares::isAppDataDirOverridden() )
{
settingsPaths << Calamares::appDataDir().absoluteFilePath( settings );
}
else
{
if ( assumeBuilddir )
{
settingsPaths << QDir::current().absoluteFilePath( settings );
}
if ( Calamares::haveExtraDirs() )
{
for ( auto s : Calamares::extraConfigDirs() )
{
settingsPaths << ( s + settings );
}
}
settingsPaths << CMAKE_INSTALL_FULL_SYSCONFDIR "/calamares/settings.conf"; // String concat
settingsPaths << Calamares::appDataDir().absoluteFilePath( settings );
}
return settingsPaths;
}
Settings*
Settings::init( bool debugMode )
{
if ( s_instance )
{
cWarning() << "Calamares::Settings already created";
return s_instance;
}
QStringList settingsFileCandidatesByPriority = settingsFileCandidates( debugMode );
QFileInfo settingsFile;
bool found = false;
foreach ( const QString& path, settingsFileCandidatesByPriority )
{
QFileInfo pathFi( path );
if ( pathFi.exists() && pathFi.isReadable() )
{
settingsFile = pathFi;
found = true;
break;
}
}
if ( !found || !settingsFile.exists() || !settingsFile.isReadable() )
{
cError() << "Cowardly refusing to continue startup without settings."
<< Logger::DebugList( settingsFileCandidatesByPriority );
if ( Calamares::isAppDataDirOverridden() )
{
cError() << "FATAL: explicitly configured application data directory is missing settings.conf";
}
else
{
cError() << "FATAL: none of the expected configuration file paths exist.";
}
::exit( EXIT_FAILURE );
}
auto* settings = new Calamares::Settings( settingsFile.absoluteFilePath(), debugMode ); // Creates singleton
if ( settings->modulesSequence().count() < 1 )
{
cError() << "FATAL: no sequence set.";
::exit( EXIT_FAILURE );
}
return settings;
}
Settings*
Settings::init( const QString& path )
{
if ( s_instance )
{
cWarning() << "Calamares::Settings already created";
return s_instance;
}
return new Calamares::Settings( path, true );
}
bool
Settings::isValid() const
{
if ( brandingComponentName().isEmpty() )
{
cWarning() << "No branding component is set";
return false;
}
const auto invalidDescriptor = []( const InstanceDescription& d ) { return !d.isValid(); };
const auto invalidDescriptorIt
= std::find_if( m_moduleInstances.constBegin(), m_moduleInstances.constEnd(), invalidDescriptor );
if ( invalidDescriptorIt != m_moduleInstances.constEnd() )
{
cWarning() << "Invalid module instance in *instances* or *sequence*";
return false;
}
return true;
}
} // namespace Calamares

View File

@@ -0,0 +1,205 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Gabriel Craciunescu <crazy@frugalware.org>
* SPDX-FileCopyrightText: 2019 Dominic Hayes <ferenosdev@outlook.com>
* SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef SETTINGS_H
#define SETTINGS_H
#include "DllMacro.h"
#include "modulesystem/Actions.h"
#include "modulesystem/InstanceKey.h"
#include <QObject>
#include <QStringList>
namespace Calamares
{
/** @brief Description of an instance as named in `settings.conf`
*
* An instance is an intended-step-in-sequence; it is not yet
* a loaded module. The instances have config-files and weights
* which are used by the module manager when loading modules
* and creating jobs.
*/
class DLLEXPORT InstanceDescription
{
using InstanceKey = Calamares::ModuleSystem::InstanceKey;
public:
/** @brief An invalid InstanceDescription
*
* Use `fromSettings()` to populate an InstanceDescription and
* check its validity.
*/
InstanceDescription() = default;
/** @brief An InstanceDescription with no special settings.
*
* Regardless of @p key being custom, sets weight to 1 and
* the configuration file to @c key.module() (plus the ".conf"
* extension).
*
* To InstanceDescription is custom if the key is.
*/
InstanceDescription( const InstanceKey& key );
static InstanceDescription fromSettings( const QVariantMap& );
bool isValid() const { return m_instanceKey.isValid(); }
const InstanceKey& key() const { return m_instanceKey; }
QString configFileName() const { return m_configFileName; }
bool isCustom() const { return m_instanceKey.isCustom(); }
int weight() const { return m_weight < 0 ? 1 : m_weight; }
bool explicitWeight() const { return m_weight > 0; }
private:
InstanceKey m_instanceKey;
QString m_configFileName;
int m_weight = 0;
};
class DLLEXPORT Settings : public QObject
{
Q_OBJECT
#ifdef BUILD_AS_TEST
public:
#endif
explicit Settings( bool debugMode );
explicit Settings( const QString& settingsFilePath, bool debugMode );
void setConfiguration( const QByteArray& configData, const QString& explainName );
void reconcileInstancesAndSequence();
public:
static Settings* instance();
/// @brief Find a settings.conf, following @p debugMode
static Settings* init( bool debugMode );
/// @brief Explicif filename, debug is always true (for testing)
static Settings* init( const QString& filename );
/// @brief Get the path this settings was created for (may be empty)
QString path() const { return m_settingsPath; }
QStringList modulesSearchPaths() const;
using InstanceDescriptionList = QList< InstanceDescription >;
/** @brief All the module instances used
*
* Each module-instance mentioned in `settings.conf` has an entry
* in the moduleInstances list -- both custom entries that are
* in the *instances* section, and each module mentioned in the
* *sequence*.
*/
InstanceDescriptionList moduleInstances() const;
using ModuleSequence = QList< QPair< ModuleSystem::Action, Calamares::ModuleSystem::InstanceKeyList > >;
/** @brief Representation of *sequence* of execution
*
* Each "section" of the *sequence* key in `settings.conf` gets an
* entry here, stating what kind of action is taken and which modules
* take part (in order). Each entry in the list is an instance key
* which can be found in the moduleInstances() list.
*/
ModuleSequence modulesSequence() const;
QString brandingComponentName() const;
/** @brief Are the settings consistent and valid?
*
* Checks that at least branding is set, and that the instances
* and sequence are valid.
*/
bool isValid() const;
/** @brief Is this a debugging run?
*
* Returns true if Calamares is in debug mode. In debug mode,
* modules and settings are loaded from more locations, to help
* development and debugging.
*/
bool debugMode() const { return m_debug; }
/** @brief Distinguish "install" from "OEM" modes.
*
* Returns true in "install" mode, which is where actions happen
* in a chroot -- the target system, which exists separately from
* the source system. In "OEM" mode, returns false and most actions
* apply to the *current* (host) system.
*/
bool doChroot() const { return m_doChroot; }
/** @brief Global setting of prompt-before-install.
*
* Returns true when the configuration is such that the user
* should be prompted one-last-time before any action is taken
* that really affects the machine.
*/
bool showPromptBeforeExecution() const { return m_promptInstall; }
/** @brief Distinguish between "install" and "setup" modes.
*
* This influences user-visible strings, for instance using the
* word "setup" instead of "install" where relevant.
*/
bool isSetupMode() const { return m_isSetupMode; }
/** @brief Returns whether the named module is enabled
*
* Returns true if @p module is enabled in settings.conf. Be aware that it
* only tests for a specific module name so if a QML and non-QML version
* of the same module exists, it must be specified explicitly
*
* @p module is a module name or module key e.g. packagechooser) and not a
* full module key+id (e.g. packagechooser@packagechooser)
*
*/
bool isModuleEnabled( const QString& module ) const;
/** @brief Global setting of disable-cancel: can't cancel ever. */
bool disableCancel() const { return m_disableCancel; }
/** @brief Temporary setting of disable-cancel: can't cancel during exec. */
bool disableCancelDuringExec() const { return m_disableCancelDuringExec; }
bool hideBackAndNextDuringExec() const { return m_hideBackAndNextDuringExec; }
/** @brief Is quit-at-end set? (Quit automatically when done) */
bool quitAtEnd() const { return m_quitAtEnd; }
private:
static Settings* s_instance;
QString m_settingsPath;
QStringList m_modulesSearchPaths;
InstanceDescriptionList m_moduleInstances;
ModuleSequence m_modulesSequence;
QString m_brandingComponentName;
// bools are initialized here according to default setting
bool m_debug;
bool m_doChroot = true;
bool m_isSetupMode = false;
bool m_promptInstall = false;
bool m_disableCancel = false;
bool m_disableCancelDuringExec = false;
bool m_hideBackAndNextDuringExec = false;
bool m_quitAtEnd = false;
};
} // namespace Calamares
#endif // SETTINGS_H

View File

@@ -0,0 +1,669 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018-2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
#include "compat/Variant.h"
#include "modulesystem/InstanceKey.h"
#include "utils/Logger.h"
#include <QObject>
#include <QSignalSpy>
#include <QtTest/QtTest>
class TestLibCalamares : public QObject
{
Q_OBJECT
public:
TestLibCalamares() {}
~TestLibCalamares() override {}
private Q_SLOTS:
void testGSModify();
void testGSLoadSave();
void testGSLoadSave2();
void testGSLoadSaveYAMLStringList();
void testGSNestedLookup();
void testInstanceKey();
void testInstanceDescription();
void testSettings();
void testJobQueue();
};
void
TestLibCalamares::testGSModify()
{
Calamares::GlobalStorage gs;
QSignalSpy spy( &gs, &Calamares::GlobalStorage::changed );
const QString key( "derp" );
QCOMPARE( gs.count(), 0 );
QVERIFY( !gs.contains( key ) );
const int value = 17;
gs.insert( key, value );
QCOMPARE( gs.count(), 1 );
QVERIFY( gs.contains( key ) );
QCOMPARE( Calamares::typeOf( gs.value( key ) ), Calamares::IntVariantType );
QCOMPARE( gs.value( key ).toString(), QString( "17" ) ); // It isn't a string, but does convert
QCOMPARE( gs.value( key ).toInt(), value );
gs.remove( key );
QCOMPARE( gs.count(), 0 );
QVERIFY( !gs.contains( key ) );
QCOMPARE( spy.count(), 2 ); // one insert, one remove
}
void
TestLibCalamares::testGSLoadSave()
{
Calamares::GlobalStorage gs;
const QString jsonfilename( "gs.test.json" );
const QString yamlfilename( "gs.test.yaml" );
gs.insert( "derp", 17 );
gs.insert( "cow", "moo" );
QVariantList l;
for ( const auto& s : QStringList { "dopey", "sneezy" } )
{
l.append( s );
}
gs.insert( "dwarfs", l );
QCOMPARE( gs.count(), 3 );
QVERIFY( gs.saveJson( jsonfilename ) );
Calamares::GlobalStorage gs2;
QCOMPARE( gs2.count(), 0 );
QVERIFY( gs2.loadJson( jsonfilename ) );
QCOMPARE( gs2.count(), 3 );
QCOMPARE( gs2.data(), gs.data() );
QVERIFY( gs.saveYaml( yamlfilename ) );
Calamares::GlobalStorage gs3;
QCOMPARE( gs3.count(), 0 );
QVERIFY( gs3.loadYaml( jsonfilename ) );
QCOMPARE( gs3.count(), 3 );
QCOMPARE( gs3.data(), gs.data() );
// YAML can load as JSON!
QVERIFY( gs3.loadYaml( jsonfilename ) );
QCOMPARE( gs3.count(), 3 );
QCOMPARE( gs3.data(), gs.data() );
// Failures in loading
QVERIFY( !gs3.loadJson( yamlfilename ) );
Calamares::GlobalStorage gs4;
gs4.insert( "derp", 32 );
gs4.insert( "dorp", "Varsseveld" );
QCOMPARE( gs4.count(), 2 );
QVERIFY( gs4.contains( "dorp" ) );
QCOMPARE( gs4.value( "derp" ).toInt(), 32 );
QVERIFY( gs4.loadJson( jsonfilename ) );
// 3 keys from the file, but one overwrite
QCOMPARE( gs4.count(), 4 );
QVERIFY( gs4.contains( "dorp" ) );
QCOMPARE( gs4.value( "derp" ).toInt(), 17 ); // This one was overwritten
}
void
TestLibCalamares::testGSLoadSave2()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
const QString filename( BUILD_AS_TEST "/testdata/yaml-list.conf" );
QVERIFY2( QFile::exists( filename ), qPrintable( filename ) );
Calamares::GlobalStorage gs1;
const QString key( "dwarfs" );
QVERIFY( gs1.loadYaml( filename ) );
QCOMPARE( gs1.count(), 4 ); // From examining the file
QVERIFY( gs1.contains( key ) );
cDebug() << Calamares::typeOf( gs1.value( key ) ) << gs1.value( key );
QCOMPARE( Calamares::typeOf( gs1.value( key ) ), Calamares::ListVariantType );
const QString yamlfilename( "gs.test.yaml" );
QVERIFY( gs1.saveYaml( yamlfilename ) );
Calamares::GlobalStorage gs2;
QVERIFY( gs2.loadYaml( yamlfilename ) );
QVERIFY( gs2.contains( key ) );
QCOMPARE( Calamares::typeOf( gs2.value( key ) ), Calamares::ListVariantType );
}
void
TestLibCalamares::testGSLoadSaveYAMLStringList()
{
Calamares::GlobalStorage gs;
const QString yamlfilename( "gs.test.yaml" );
gs.insert( "derp", 17 );
gs.insert( "cow", "moo" );
gs.insert( "dwarfs", QStringList { "happy", "dopey", "sleepy", "sneezy", "doc", "thorin", "balin" } );
QCOMPARE( gs.count(), 3 );
QCOMPARE( gs.value( "dwarfs" ).toList().count(), 7 ); // There's seven dwarfs, right?
QVERIFY( gs.value( "dwarfs" ).toStringList().contains( "thorin" ) );
QVERIFY( !gs.value( "dwarfs" ).toStringList().contains( "gimli" ) );
QVERIFY( gs.saveYaml( yamlfilename ) );
Calamares::GlobalStorage gs2;
QCOMPARE( gs2.count(), 0 );
QVERIFY( gs2.loadYaml( yamlfilename ) );
QCOMPARE( gs2.count(), 3 );
QEXPECT_FAIL( "", "QStringList doesn't write out nicely", Continue );
QCOMPARE( gs2.value( "dwarfs" ).toList().count(), 7 ); // There's seven dwarfs, right?
QCOMPARE( gs2.value( "dwarfs" ).toString(), QStringLiteral( "<QStringList>" ) ); // .. they're gone
}
void
TestLibCalamares::testGSNestedLookup()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
const QString filename( BUILD_AS_TEST "/testdata/yaml-list.conf" );
QVERIFY2( QFile::exists( filename ), qPrintable( filename ) );
Calamares::GlobalStorage gs2;
QVERIFY( gs2.loadYaml( filename ) );
bool ok = false;
const auto v0 = Calamares::lookup( &gs2, "horse.colors.neck", ok );
QVERIFY( ok );
QVERIFY( v0.canConvert< QString >() );
QCOMPARE( v0.toString(), QStringLiteral( "roan" ) );
const auto v1 = Calamares::lookup( &gs2, "horse.colors.nose", ok );
QVERIFY( !ok );
QVERIFY( !v1.isValid() );
const auto v2 = Calamares::lookup( &gs2, "cow.colors.nose", ok );
QVERIFY( !ok );
QVERIFY( !v2.isValid() );
const auto v3 = Calamares::lookup( &gs2, "dwarfs", ok );
QVERIFY( ok );
QVERIFY( v3.canConvert< QVariantList >() ); // because it's a list-valued thing
const auto v4 = Calamares::lookup( &gs2, "dwarfs.sleepy", ok );
QVERIFY( !ok ); // Sleepy is a value in the list of dwarfs, not a key
const auto v5 = Calamares::lookup( &gs2, "derp", ok );
QVERIFY( ok );
QCOMPARE( v5.toInt(), 17 );
}
void
TestLibCalamares::testInstanceKey()
{
using InstanceKey = Calamares::ModuleSystem::InstanceKey;
{
InstanceKey k;
QVERIFY( !k.isValid() );
QVERIFY( !k.isCustom() );
QVERIFY( k.module().isEmpty() );
}
{
InstanceKey k( QStringLiteral( "welcome" ), QString() );
QVERIFY( k.isValid() );
QVERIFY( !k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( "welcome" ) );
QCOMPARE( k.id(), QStringLiteral( "welcome" ) );
}
{
InstanceKey k( QStringLiteral( "shellprocess" ), QStringLiteral( "zfssetup" ) );
QVERIFY( k.isValid() );
QVERIFY( k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( "shellprocess" ) );
QCOMPARE( k.id(), QStringLiteral( "zfssetup" ) );
}
{
// This is a bad idea, names and ids with odd punctuation
InstanceKey k( QStringLiteral( " o__O " ), QString() );
QVERIFY( k.isValid() );
QVERIFY( !k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( " o__O " ) );
}
{
// .. but @ is disallowed
InstanceKey k( QStringLiteral( "welcome@hi" ), QString() );
QVERIFY( !k.isValid() );
QVERIFY( !k.isCustom() );
QVERIFY( k.module().isEmpty() );
}
{
InstanceKey k = InstanceKey::fromString( "welcome" );
QVERIFY( k.isValid() );
QVERIFY( !k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( "welcome" ) );
QCOMPARE( k.id(), QStringLiteral( "welcome" ) );
}
{
InstanceKey k = InstanceKey::fromString( "welcome@welcome" );
QVERIFY( k.isValid() );
QVERIFY( !k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( "welcome" ) );
QCOMPARE( k.id(), QStringLiteral( "welcome" ) );
}
{
InstanceKey k = InstanceKey::fromString( "welcome@hi" );
QVERIFY( k.isValid() );
QVERIFY( k.isCustom() );
QCOMPARE( k.module(), QStringLiteral( "welcome" ) );
QCOMPARE( k.id(), QStringLiteral( "hi" ) );
}
{
InstanceKey k = InstanceKey::fromString( "welcome@hi@hi" );
QVERIFY( !k.isValid() );
QVERIFY( !k.isCustom() );
QVERIFY( k.module().isEmpty() );
QVERIFY( k.id().isEmpty() );
}
}
void
TestLibCalamares::testInstanceDescription()
{
using InstanceDescription = Calamares::InstanceDescription;
using InstanceKey = Calamares::ModuleSystem::InstanceKey;
// With invalid keys
//
//
{
InstanceDescription d;
QVERIFY( !d.isValid() );
QVERIFY( !d.isCustom() );
QCOMPARE( d.weight(), 0 );
QVERIFY( d.configFileName().isEmpty() );
QVERIFY( !d.explicitWeight() );
}
{
InstanceDescription d( InstanceKey {} ); // most-vexing, use brace-init instead
QVERIFY( !d.isValid() );
QVERIFY( !d.isCustom() );
QCOMPARE( d.weight(), 0 );
QVERIFY( d.configFileName().isEmpty() );
QVERIFY( !d.explicitWeight() );
}
// Private constructor
//
// This does set up the config file, to default values
{
InstanceDescription d( InstanceKey::fromString( "welcome" ) );
QVERIFY( d.isValid() );
QVERIFY( !d.isCustom() );
QCOMPARE( d.weight(), 1 ); // **now** the constraints kick in
QVERIFY( !d.configFileName().isEmpty() );
QCOMPARE( d.configFileName(), QStringLiteral( "welcome.conf" ) );
QVERIFY( !d.explicitWeight() );
}
{
InstanceDescription d( InstanceKey::fromString( "welcome@hi" ) );
QVERIFY( d.isValid() );
QVERIFY( d.isCustom() );
QCOMPARE( d.weight(), 1 ); // **now** the constraints kick in
QVERIFY( !d.configFileName().isEmpty() );
QCOMPARE( d.configFileName(), QStringLiteral( "welcome.conf" ) );
QVERIFY( !d.explicitWeight() );
}
// From settings, normal program flow
//
//
{
QVariantMap m;
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( !d.isValid() );
}
{
QVariantMap m;
m.insert( "name", "welcome" );
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( !d.isValid() );
QVERIFY( !d.explicitWeight() );
}
{
QVariantMap m;
m.insert( "module", "welcome" );
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( d.isValid() );
QVERIFY( !d.isCustom() );
// Valid, but no weight set by settings
QCOMPARE( d.weight(), 1 );
QVERIFY( !d.explicitWeight() );
QCOMPARE( d.key().module(), QString( "welcome" ) );
QCOMPARE( d.key().id(), QString( "welcome" ) );
QCOMPARE( d.configFileName(), QString( "welcome.conf" ) );
}
{
QVariantMap m;
m.insert( "module", "welcome" );
m.insert( "weight", 1 );
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( d.isValid() );
QVERIFY( !d.isCustom() );
//Valid, set explicitly
QCOMPARE( d.weight(), 1 );
QVERIFY( d.explicitWeight() );
QCOMPARE( d.key().module(), QString( "welcome" ) );
QCOMPARE( d.key().id(), QString( "welcome" ) );
QCOMPARE( d.configFileName(), QString( "welcome.conf" ) );
}
{
QVariantMap m;
m.insert( "module", "welcome" );
m.insert( "id", "hi" );
m.insert( "weight", "17" ); // String, that's kind of bogus
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( d.isValid() );
QVERIFY( d.isCustom() );
QCOMPARE( d.weight(), 17 );
QCOMPARE( d.key().module(), QString( "welcome" ) );
QCOMPARE( d.key().id(), QString( "hi" ) );
QCOMPARE( d.configFileName(), QString( "welcome.conf" ) );
QVERIFY( d.explicitWeight() );
}
{
QVariantMap m;
m.insert( "module", "welcome" );
m.insert( "id", "hi" );
m.insert( "weight", 134 );
m.insert( "config", "hi.conf" );
InstanceDescription d = InstanceDescription::fromSettings( m );
QVERIFY( d.isValid() );
QVERIFY( d.isCustom() );
QCOMPARE( d.weight(), 100 );
QCOMPARE( d.key().module(), QString( "welcome" ) );
QCOMPARE( d.key().id(), QString( "hi" ) );
QCOMPARE( d.configFileName(), QString( "hi.conf" ) );
QVERIFY( d.explicitWeight() );
}
}
void
TestLibCalamares::testSettings()
{
{
Calamares::Settings s( false );
QVERIFY( !s.debugMode() );
QVERIFY( !s.isValid() );
}
{
Calamares::Settings s( true );
QVERIFY( s.debugMode() );
QVERIFY( s.moduleInstances().isEmpty() );
QVERIFY( s.modulesSequence().isEmpty() );
QVERIFY( s.brandingComponentName().isEmpty() );
QVERIFY( !s.isValid() );
// *INDENT-OFF*
s.setConfiguration( R"(---
branding: default # needed for it to be considered valid
instances:
- module: welcome
id: hi
weight: 75
- module: welcome
id: yo
config: yolo.conf
sequence:
- show:
- welcome@hi
- welcome@yo
- dummycpp
- summary
- exec:
- welcome@hi
)",
QStringLiteral( "<testdata>" ) );
// *INDENT-ON*
QVERIFY( s.debugMode() );
QCOMPARE( s.moduleInstances().count(), 4 ); // there are 4 module instances mentioned
QCOMPARE( s.modulesSequence().count(), 2 ); // 2 steps (show, exec)
QVERIFY( !s.brandingComponentName().isEmpty() );
QVERIFY( s.isValid() );
// Make a lambda where we can adjust what it looks for from the outside,
// by capturing a reference.
QString moduleKey = QString( "welcome" );
auto moduleFinder = [ &moduleKey ]( const Calamares::InstanceDescription& d )
{ return d.isValid() && d.key().module() == moduleKey; };
const auto it0 = std::find_if( s.moduleInstances().constBegin(), s.moduleInstances().constEnd(), moduleFinder );
QVERIFY( it0 != s.moduleInstances().constEnd() );
moduleKey = QString( "derp" );
const auto it1 = std::find_if( s.moduleInstances().constBegin(), s.moduleInstances().constEnd(), moduleFinder );
QVERIFY( it1 == s.moduleInstances().constEnd() );
int validCount = 0;
int customCount = 0;
for ( const auto& d : s.moduleInstances() )
{
if ( d.isValid() )
{
validCount++;
}
if ( d.isCustom() )
{
customCount++;
}
QVERIFY( d.isCustom() ? d.isValid() : true ); // All custom entries are valid
if ( !d.isCustom() )
{
QCOMPARE( d.configFileName(), QString( "%1.conf" ).arg( d.key().module() ) );
}
else
{
// Specific cases from this config file
if ( d.key().id() == QString( "yo" ) )
{
QCOMPARE( d.configFileName(), QString( "yolo.conf" ) );
}
else
{
QCOMPARE( d.configFileName(), QString( "welcome.conf" ) ); // Not set in the settings data
}
}
}
QCOMPARE( customCount, 2 );
QCOMPARE( validCount, 4 ); // welcome@hi is listed twice, in *show* and *exec*
}
}
constexpr const std::chrono::milliseconds MAX_TEST_DURATION( 3000 );
constexpr const int MAX_TEST_SLEEP( 2 ); // seconds, < MAX_TEST_DURATION
Q_STATIC_ASSERT( std::chrono::seconds( MAX_TEST_SLEEP ) < MAX_TEST_DURATION );
class DummyJob : public Calamares::Job
{
public:
DummyJob( QObject* parent )
: Calamares::Job( parent )
{
}
~DummyJob() override;
QString prettyName() const override;
Calamares::JobResult exec() override;
};
DummyJob::~DummyJob() {}
QString
DummyJob::prettyName() const
{
return QString( "DummyJob" );
}
Calamares::JobResult
DummyJob::exec()
{
cDebug() << "Starting DummyJob";
progress( 0.5 );
QThread::sleep( MAX_TEST_SLEEP );
cDebug() << ".. continuing DummyJob";
progress( 0.75 );
return Calamares::JobResult::ok();
}
void
TestLibCalamares::testJobQueue()
{
// Run an empty queue
{
Calamares::JobQueue q;
QVERIFY( !q.isRunning() );
QSignalSpy spy_progress( &q, &Calamares::JobQueue::progress );
QSignalSpy spy_finished( &q, &Calamares::JobQueue::finished );
QSignalSpy spy_failed( &q, &Calamares::JobQueue::failed );
QEventLoop loop;
connect( &q, &Calamares::JobQueue::finished, &loop, &QEventLoop::quit );
QTimer::singleShot( MAX_TEST_DURATION, &loop, &QEventLoop::quit );
q.start();
QVERIFY( q.isRunning() );
loop.exec();
QVERIFY( !q.isRunning() );
QCOMPARE( spy_finished.count(), 1 );
QCOMPARE( spy_failed.count(), 0 );
QCOMPARE( spy_progress.count(), 1 ); // just one, 100% at queue end
}
// Run a dummy queue
{
Calamares::JobQueue q;
QVERIFY( !q.isRunning() );
q.enqueue( 8, Calamares::JobList() << Calamares::job_ptr( new DummyJob( this ) ) );
QSignalSpy spy_progress( &q, &Calamares::JobQueue::progress );
QSignalSpy spy_finished( &q, &Calamares::JobQueue::finished );
QSignalSpy spy_failed( &q, &Calamares::JobQueue::failed );
QEventLoop loop;
connect( &q, &Calamares::JobQueue::finished, &loop, &QEventLoop::quit );
QTimer::singleShot( MAX_TEST_DURATION, &loop, &QEventLoop::quit );
q.start();
QVERIFY( q.isRunning() );
loop.exec();
QVERIFY( !q.isRunning() );
QCOMPARE( spy_finished.count(), 1 );
QCOMPARE( spy_failed.count(), 0 );
// 0% by the queue at job start
// 50% by the job itself
// 90% by the job itself
// 100% by the queue at job end
// 100% by the queue at queue end
QCOMPARE( spy_progress.count(), 5 );
}
{
Calamares::JobQueue q;
QVERIFY( !q.isRunning() );
q.enqueue( 8, Calamares::JobList() << Calamares::job_ptr( new DummyJob( this ) ) );
q.enqueue( 12,
Calamares::JobList() << Calamares::job_ptr( new DummyJob( this ) )
<< Calamares::job_ptr( new DummyJob( this ) ) );
QSignalSpy spy_progress( &q, &Calamares::JobQueue::progress );
QSignalSpy spy_finished( &q, &Calamares::JobQueue::finished );
QSignalSpy spy_failed( &q, &Calamares::JobQueue::failed );
QEventLoop loop;
connect( &q, &Calamares::JobQueue::finished, &loop, &QEventLoop::quit );
// Run the loop longer because the jobs take longer (there are 3 of them)
QTimer::singleShot( 3 * MAX_TEST_DURATION, &loop, &QEventLoop::quit );
q.start();
QVERIFY( q.isRunning() );
loop.exec();
QVERIFY( !q.isRunning() );
QCOMPARE( spy_finished.count(), 1 );
QCOMPARE( spy_failed.count(), 0 );
// 0% by the queue at job start
// 50% by the job itself
// 90% by the job itself
// 100% by the queue at job end
// 4 more for the next job
// 4 more for the next job
// 100% by the queue at queue end
QCOMPARE( spy_progress.count(), 13 );
/* Consider how progress will be reported:
*
* - the first module has weight 8, so the 1 job it has has weight 8
* - the second module has weight 12, so each of its two jobs has weight 6
*
* Total weight of the modules is 20. So the events are
*
* Job Progress Overall Weight Consumed Overall Progress
* 1 0 0 0.00
* 1 50 4 0.20
* 1 75 6 0.30
* 1 100 8 0.40
* 2 0 8 0.40
* 2 50 11 (8 + 50% of 6) 0.55
* 2 75 12.5 0.625
* 2 100 14 0.70
* 3 0 14 0.70
* 3 50 17 0.85
* 3 75 18.5 0.925
* 3 100 20 1.00
* - 100 20 1.00
*/
cDebug() << "Progress signals:";
qreal overallProgress = 0.0;
for ( const auto& e : spy_progress )
{
QCOMPARE( e.count(), 2 );
const auto v = e.first();
QVERIFY( v.canConvert< qreal >() );
qreal progress = v.toReal();
cDebug() << Logger::SubEntry << progress;
QVERIFY( progress >= overallProgress ); // Doesn't go backwards
overallProgress = progress;
}
}
}
QTEST_GUILESS_MAIN( TestLibCalamares )
#include "utils/moc-warnings.h"
#include "Tests.moc"

View File

@@ -0,0 +1,30 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2024 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_COMPAT_XML_H
#define CALAMARES_COMPAT_XML_H
#include <QCheckBox>
namespace Calamares
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 7, 0 )
using checkBoxStateType = int;
const auto checkBoxStateChangedSignal = &QCheckBox::stateChanged;
constexpr checkBoxStateType checkBoxUncheckedValue = 0;
#else
using checkBoxStateType = Qt::CheckState;
const auto checkBoxStateChangedSignal = &QCheckBox::checkStateChanged;
constexpr checkBoxStateType checkBoxUncheckedValue = Qt::Unchecked;
#endif
} // namespace Calamares
#endif

View File

@@ -0,0 +1,30 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2023 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_COMPAT_MUTEX_H
#define CALAMARES_COMPAT_MUTEX_H
#include <QMutexLocker>
namespace Calamares
{
/*
* In Qt5, QMutexLocker is a class and operates implicitly on
* QMutex but in Qt6 it is a template and needs a specialization.
*/
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
using MutexLocker = QMutexLocker;
#else
using MutexLocker = QMutexLocker< QMutex >;
#endif
} // namespace Calamares
#endif

View File

@@ -0,0 +1,23 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2024 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_COMPAT_SIZE_H
#define CALAMARES_COMPAT_SIZE_H
#include <QVariant>
namespace Calamares
{
/* Compatibility of size types (e.g. qsizetype, STL sizes, int) between Qt5 and Qt6 */
using NumberForTr = int;
}
#endif

View File

@@ -0,0 +1,57 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2023 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_COMPAT_VARIANT_H
#define CALAMARES_COMPAT_VARIANT_H
#include <QVariant>
namespace Calamares
{
/* Compatibility of QVariant between Qt5 and Qt6 */
#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
const auto typeOf = []( const QVariant& v ) { return v.type(); };
const auto ListVariantType = QVariant::List;
const auto MapVariantType = QVariant::Map;
const auto HashVariantType = QVariant::Hash;
const auto StringVariantType = QVariant::String;
const auto CharVariantType = QVariant::Char;
const auto StringListVariantType = QVariant::StringList;
const auto BoolVariantType = QVariant::Bool;
const auto IntVariantType = QVariant::Int;
const auto UIntVariantType = QVariant::UInt;
const auto LongLongVariantType = QVariant::LongLong;
const auto ULongLongVariantType = QVariant::ULongLong;
const auto DoubleVariantType = QVariant::Double;
#else
const auto typeOf = []( const QVariant& v ) { return v.typeId(); };
const auto ListVariantType = QMetaType::Type::QVariantList;
const auto MapVariantType = QMetaType::Type::QVariantMap;
const auto HashVariantType = QMetaType::Type::QVariantHash;
const auto StringVariantType = QMetaType::Type::QString;
const auto CharVariantType = QMetaType::Type::Char;
const auto StringListVariantType = QMetaType::Type::QStringList;
const auto BoolVariantType = QMetaType::Type::Bool;
const auto IntVariantType = QMetaType::Type::Int;
const auto UIntVariantType = QMetaType::Type::UInt;
const auto LongLongVariantType = QMetaType::Type::LongLong;
const auto ULongLongVariantType = QMetaType::Type::ULongLong;
const auto DoubleVariantType = QMetaType::Type::Double;
#endif
inline bool
isIntegerVariantType( const QVariant& v )
{
const auto t = typeOf( v );
return t == IntVariantType || t == UIntVariantType || t == LongLongVariantType || t == ULongLongVariantType;
}
} // namespace Calamares
#endif

View File

@@ -0,0 +1,42 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2024 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef CALAMARES_COMPAT_XML_H
#define CALAMARES_COMPAT_XML_H
#include <QDomDocument>
namespace Calamares
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
struct ParseResult
{
QString errorMessage;
int errorLine = -1;
int errorColumn = -1;
};
[[nodiscard]] inline ParseResult
setXmlContent( QDomDocument& doc, const QByteArray& ba )
{
ParseResult p;
const bool r = doc.setContent( ba, &p.errorMessage, &p.errorLine, &p.errorColumn );
return r ? ParseResult {} : p;
}
#else
[[nodiscard]] inline QDomDocument::ParseResult
setXmlContent( QDomDocument& doc, const QByteArray& ba )
{
return doc.setContent( ba );
}
#endif
} // namespace Calamares
#endif

View File

@@ -0,0 +1,35 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "GeoIPFixed.h"
namespace Calamares
{
namespace GeoIP
{
GeoIPFixed::GeoIPFixed( const QString& attribute )
: Interface( attribute.isEmpty() ? QStringLiteral( "Europe/Amsterdam" ) : attribute )
{
}
QString
GeoIPFixed::rawReply( const QByteArray& )
{
return m_element;
}
GeoIP::RegionZonePair
GeoIPFixed::processReply( const QByteArray& data )
{
return splitTZString( rawReply( data ) );
}
} // namespace GeoIP
} // namespace Calamares

View File

@@ -0,0 +1,43 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIP_GEOIPFIXED_H
#define GEOIP_GEOIPFIXED_H
#include "Interface.h"
namespace Calamares
{
namespace GeoIP
{
/** @brief GeoIP with a fixed return value
*
* The data is ignored entirely and the attribute value is returned unchanged.
* Note that you still need to provide a usable URL for a successful GeoIP
* lookup -- the URL's data is just ignored.
*
* @note This class is an implementation detail.
*/
class GeoIPFixed : public Interface
{
public:
/** @brief Configure the value to return from rawReply()
*
* An empty string, which would not be a valid zone name, is
* translated to "Europe/Amsterdam".
*/
explicit GeoIPFixed( const QString& value = QString() );
virtual RegionZonePair processReply( const QByteArray& ) override;
virtual QString rawReply( const QByteArray& ) override;
};
} // namespace GeoIP
} // namespace Calamares
#endif

View File

@@ -0,0 +1,92 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "GeoIPJSON.h"
#include "compat/Variant.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include "utils/Yaml.h"
#include <QByteArray>
namespace Calamares
{
namespace GeoIP
{
GeoIPJSON::GeoIPJSON( const QString& attribute )
: Interface( attribute.isEmpty() ? QStringLiteral( "time_zone" ) : attribute )
{
}
/** @brief Indexes into a map @m by selectors @p l
*
* Each element of @p l is an index into map @m or a sub-map thereof,
* so that "foo.bar.baz" looks up "baz" in the sub-map "bar" of sub-map
* "foo" of @p m, like a regular JSON lookup would.
*/
static QString
selectMap( const QVariantMap& m, const QStringList& l, int index )
{
if ( index >= l.count() )
{
return QString();
}
QString attributeName = l[ index ];
if ( index == l.count() - 1 )
{
return Calamares::getString( m, attributeName );
}
else
{
bool success = false; // bogus
if ( m.contains( attributeName ) )
{
return selectMap( Calamares::getSubMap( m, attributeName, success ), l, index + 1 );
}
return QString();
}
}
QString
GeoIPJSON::rawReply( const QByteArray& data )
{
try
{
auto doc = ::YAML::Load( data );
QVariant var = Calamares::YAML::toVariant( doc );
if ( !var.isNull() && var.isValid() && Calamares::typeOf( var ) == Calamares::MapVariantType )
{
return selectMap( var.toMap(), m_element.split( '.' ), 0 );
}
else
{
cWarning() << "Invalid YAML data for GeoIPJSON";
}
}
catch ( ::YAML::Exception& e )
{
Calamares::YAML::explainException( e, data, "GeoIP data" );
}
return QString();
}
GeoIP::RegionZonePair
GeoIPJSON::processReply( const QByteArray& data )
{
return splitTZString( rawReply( data ) );
}
} // namespace GeoIP
} // namespace Calamares

View File

@@ -0,0 +1,44 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIP_GEOIPJSON_H
#define GEOIP_GEOIPJSON_H
#include "Interface.h"
namespace Calamares
{
namespace GeoIP
{
/** @brief GeoIP lookup for services that return JSON.
*
* This is the original implementation of GeoIP lookup,
* (e.g. using the FreeGeoIP.net service), or similar.
*
* The data is assumed to be in JSON format with a time_zone attribute.
*
* @note This class is an implementation detail.
*/
class GeoIPJSON : public Interface
{
public:
/** @brief Configure the attribute name which is selected.
*
* If an empty string is passed in (not a valid attribute name),
* then "time_zone" is used.
*/
explicit GeoIPJSON( const QString& attribute = QString() );
virtual RegionZonePair processReply( const QByteArray& ) override;
virtual QString rawReply( const QByteArray& ) override;
};
} // namespace GeoIP
} // namespace Calamares
#endif

View File

@@ -0,0 +1,262 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "GeoIPTests.h"
#include "GeoIPFixed.h"
#include "GeoIPJSON.h"
#ifdef QT_XML_LIB
#include "GeoIPXML.h"
#endif
#include "Handler.h"
#include "network/Manager.h"
#include <QtTest/QtTest>
QTEST_GUILESS_MAIN( GeoIPTests )
using namespace Calamares::GeoIP;
GeoIPTests::GeoIPTests() {}
GeoIPTests::~GeoIPTests() {}
void
GeoIPTests::initTestCase()
{
}
static const char json_data_attribute[] = "{\"time_zone\":\"Europe/Amsterdam\"}";
void
GeoIPTests::testJSON()
{
GeoIPJSON handler;
auto tz = handler.processReply( json_data_attribute );
QCOMPARE( tz.region(), QStringLiteral( "Europe" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Amsterdam" ) );
// JSON is quite tolerant
tz = handler.processReply( "time_zone: \"Europe/Brussels\"" );
QCOMPARE( tz.zone(), QStringLiteral( "Brussels" ) );
tz = handler.processReply( "time_zone: America/New_York\n" );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
}
void
GeoIPTests::testJSONalt()
{
GeoIPJSON handler( "zona_de_hora" );
auto tz = handler.processReply( json_data_attribute );
QCOMPARE( tz.region(), QString() ); // Not found
tz = handler.processReply( "tarifa: 12\nzona_de_hora: Europe/Madrid" );
QCOMPARE( tz.region(), QStringLiteral( "Europe" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Madrid" ) );
}
void
GeoIPTests::testJSONbad()
{
static const char data[] = "time_zone: 1";
GeoIPJSON handler;
auto tz = handler.processReply( data );
tz = handler.processReply( data );
QCOMPARE( tz.region(), QString() );
tz = handler.processReply( "" );
QCOMPARE( tz.region(), QString() );
tz = handler.processReply( "<html><body>404 Forbidden</body></html>" );
QCOMPARE( tz.region(), QString() );
tz = handler.processReply( "{ time zone = 'America/LosAngeles'}" );
QCOMPARE( tz.region(), QString() );
}
static const char xml_data_ubiquity[] =
R"(<Response>
<Ip>85.150.1.1</Ip>
<Status>OK</Status>
<CountryCode>NL</CountryCode>
<CountryCode3>NLD</CountryCode3>
<CountryName>Netherlands</CountryName>
<RegionCode>None</RegionCode>
<RegionName>None</RegionName>
<City>None</City>
<ZipPostalCode/>
<Latitude>50.0</Latitude>
<Longitude>4.0</Longitude>
<AreaCode>0</AreaCode>
<TimeZone>Europe/Amsterdam</TimeZone>
</Response>)";
void
GeoIPTests::testXML()
{
#ifdef QT_XML_LIB
GeoIPXML handler;
auto tz = handler.processReply( xml_data_ubiquity );
QCOMPARE( tz.region(), QStringLiteral( "Europe" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Amsterdam" ) );
#endif
}
void
GeoIPTests::testXML2()
{
#ifdef QT_XML_LIB
static const char data[]
= "<Response><TimeZone>America/North Dakota/Beulah</TimeZone></Response>"; // With a space!
GeoIPXML handler;
auto tz = handler.processReply( data );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
QCOMPARE( tz.zone(), QStringLiteral( "North_Dakota/Beulah" ) ); // Without space
#endif
}
void
GeoIPTests::testXMLalt()
{
#ifdef QT_XML_LIB
GeoIPXML handler( "ZT" );
auto tz = handler.processReply( "<A><B/><C><ZT>Moon/Dark_side</ZT></C></A>" );
QCOMPARE( tz.region(), QStringLiteral( "Moon" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Dark_side" ) );
#endif
}
void
GeoIPTests::testXMLbad()
{
#ifdef QT_XML_LIB
GeoIPXML handler;
auto tz = handler.processReply( "{time_zone: \"Europe/Paris\"}" );
QCOMPARE( tz.region(), QString() );
tz = handler.processReply( "<Response></Response>" );
QCOMPARE( tz.region(), QString() );
tz = handler.processReply( "fnord<html/>" );
QCOMPARE( tz.region(), QString() );
#endif
}
void
GeoIPTests::testSplitTZ()
{
using namespace Calamares::GeoIP;
auto tz = splitTZString( QStringLiteral( "Moon/Dark_side" ) );
QCOMPARE( tz.region(), QStringLiteral( "Moon" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Dark_side" ) );
// Some providers return weirdly escaped data
tz = splitTZString( QStringLiteral( "America\\/NewYork" ) );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
QCOMPARE( tz.zone(), QStringLiteral( "NewYork" ) ); // That's not actually the zone name
// Check that bogus data fails
tz = splitTZString( QString() );
QCOMPARE( tz.region(), QString() );
tz = splitTZString( QStringLiteral( "America.NewYork" ) );
QCOMPARE( tz.region(), QString() );
// Check that three-level is split properly and space is replaced
tz = splitTZString( QStringLiteral( "America/North Dakota/Beulah" ) );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
QCOMPARE( tz.zone(), QStringLiteral( "North_Dakota/Beulah" ) );
}
#define CHECK_GET( t, selector, url ) \
{ \
auto tz = GeoIP##t( selector ).processReply( Calamares::Network::Manager().synchronousGet( QUrl( url ) ) ); \
qDebug() << tz; \
QCOMPARE( default_tz, tz ); \
auto tz2 = Calamares::GeoIP::Handler( "" #t, url, selector ).get(); \
qDebug() << tz2; \
QCOMPARE( default_tz, tz2 ); \
}
void
GeoIPTests::testGet()
{
if ( !QProcessEnvironment::systemEnvironment().contains( QStringLiteral( "TEST_HTTP_GET" ) ) )
{
qDebug() << "Skipping HTTP GET tests, set TEST_HTTP_GET environment variable to enable";
return;
}
GeoIPJSON default_handler;
// Call the KDE service the definitive source.
auto default_tz = default_handler.processReply(
Calamares::Network::Manager().synchronousGet( QUrl( "https://geoip.kde.org/v1/calamares" ) ) );
// This is bogus, because the test isn't always run by me
// QCOMPARE( default_tz.region(), QStringLiteral("Europe") );
// QCOMPARE( default_tz.zone(), QStringLiteral("Amsterdam") );
QVERIFY( !default_tz.region().isEmpty() );
QVERIFY( !default_tz.zone().isEmpty() );
// Each expansion of CHECK_GET does a synchronous GET, then checks that
// the TZ data is the same as the default_tz; this is fragile if the
// services don't agree on the location of where the test is run.
CHECK_GET( JSON, QString(), "https://geoip.kde.org/v1/calamares" ) // Check it's consistent
CHECK_GET( JSON, QStringLiteral( "timezone" ), "https://ipapi.co/json" ) // Different JSON
CHECK_GET( JSON, QStringLiteral( "timezone" ), "http://ip-api.com/json" )
CHECK_GET( JSON, QStringLiteral( "Location.TimeZone" ), "https://geoip.kde.org/debug" ) // 2-level JSON
#ifdef QT_XML_LIB
CHECK_GET( XML, QString(), "http://geoip.ubuntu.com/lookup" ) // Ubiquity's XML format
CHECK_GET( XML, QString(), "https://geoip.kde.org/v1/ubiquity" ) // Temporary KDE service
#endif
}
void
GeoIPTests::testFixed()
{
{
GeoIPFixed f;
auto tz = f.processReply( QByteArray() );
QCOMPARE( tz.region(), QStringLiteral( "Europe" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Amsterdam" ) );
QCOMPARE( f.processReply( xml_data_ubiquity ), tz );
QCOMPARE( f.processReply( QByteArray( "derp" ) ), tz );
}
{
GeoIPFixed f( QStringLiteral( "America/Vancouver" ) );
auto tz = f.processReply( QByteArray() );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
QCOMPARE( tz.zone(), QStringLiteral( "Vancouver" ) );
QCOMPARE( f.processReply( xml_data_ubiquity ), tz );
QCOMPARE( f.processReply( QByteArray( "derp" ) ), tz );
}
{
GeoIPFixed f( QStringLiteral( "America/North Dakota/Beulah" ) );
auto tz = f.processReply( QByteArray() );
QCOMPARE( tz.region(), QStringLiteral( "America" ) );
QCOMPARE( tz.zone(), QStringLiteral( "North_Dakota/Beulah" ) );
QCOMPARE( f.processReply( xml_data_ubiquity ), tz );
QCOMPARE( f.processReply( QByteArray( "derp" ) ), tz );
}
}

View File

@@ -0,0 +1,37 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIPTESTS_H
#define GEOIPTESTS_H
#include <QObject>
class GeoIPTests : public QObject
{
Q_OBJECT
public:
GeoIPTests();
~GeoIPTests() override;
private Q_SLOTS:
void initTestCase();
void testFixed();
void testJSON();
void testJSONalt();
void testJSONbad();
void testXML();
void testXML2();
void testXMLalt();
void testXMLbad();
void testSplitTZ();
void testGet();
};
#endif

View File

@@ -0,0 +1,92 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "GeoIPXML.h"
#include "compat/Xml.h"
#include "utils/Logger.h"
#include <QtXml/QDomDocument>
namespace Calamares
{
namespace GeoIP
{
GeoIPXML::GeoIPXML( const QString& element )
: Interface( element.isEmpty() ? QStringLiteral( "TimeZone" ) : element )
{
}
static QStringList
getElementTexts( const QByteArray& data, const QString& tag )
{
QStringList elements;
QDomDocument doc;
const auto p = Calamares::setXmlContent( doc, data );
if ( p.errorMessage.isEmpty() )
{
const auto tzElements = doc.elementsByTagName( tag );
cDebug() << "GeoIP found" << tzElements.length() << "elements";
for ( int it = 0; it < tzElements.length(); ++it )
{
auto e = tzElements.at( it ).toElement();
auto e_text = e.text();
if ( !e_text.isEmpty() )
{
elements.append( e_text );
}
}
}
else
{
cWarning() << "GeoIP XML data error:" << p.errorMessage << "(line" << p.errorLine << ':' << p.errorColumn
<< ')';
}
if ( elements.count() < 1 )
{
cWarning() << "GeopIP XML had no non-empty elements" << tag;
}
return elements;
}
QString
GeoIPXML::rawReply( const QByteArray& data )
{
for ( const auto& e : getElementTexts( data, m_element ) )
{
if ( !e.isEmpty() )
{
return e;
}
}
return QString();
}
GeoIP::RegionZonePair
GeoIPXML::processReply( const QByteArray& data )
{
for ( const auto& e : getElementTexts( data, m_element ) )
{
auto tz = splitTZString( e );
if ( tz.isValid() )
{
return RegionZonePair( tz );
}
}
return RegionZonePair();
}
} // namespace GeoIP
} // namespace Calamares

View File

@@ -0,0 +1,44 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIP_GEOIPXML_H
#define GEOIP_GEOIPXML_H
#include "Interface.h"
namespace Calamares
{
namespace GeoIP
{
/** @brief GeoIP lookup with XML data
*
* The data is assumed to be in XML format with a
* <Response><TimeZone></TimeZone></Response>
* element, which contains the text (string) for the region/zone. This
* format is expected by, e.g. the Ubiquity installer.
*
* @note This class is an implementation detail.
*/
class GeoIPXML : public Interface
{
public:
/** @brief Configure the element tag which is selected.
*
* If an empty string is passed in (not a valid element tag),
* then "TimeZone" is used.
*/
explicit GeoIPXML( const QString& element = QString() );
virtual RegionZonePair processReply( const QByteArray& ) override;
virtual QString rawReply( const QByteArray& ) override;
};
} // namespace GeoIP
} // namespace Calamares
#endif

View File

@@ -0,0 +1,176 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "Handler.h"
#include "GeoIPFixed.h"
#include "GeoIPJSON.h"
#if defined( QT_XML_LIB )
#include "GeoIPXML.h"
#endif
#include "Settings.h"
#include "network/Manager.h"
#include "utils/Logger.h"
#include "utils/NamedEnum.h"
#include "utils/Variant.h"
#include <memory>
static const NamedEnumTable< Calamares::GeoIP::Handler::Type >&
handlerTypes()
{
using Type = Calamares::GeoIP::Handler::Type;
// *INDENT-OFF*
// clang-format off
static const NamedEnumTable<Type> names{
{ QStringLiteral( "none" ), Type::None },
{ QStringLiteral( "json" ), Type::JSON },
{ QStringLiteral( "xml" ), Type::XML },
{ QStringLiteral( "fixed" ), Type::Fixed }
};
// *INDENT-ON*
// clang-format on
return names;
}
namespace Calamares
{
namespace GeoIP
{
Handler::Handler()
: m_type( Type::None )
{
}
Handler::Handler( const QString& implementation, const QString& url, const QString& selector )
: m_type( Type::None )
, m_url( url )
, m_selector( selector )
{
bool ok = false;
m_type = handlerTypes().find( implementation, ok );
if ( !ok )
{
cWarning() << "GeoIP style" << implementation << "is not recognized.";
}
else if ( m_type == Type::None )
{
cWarning() << "GeoIP style *none* does not do anything.";
}
else if ( m_type == Type::Fixed && Calamares::Settings::instance()
&& !Calamares::Settings::instance()->debugMode() )
{
cWarning() << "GeoIP style *fixed* is not recommended for production.";
}
#if !defined( QT_XML_LIB )
else if ( m_type == Type::XML )
{
m_type = Type::None;
cWarning() << "GeoIP style *xml* is not supported in this version of Calamares.";
}
#endif
}
Handler::~Handler() {}
static std::unique_ptr< Interface >
create_interface( Handler::Type t, const QString& selector )
{
switch ( t )
{
case Handler::Type::None:
return nullptr;
case Handler::Type::JSON:
return std::make_unique< GeoIPJSON >( selector );
case Handler::Type::XML:
#if defined( QT_XML_LIB )
return std::make_unique< GeoIPXML >( selector );
#else
return nullptr;
#endif
case Handler::Type::Fixed:
return std::make_unique< GeoIPFixed >( selector );
}
__builtin_unreachable();
}
static RegionZonePair
do_query( Handler::Type type, const QString& url, const QString& selector )
{
const auto interface = create_interface( type, selector );
if ( !interface )
{
return RegionZonePair();
}
using namespace Calamares::Network;
return interface->processReply(
Calamares::Network::Manager().synchronousGet( url, { RequestOptions::FakeUserAgent } ) );
}
static QString
do_raw_query( Handler::Type type, const QString& url, const QString& selector )
{
const auto interface = create_interface( type, selector );
if ( !interface )
{
return QString();
}
using namespace Calamares::Network;
return interface->rawReply(
Calamares::Network::Manager().synchronousGet( url, { RequestOptions::FakeUserAgent } ) );
}
RegionZonePair
Handler::get() const
{
if ( !isValid() )
{
return RegionZonePair();
}
return do_query( m_type, m_url, m_selector );
}
QFuture< RegionZonePair >
Handler::query() const
{
Handler::Type type = m_type;
QString url = m_url;
QString selector = m_selector;
return QtConcurrent::run( [ = ] { return do_query( type, url, selector ); } );
}
QString
Handler::getRaw() const
{
if ( !isValid() )
{
return QString();
}
return do_raw_query( m_type, m_url, m_selector );
}
QFuture< QString >
Handler::queryRaw() const
{
Handler::Type type = m_type;
QString url = m_url;
QString selector = m_selector;
return QtConcurrent::run( [ = ] { return do_raw_query( type, url, selector ); } );
}
} // namespace GeoIP
} // namespace Calamares

View File

@@ -0,0 +1,86 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIP_HANDLER_H
#define GEOIP_HANDLER_H
#include "Interface.h"
#include <QString>
#include <QVariantMap>
#include <QtConcurrent/QtConcurrentRun>
namespace Calamares
{
namespace GeoIP
{
/** @brief Handle one complete GeoIP lookup.
*
* This class handles one complete GeoIP lookup. Create it with
* suitable configuration values, then call get(). This is a
* synchronous API and will return an invalid zone pair on
* error or if the configuration is not understood. For an
* async API, use query().
*/
class DLLEXPORT Handler
{
public:
enum class Type
{
None, // No lookup, returns empty string
JSON, // JSON-formatted data, returns extracted field
XML, // XML-formatted data, returns extracted field
Fixed // Returns selector string verbatim
};
/** @brief An unconfigured handler; this always returns errors. */
Handler();
/** @brief A handler for a specific GeoIP source.
*
* The @p implementation name selects an implementation; currently JSON and XML
* are supported. The @p url is retrieved by query() and then the @p selector
* is used to select something from the data returned by the @url.
*/
Handler( const QString& implementation, const QString& url, const QString& selector );
~Handler();
/** @brief Synchronously get the GeoIP result.
*
* If the Handler is valid, then do the actual fetching and interpretation
* of data and return the result. An invalid Handler will return an
* invalid (empty) result.
*/
RegionZonePair get() const;
/// @brief Like get, but don't interpret the contents
QString getRaw() const;
/** @brief Asynchronously get the GeoIP result.
*
* See get() for the return value.
*/
QFuture< RegionZonePair > query() const;
/// @brief Like query, but don't interpret the contents
QFuture< QString > queryRaw() const;
bool isValid() const { return m_type != Type::None; }
Type type() const { return m_type; }
QString url() const { return m_url; }
QString selector() const { return m_selector; }
private:
Type m_type;
const QString m_url;
const QString m_selector;
};
} // namespace GeoIP
} // namespace Calamares
#endif

View File

@@ -0,0 +1,46 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "Interface.h"
#include "utils/Logger.h"
#include "utils/String.h"
namespace Calamares
{
namespace GeoIP
{
Interface::Interface( const QString& e )
: m_element( e )
{
}
Interface::~Interface() {}
RegionZonePair
splitTZString( const QString& tz )
{
QString timezoneString( tz );
timezoneString.remove( '\\' );
timezoneString.replace( ' ', '_' );
QStringList tzParts = timezoneString.split( '/', SplitSkipEmptyParts );
if ( tzParts.size() >= 2 )
{
QString region = tzParts.takeFirst();
QString zone = tzParts.join( '/' );
return RegionZonePair( region, zone );
}
return RegionZonePair( QString(), QString() );
}
} // namespace GeoIP
} // namespace Calamares

View File

@@ -0,0 +1,127 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef GEOIP_INTERFACE_H
#define GEOIP_INTERFACE_H
#include "DllMacro.h"
#include <QDebug>
#include <QString>
#include <QUrl>
#include <tuple>
class QByteArray;
namespace Calamares
{
namespace GeoIP
{
/** @brief A Region, Zone pair of strings
*
* A GeoIP lookup returns a timezone, which is represented as a Region,
* Zone pair of strings (e.g. "Europe" and "Amsterdam"). Generally,
* pasting the strings back together with a "/" is the right thing to
* do. The Zone **may** contain a "/" (e.g. "Kentucky/Monticello").
*/
class DLLEXPORT RegionZonePair
{
public:
/** @brief Construct from two strings, like qMakePair(). */
RegionZonePair( const QString& region, const QString& zone )
: m_region( region )
, m_zone( zone )
{
}
/** @brief Construct from an existing pair. */
RegionZonePair( const RegionZonePair& p )
: RegionZonePair( p.m_region, p.m_zone )
{
}
/** @brief An invalid zone pair (empty strings). */
RegionZonePair() = default;
bool isValid() const { return !m_region.isEmpty(); }
QString region() const { return m_region; }
QString zone() const { return m_zone; }
friend bool operator==( const RegionZonePair& lhs, const RegionZonePair& rhs ) noexcept
{
return std::tie( lhs.m_region, lhs.m_zone ) == std::tie( rhs.m_region, rhs.m_zone );
}
QString asString() const { return isValid() ? region() + QChar( '/' ) + zone() : QString(); }
private:
QString m_region;
QString m_zone;
};
inline QDebug&
operator<<( QDebug&& s, const RegionZonePair& tz )
{
return s << tz.asString();
}
inline QDebug&
operator<<( QDebug& s, const RegionZonePair& tz )
{
return s << tz.asString();
}
/** @brief Splits a region/zone string into a pair.
*
* Cleans up the string by removing backslashes (\\)
* since some providers return silly-escaped names. Replaces
* spaces with _ since some providers return human-readable names.
* Splits on the first / in the resulting string, or returns a
* pair of empty QStrings if it can't. (e.g. America/North Dakota/Beulah
* will return "America", "North_Dakota/Beulah").
*/
DLLEXPORT RegionZonePair splitTZString( const QString& s );
/**
* @brief Interface for GeoIP retrievers.
*
* A GeoIP retriever takes a configured URL (from the config file)
* and can handle the data returned from its interpretation of that
* configured URL, returning a region and zone.
*/
class DLLEXPORT Interface
{
public:
virtual ~Interface();
/** @brief Handle a (successful) request by interpreting the data.
*
* Should return a ( <zone>, <region> ) pair, e.g.
* ( "Europe", "Amsterdam" ). This is called **only** if the
* request to the fullUrl was successful; the handler
* is free to read as much, or as little, data as it
* likes. On error, returns a RegionZonePair with empty
* strings (e.g. ( "", "" ) ).
*/
virtual RegionZonePair processReply( const QByteArray& ) = 0;
/** @brief Get the raw reply data. */
virtual QString rawReply( const QByteArray& ) = 0;
protected:
Interface( const QString& element = QString() );
QString m_element; // string for selecting from data
};
} // namespace GeoIP
} // namespace Calamares
#endif

View File

@@ -0,0 +1,85 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
/**
* This is a test-application that does one GeoIP parse.
*/
#include "GeoIPFixed.h"
#include "GeoIPJSON.h"
#ifdef QT_XML_LIB
#include "GeoIPXML.h"
#endif
#include "utils/Logger.h"
#include <iostream>
using std::cerr;
using namespace Calamares::GeoIP;
int
main( int argc, char** argv )
{
if ( ( argc != 2 ) && ( argc != 3 ) )
{
cerr << "Usage: curl url | test_geoip <format> [selector]\n";
return 1;
}
QString format( argv[ 1 ] );
QString selector = argc == 3 ? QString( argv[ 2 ] ) : QString();
Logger::setupLogLevel( Logger::LOGVERBOSE );
cDebug() << "Doing GeoIP interpretation with format=" << format << "selector=" << selector;
Interface* handler = nullptr;
if ( QStringLiteral( "json" ) == format )
{
handler = new GeoIPJSON( selector );
}
#ifdef QT_XML_LIB
else if ( QStringLiteral( "xml" ) == format )
{
handler = new GeoIPXML( selector );
}
#endif
else if ( QStringLiteral( "fixed" ) == format )
{
handler = new GeoIPFixed( selector );
}
if ( !handler )
{
cerr << "Unknown format '" << format.toLatin1().constData() << "'\n";
return 1;
}
QByteArray ba;
while ( !std::cin.eof() )
{
char arr[ 1024 ];
std::cin.read( arr, sizeof( arr ) );
int s = static_cast< int >( std::cin.gcount() );
ba.append( arr, s );
}
auto tz = handler->processReply( ba );
if ( tz.region().isEmpty() )
{
std::cout << "No TimeZone determined from input.\n";
}
else
{
std::cout << "TimeZone Region=" << tz.region().toLatin1().constData()
<< "\nTimeZone Zone=" << tz.zone().toLatin1().constData() << '\n';
}
return 0;
}

View File

@@ -0,0 +1,255 @@
/* GENERATED FILE DO NOT EDIT
*
* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 1991-2019 Unicode, Inc.
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: CC0-1.0
*
* This file is derived from CLDR data from Unicode, Inc. Applicable terms
* are listed at http://unicode.org/copyright.html , of which the most
* important are:
*
* A. Unicode Copyright
* 1. Copyright © 1991-2019 Unicode, Inc. All rights reserved.
* B. Definitions
* Unicode Data Files ("DATA FILES") include all data files under the directories:
* https://www.unicode.org/Public/
* C. Terms of Use
* 1. Certain documents and files on this website contain a legend indicating
* that "Modification is permitted." Any person is hereby authorized,
* without fee, to modify such documents and files to create derivative
* works conforming to the Unicode® Standard, subject to Terms and
* Conditions herein.
* 2. Any person is hereby authorized, without fee, to view, use, reproduce,
* and distribute all documents and files, subject to the Terms and
* Conditions herein.
*/
/* MODIFICATIONS
*
* Edited anyway:
* 20191211 India (IN) changed to AnyLanguage, since Hindi doesn't make sense. #1284
* 20210207 Belarus (BY) changed to Russian, as the more-common-language. #1634
* 20210615 Tokelau and Tuvalu country enum values changed to avoid deprecation warning.
*
*/
// BEGIN Generated from CLDR data
// *INDENT-OFF*
// clang-format off
struct CountryData
{
QLocale::Language l;
QLocale::Country c;
char cc1;
char cc2;
};
static constexpr int const country_data_size = 198;
static const CountryData country_data_table[] = {
{ QLocale::Language::Catalan, QLocale::Country::Andorra, 'A', 'D' },
{ QLocale::Language::Arabic, QLocale::Country::UnitedArabEmirates, 'A', 'E' },
{ QLocale::Language::Persian, QLocale::Country::Afghanistan, 'A', 'F' },
{ QLocale::Language::Albanian, QLocale::Country::Albania, 'A', 'L' },
{ QLocale::Language::Armenian, QLocale::Country::Armenia, 'A', 'M' },
{ QLocale::Language::Portuguese, QLocale::Country::Angola, 'A', 'O' },
{ QLocale::Language::AnyLanguage, QLocale::Country::Antarctica, 'A', 'Q' },
{ QLocale::Language::Spanish, QLocale::Country::Argentina, 'A', 'R' },
{ QLocale::Language::Samoan, QLocale::Country::AmericanSamoa, 'A', 'S' },
{ QLocale::Language::German, QLocale::Country::Austria, 'A', 'T' },
{ QLocale::Language::Dutch, QLocale::Country::Aruba, 'A', 'W' },
{ QLocale::Language::Swedish, QLocale::Country::AlandIslands, 'A', 'X' },
{ QLocale::Language::Azerbaijani, QLocale::Country::Azerbaijan, 'A', 'Z' },
{ QLocale::Language::Bosnian, QLocale::Country::BosniaAndHerzegowina, 'B', 'A' },
{ QLocale::Language::Bengali, QLocale::Country::Bangladesh, 'B', 'D' },
{ QLocale::Language::Dutch, QLocale::Country::Belgium, 'B', 'E' },
{ QLocale::Language::French, QLocale::Country::BurkinaFaso, 'B', 'F' },
{ QLocale::Language::Bulgarian, QLocale::Country::Bulgaria, 'B', 'G' },
{ QLocale::Language::Arabic, QLocale::Country::Bahrain, 'B', 'H' },
{ QLocale::Language::Rundi, QLocale::Country::Burundi, 'B', 'I' },
{ QLocale::Language::French, QLocale::Country::Benin, 'B', 'J' },
{ QLocale::Language::French, QLocale::Country::SaintBarthelemy, 'B', 'L' },
{ QLocale::Language::Malay, QLocale::Country::Brunei, 'B', 'N' },
{ QLocale::Language::Spanish, QLocale::Country::Bolivia, 'B', 'O' },
{ QLocale::Language::Papiamento, QLocale::Country::Bonaire, 'B', 'Q' },
{ QLocale::Language::Portuguese, QLocale::Country::Brazil, 'B', 'R' },
{ QLocale::Language::Dzongkha, QLocale::Country::Bhutan, 'B', 'T' },
{ QLocale::Language::AnyLanguage, QLocale::Country::BouvetIsland, 'B', 'V' },
{ QLocale::Language::Russian, QLocale::Country::Belarus, 'B', 'Y' },
{ QLocale::Language::Swahili, QLocale::Country::CongoKinshasa, 'C', 'D' },
{ QLocale::Language::French, QLocale::Country::CentralAfricanRepublic, 'C', 'F' },
{ QLocale::Language::French, QLocale::Country::CongoBrazzaville, 'C', 'G' },
{ QLocale::Language::German, QLocale::Country::Switzerland, 'C', 'H' },
{ QLocale::Language::French, QLocale::Country::IvoryCoast, 'C', 'I' },
{ QLocale::Language::Spanish, QLocale::Country::Chile, 'C', 'L' },
{ QLocale::Language::French, QLocale::Country::Cameroon, 'C', 'M' },
{ QLocale::Language::Chinese, QLocale::Country::China, 'C', 'N' },
{ QLocale::Language::Spanish, QLocale::Country::Colombia, 'C', 'O' },
{ QLocale::Language::AnyLanguage, QLocale::Country::ClippertonIsland, 'C', 'P' },
{ QLocale::Language::Spanish, QLocale::Country::CostaRica, 'C', 'R' },
{ QLocale::Language::Spanish, QLocale::Country::Cuba, 'C', 'U' },
{ QLocale::Language::Portuguese, QLocale::Country::CapeVerde, 'C', 'V' },
{ QLocale::Language::Papiamento, QLocale::Country::CuraSao, 'C', 'W' },
{ QLocale::Language::Greek, QLocale::Country::Cyprus, 'C', 'Y' },
{ QLocale::Language::Czech, QLocale::Country::CzechRepublic, 'C', 'Z' },
{ QLocale::Language::German, QLocale::Country::Germany, 'D', 'E' },
{ QLocale::Language::Afar, QLocale::Country::Djibouti, 'D', 'J' },
{ QLocale::Language::Danish, QLocale::Country::Denmark, 'D', 'K' },
{ QLocale::Language::Spanish, QLocale::Country::DominicanRepublic, 'D', 'O' },
{ QLocale::Language::Arabic, QLocale::Country::Algeria, 'D', 'Z' },
{ QLocale::Language::Spanish, QLocale::Country::CeutaAndMelilla, 'E', 'A' },
{ QLocale::Language::Spanish, QLocale::Country::Ecuador, 'E', 'C' },
{ QLocale::Language::Estonian, QLocale::Country::Estonia, 'E', 'E' },
{ QLocale::Language::Arabic, QLocale::Country::Egypt, 'E', 'G' },
{ QLocale::Language::Arabic, QLocale::Country::WesternSahara, 'E', 'H' },
{ QLocale::Language::Tigrinya, QLocale::Country::Eritrea, 'E', 'R' },
{ QLocale::Language::Spanish, QLocale::Country::Spain, 'E', 'S' },
{ QLocale::Language::Amharic, QLocale::Country::Ethiopia, 'E', 'T' },
{ QLocale::Language::English, QLocale::Country::EuropeanUnion, 'E', 'U' },
{ QLocale::Language::German, QLocale::Country::AnyCountry, 'E', 'Z' },
{ QLocale::Language::Finnish, QLocale::Country::Finland, 'F', 'I' },
{ QLocale::Language::Faroese, QLocale::Country::FaroeIslands, 'F', 'O' },
{ QLocale::Language::French, QLocale::Country::France, 'F', 'R' },
{ QLocale::Language::French, QLocale::Country::Gabon, 'G', 'A' },
{ QLocale::Language::Georgian, QLocale::Country::Georgia, 'G', 'E' },
{ QLocale::Language::French, QLocale::Country::FrenchGuiana, 'G', 'F' },
{ QLocale::Language::Akan, QLocale::Country::Ghana, 'G', 'H' },
{ QLocale::Language::Greenlandic, QLocale::Country::Greenland, 'G', 'L' },
{ QLocale::Language::French, QLocale::Country::Guinea, 'G', 'N' },
{ QLocale::Language::French, QLocale::Country::Guadeloupe, 'G', 'P' },
{ QLocale::Language::Spanish, QLocale::Country::EquatorialGuinea, 'G', 'Q' },
{ QLocale::Language::Greek, QLocale::Country::Greece, 'G', 'R' },
{ QLocale::Language::AnyLanguage, QLocale::Country::SouthGeorgiaAndTheSouthSandwichIslands, 'G', 'S' },
{ QLocale::Language::Spanish, QLocale::Country::Guatemala, 'G', 'T' },
{ QLocale::Language::Portuguese, QLocale::Country::GuineaBissau, 'G', 'W' },
{ QLocale::Language::Chinese, QLocale::Country::HongKong, 'H', 'K' },
{ QLocale::Language::AnyLanguage, QLocale::Country::HeardAndMcDonaldIslands, 'H', 'M' },
{ QLocale::Language::Spanish, QLocale::Country::Honduras, 'H', 'N' },
{ QLocale::Language::Croatian, QLocale::Country::Croatia, 'H', 'R' },
{ QLocale::Language::Haitian, QLocale::Country::Haiti, 'H', 'T' },
{ QLocale::Language::Hungarian, QLocale::Country::Hungary, 'H', 'U' },
{ QLocale::Language::Spanish, QLocale::Country::CanaryIslands, 'I', 'C' },
{ QLocale::Language::Indonesian, QLocale::Country::Indonesia, 'I', 'D' },
{ QLocale::Language::Hebrew, QLocale::Country::Israel, 'I', 'L' },
{ QLocale::Language::AnyLanguage, QLocale::Country::India, 'I', 'N' },
{ QLocale::Language::Arabic, QLocale::Country::Iraq, 'I', 'Q' },
{ QLocale::Language::Persian, QLocale::Country::Iran, 'I', 'R' },
{ QLocale::Language::Icelandic, QLocale::Country::Iceland, 'I', 'S' },
{ QLocale::Language::Italian, QLocale::Country::Italy, 'I', 'T' },
{ QLocale::Language::Arabic, QLocale::Country::Jordan, 'J', 'O' },
{ QLocale::Language::Japanese, QLocale::Country::Japan, 'J', 'P' },
{ QLocale::Language::Swahili, QLocale::Country::Kenya, 'K', 'E' },
{ QLocale::Language::Kirghiz, QLocale::Country::Kyrgyzstan, 'K', 'G' },
{ QLocale::Language::Khmer, QLocale::Country::Cambodia, 'K', 'H' },
{ QLocale::Language::Arabic, QLocale::Country::Comoros, 'K', 'M' },
{ QLocale::Language::Korean, QLocale::Country::NorthKorea, 'K', 'P' },
{ QLocale::Language::Korean, QLocale::Country::SouthKorea, 'K', 'R' },
{ QLocale::Language::Arabic, QLocale::Country::Kuwait, 'K', 'W' },
{ QLocale::Language::Russian, QLocale::Country::Kazakhstan, 'K', 'Z' },
{ QLocale::Language::Lao, QLocale::Country::Laos, 'L', 'A' },
{ QLocale::Language::Arabic, QLocale::Country::Lebanon, 'L', 'B' },
{ QLocale::Language::German, QLocale::Country::Liechtenstein, 'L', 'I' },
{ QLocale::Language::Sinhala, QLocale::Country::SriLanka, 'L', 'K' },
{ QLocale::Language::SouthernSotho, QLocale::Country::Lesotho, 'L', 'S' },
{ QLocale::Language::Lithuanian, QLocale::Country::Lithuania, 'L', 'T' },
{ QLocale::Language::French, QLocale::Country::Luxembourg, 'L', 'U' },
{ QLocale::Language::Latvian, QLocale::Country::Latvia, 'L', 'V' },
{ QLocale::Language::Arabic, QLocale::Country::Libya, 'L', 'Y' },
{ QLocale::Language::Arabic, QLocale::Country::Morocco, 'M', 'A' },
{ QLocale::Language::French, QLocale::Country::Monaco, 'M', 'C' },
{ QLocale::Language::Romanian, QLocale::Country::Moldova, 'M', 'D' },
{ QLocale::Language::Serbian, QLocale::Country::Montenegro, 'M', 'E' },
{ QLocale::Language::French, QLocale::Country::SaintMartin, 'M', 'F' },
{ QLocale::Language::Malagasy, QLocale::Country::Madagascar, 'M', 'G' },
{ QLocale::Language::Macedonian, QLocale::Country::Macedonia, 'M', 'K' },
{ QLocale::Language::Bambara, QLocale::Country::Mali, 'M', 'L' },
{ QLocale::Language::Burmese, QLocale::Country::Myanmar, 'M', 'M' },
{ QLocale::Language::Mongolian, QLocale::Country::Mongolia, 'M', 'N' },
{ QLocale::Language::Chinese, QLocale::Country::Macau, 'M', 'O' },
{ QLocale::Language::French, QLocale::Country::Martinique, 'M', 'Q' },
{ QLocale::Language::Arabic, QLocale::Country::Mauritania, 'M', 'R' },
{ QLocale::Language::Maltese, QLocale::Country::Malta, 'M', 'T' },
{ QLocale::Language::Morisyen, QLocale::Country::Mauritius, 'M', 'U' },
{ QLocale::Language::Divehi, QLocale::Country::Maldives, 'M', 'V' },
{ QLocale::Language::Spanish, QLocale::Country::Mexico, 'M', 'X' },
{ QLocale::Language::Malay, QLocale::Country::Malaysia, 'M', 'Y' },
{ QLocale::Language::Portuguese, QLocale::Country::Mozambique, 'M', 'Z' },
{ QLocale::Language::Afrikaans, QLocale::Country::Namibia, 'N', 'A' },
{ QLocale::Language::French, QLocale::Country::NewCaledonia, 'N', 'C' },
{ QLocale::Language::Hausa, QLocale::Country::Niger, 'N', 'E' },
{ QLocale::Language::Spanish, QLocale::Country::Nicaragua, 'N', 'I' },
{ QLocale::Language::Dutch, QLocale::Country::Netherlands, 'N', 'L' },
{ QLocale::Language::NorwegianBokmal, QLocale::Country::Norway, 'N', 'O' },
{ QLocale::Language::Nepali, QLocale::Country::Nepal, 'N', 'P' },
{ QLocale::Language::Arabic, QLocale::Country::Oman, 'O', 'M' },
{ QLocale::Language::Spanish, QLocale::Country::Panama, 'P', 'A' },
{ QLocale::Language::Spanish, QLocale::Country::Peru, 'P', 'E' },
{ QLocale::Language::French, QLocale::Country::FrenchPolynesia, 'P', 'F' },
{ QLocale::Language::TokPisin, QLocale::Country::PapuaNewGuinea, 'P', 'G' },
{ QLocale::Language::Filipino, QLocale::Country::Philippines, 'P', 'H' },
{ QLocale::Language::Urdu, QLocale::Country::Pakistan, 'P', 'K' },
{ QLocale::Language::Polish, QLocale::Country::Poland, 'P', 'L' },
{ QLocale::Language::French, QLocale::Country::SaintPierreAndMiquelon, 'P', 'M' },
{ QLocale::Language::Spanish, QLocale::Country::PuertoRico, 'P', 'R' },
{ QLocale::Language::Arabic, QLocale::Country::PalestinianTerritories, 'P', 'S' },
{ QLocale::Language::Portuguese, QLocale::Country::Portugal, 'P', 'T' },
{ QLocale::Language::Palauan, QLocale::Country::Palau, 'P', 'W' },
{ QLocale::Language::Guarani, QLocale::Country::Paraguay, 'P', 'Y' },
{ QLocale::Language::Arabic, QLocale::Country::Qatar, 'Q', 'A' },
{ QLocale::Language::English, QLocale::Country::OutlyingOceania, 'Q', 'O' },
{ QLocale::Language::French, QLocale::Country::Reunion, 'R', 'E' },
{ QLocale::Language::Romanian, QLocale::Country::Romania, 'R', 'O' },
{ QLocale::Language::Serbian, QLocale::Country::Serbia, 'R', 'S' },
{ QLocale::Language::Russian, QLocale::Country::Russia, 'R', 'U' },
{ QLocale::Language::Kinyarwanda, QLocale::Country::Rwanda, 'R', 'W' },
{ QLocale::Language::Arabic, QLocale::Country::SaudiArabia, 'S', 'A' },
{ QLocale::Language::French, QLocale::Country::Seychelles, 'S', 'C' },
{ QLocale::Language::Arabic, QLocale::Country::Sudan, 'S', 'D' },
{ QLocale::Language::Swedish, QLocale::Country::Sweden, 'S', 'E' },
{ QLocale::Language::Slovenian, QLocale::Country::Slovenia, 'S', 'I' },
{ QLocale::Language::NorwegianBokmal, QLocale::Country::SvalbardAndJanMayenIslands, 'S', 'J' },
{ QLocale::Language::Slovak, QLocale::Country::Slovakia, 'S', 'K' },
{ QLocale::Language::Italian, QLocale::Country::SanMarino, 'S', 'M' },
{ QLocale::Language::French, QLocale::Country::Senegal, 'S', 'N' },
{ QLocale::Language::Somali, QLocale::Country::Somalia, 'S', 'O' },
{ QLocale::Language::Dutch, QLocale::Country::Suriname, 'S', 'R' },
{ QLocale::Language::Portuguese, QLocale::Country::SaoTomeAndPrincipe, 'S', 'T' },
{ QLocale::Language::Spanish, QLocale::Country::ElSalvador, 'S', 'V' },
{ QLocale::Language::Arabic, QLocale::Country::Syria, 'S', 'Y' },
{ QLocale::Language::French, QLocale::Country::Chad, 'T', 'D' },
{ QLocale::Language::French, QLocale::Country::FrenchSouthernTerritories, 'T', 'F' },
{ QLocale::Language::French, QLocale::Country::Togo, 'T', 'G' },
{ QLocale::Language::Thai, QLocale::Country::Thailand, 'T', 'H' },
{ QLocale::Language::Tajik, QLocale::Country::Tajikistan, 'T', 'J' },
{ QLocale::Language::TokelauLanguage, QLocale::Country::TokelauCountry, 'T', 'K' },
{ QLocale::Language::Portuguese, QLocale::Country::EastTimor, 'T', 'L' },
{ QLocale::Language::Turkmen, QLocale::Country::Turkmenistan, 'T', 'M' },
{ QLocale::Language::Arabic, QLocale::Country::Tunisia, 'T', 'N' },
{ QLocale::Language::Tongan, QLocale::Country::Tonga, 'T', 'O' },
{ QLocale::Language::Turkish, QLocale::Country::Turkey, 'T', 'R' },
{ QLocale::Language::TuvaluLanguage, QLocale::Country::TuvaluCountry, 'T', 'V' },
{ QLocale::Language::Chinese, QLocale::Country::Taiwan, 'T', 'W' },
{ QLocale::Language::Swahili, QLocale::Country::Tanzania, 'T', 'Z' },
{ QLocale::Language::Ukrainian, QLocale::Country::Ukraine, 'U', 'A' },
{ QLocale::Language::Swahili, QLocale::Country::Uganda, 'U', 'G' },
{ QLocale::Language::Spanish, QLocale::Country::Uruguay, 'U', 'Y' },
{ QLocale::Language::Uzbek, QLocale::Country::Uzbekistan, 'U', 'Z' },
{ QLocale::Language::Italian, QLocale::Country::VaticanCityState, 'V', 'A' },
{ QLocale::Language::Spanish, QLocale::Country::Venezuela, 'V', 'E' },
{ QLocale::Language::Vietnamese, QLocale::Country::Vietnam, 'V', 'N' },
{ QLocale::Language::Bislama, QLocale::Country::Vanuatu, 'V', 'U' },
{ QLocale::Language::French, QLocale::Country::WallisAndFutunaIslands, 'W', 'F' },
{ QLocale::Language::Samoan, QLocale::Country::Samoa, 'W', 'S' },
{ QLocale::Language::Albanian, QLocale::Country::Kosovo, 'X', 'K' },
{ QLocale::Language::Arabic, QLocale::Country::Yemen, 'Y', 'E' },
{ QLocale::Language::French, QLocale::Country::Mayotte, 'Y', 'T' },
{ QLocale::Language::Shona, QLocale::Country::Zimbabwe, 'Z', 'W' },
{ QLocale::Language::AnyLanguage, QLocale::Country::AnyCountry, 0, 0 },
};
static_assert( (sizeof(country_data_table) / sizeof(CountryData)) == country_data_size, "Table size mismatch for CountryData" );
// END Generated from CLDR data

View File

@@ -0,0 +1,91 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "Global.h"
#include "GlobalStorage.h"
#include "utils/Logger.h"
namespace Calamares
{
namespace Locale
{
static const char gsKey[] = "localeConf";
template < typename T >
void
insertGS( const QMap< QString, T >& values, QVariantMap& localeConf )
{
for ( auto it = values.constBegin(); it != values.constEnd(); ++it )
{
localeConf.insert( it.key(), it.value() );
}
}
void
insertGS( Calamares::GlobalStorage& gs, const QMap< QString, QString >& values, InsertMode mode )
{
QVariantMap localeConf = mode == InsertMode::Overwrite ? QVariantMap() : gs.value( gsKey ).toMap();
insertGS( values, localeConf );
gs.insert( gsKey, localeConf );
}
void
insertGS( Calamares::GlobalStorage& gs, const QVariantMap& values, InsertMode mode )
{
QVariantMap localeConf = mode == InsertMode::Overwrite ? QVariantMap() : gs.value( gsKey ).toMap();
insertGS( values, localeConf );
gs.insert( gsKey, localeConf );
}
void
insertGS( Calamares::GlobalStorage& gs, const QString& key, const QString& value )
{
QVariantMap localeConf = gs.value( gsKey ).toMap();
localeConf.insert( key, value );
gs.insert( gsKey, localeConf );
}
void
removeGS( Calamares::GlobalStorage& gs, const QString& key )
{
if ( gs.contains( gsKey ) )
{
QVariantMap localeConf = gs.value( gsKey ).toMap();
if ( localeConf.contains( key ) )
{
localeConf.remove( key );
gs.insert( gsKey, localeConf );
}
}
}
void
clearGS( Calamares::GlobalStorage& gs )
{
gs.remove( gsKey );
}
QString
readGS( Calamares::GlobalStorage& gs, const QString& key )
{
if ( gs.contains( gsKey ) )
{
QVariantMap localeConf = gs.value( gsKey ).toMap();
if ( localeConf.contains( key ) )
{
return localeConf.value( key ).toString();
}
}
return QString();
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,83 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
/** @file GlobalStorage management for Locale settings
*
* The *localeConf* key in Global Storage is semi-structured,
* and there are multiple modules that write to it (and some that
* read from it). Functions in this file provide access to
* that semi-structured data.
*/
#ifndef LOCALE_GLOBAL_H
#define LOCALE_GLOBAL_H
#include "DllMacro.h"
#include <QMap>
#include <QString>
#include <QVariantMap>
namespace Calamares
{
class GlobalStorage;
namespace Locale
{
/** @brief Selector for methods that insert multiple values.
*
* When inserting, use @c Overwrite to remove all keys not in the collection
* of values being inserted; use @c Merge to preserve whatever is
* already in Global Storage but not mentioned in the collection.
*/
enum class InsertMode
{
Overwrite,
Merge
};
/** @brief Insert the given @p values into the *localeConf* map in @p gs
*
* @param gs The Global Storage to write to
* @param values The collection of keys and values to write to @p gs
* @param mode Indicates whether the *localeConf* key is cleared first
*
* The keys in the collection @p values should be first-level keys
* in *localeConf*, e.g. "LANG" or "LC_TIME". No effort is made to
* enforce this.
*/
DLLEXPORT void insertGS( Calamares::GlobalStorage& gs, const QVariantMap& values, InsertMode mode = InsertMode::Merge );
/** @brief Insert the given @p values into the *localeConf* map in @p gs
*
* Alternate way of providing the keys and values.
*/
DLLEXPORT void
insertGS( Calamares::GlobalStorage& gs, const QMap< QString, QString >& values, InsertMode mode = InsertMode::Merge );
/** @brief Write a single @p key and @p value to the *localeConf* map
*/
DLLEXPORT void insertGS( Calamares::GlobalStorage& gs, const QString& key, const QString& value );
/** @brief Remove a single @p key from the *localeConf* map
*/
DLLEXPORT void removeGS( Calamares::GlobalStorage& gs, const QString& key );
/** @brief Remove the *localeConf* map from Global Storage
*/
DLLEXPORT void clearGS( Calamares::GlobalStorage& gs );
/** @brief Gets a value from the *localeConf* map in @p gs
*
* If the key is not set (or doesn't exist), returns QString().
*/
DLLEXPORT QString readGS( Calamares::GlobalStorage& gs, const QString& key );
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,98 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "Lookup.h"
#include "CountryData_p.cpp"
namespace Calamares
{
namespace Locale
{
struct TwoChar
{
TwoChar( const QString& code )
: cc1( 0 )
, cc2( 0 )
{
if ( code.length() == 2 )
{
cc1 = code[ 0 ].toLatin1();
cc2 = code[ 1 ].toLatin1();
}
}
char cc1;
char cc2;
};
static const CountryData*
lookup( TwoChar c )
{
if ( !c.cc1 )
{
return nullptr;
}
const CountryData* p
= std::find_if( country_data_table,
country_data_table + country_data_size,
[ c = c ]( const CountryData& d ) { return ( d.cc1 == c.cc1 ) && ( d.cc2 == c.cc2 ); } );
if ( p == country_data_table + country_data_size )
{
return nullptr;
}
return p;
}
QLocale::Country
countryForCode( const QString& code )
{
const CountryData* p = lookup( TwoChar( code ) );
return p ? p->c : QLocale::Country::AnyCountry;
}
QLocale::Language
languageForCountry( const QString& code )
{
const CountryData* p = lookup( TwoChar( code ) );
return p ? p->l : QLocale::Language::AnyLanguage;
}
QPair< QLocale::Country, QLocale::Language >
countryData( const QString& code )
{
const CountryData* p = lookup( TwoChar( code ) );
return p ? qMakePair( p->c, p->l ) : qMakePair( QLocale::Country::AnyCountry, QLocale::Language::AnyLanguage );
}
QLocale
countryLocale( const QString& code )
{
auto p = countryData( code );
return QLocale( p.second, p.first );
}
QLocale::Language
languageForCountry( QLocale::Country country )
{
const CountryData* p = std::find_if( country_data_table,
country_data_table + country_data_size,
[ c = country ]( const CountryData& d ) { return d.c == c; } );
if ( p == country_data_table + country_data_size )
{
return QLocale::Language::AnyLanguage;
}
return p->l;
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,47 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef LOCALE_LOOKUP_H
#define LOCALE_LOOKUP_H
#include "DllMacro.h"
#include <QLocale>
#include <QPair>
namespace Calamares
{
namespace Locale
{
/* All the functions in this file do lookups of locale data
* based on CLDR tables; these are lookups that you can't (easily)
* do with just QLocale (e.g. from 2-letter country code to a likely
* locale).
*/
/// @brief Map a 2-letter code to a Country, or AnyCountry if not found
DLLEXPORT QLocale::Country countryForCode( const QString& code );
/** @brief Map a Country to a Language, or AnyLanguage if not found
*
* This is a *likely* language for the given country, based on the
* CLDR tables. For instance, this maps Belgium to Dutch.
*/
DLLEXPORT QLocale::Language languageForCountry( QLocale::Country country );
/// @brief Map a 2-letter code to a Language, or AnyLanguage if not found
DLLEXPORT QLocale::Language languageForCountry( const QString& code );
/// @brief Get both Country and Language for a 2-letter code
DLLEXPORT QPair< QLocale::Country, QLocale::Language > countryData( const QString& code );
/// @brief Get a likely locale for a 2-letter country code
DLLEXPORT QLocale countryLocale( const QString& code );
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,553 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "locale/Global.h"
#include "locale/TimeZone.h"
#include "locale/TranslatableConfiguration.h"
#include "locale/TranslationsModel.h"
#include "CalamaresVersion.h"
#include "GlobalStorage.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
#include <QtTest/QtTest>
class LocaleTests : public QObject
{
Q_OBJECT
public:
LocaleTests();
~LocaleTests() override;
private Q_SLOTS:
void initTestCase();
void testLanguageModelCount();
void testTranslatableLanguages();
void testTranslatableConfig1();
void testTranslatableConfig2();
void testTranslatableConfigContext();
void testLanguageScripts();
void testEsperanto();
void testInterlingue();
// TimeZone testing
void testRegions();
void testSimpleZones();
void testComplexZones();
void testTZLookup();
void testTZIterator();
void testLocationLookup_data();
void testLocationLookup();
void testLocationLookup2();
// Global Storage updates
void testGSUpdates();
};
LocaleTests::LocaleTests() {}
LocaleTests::~LocaleTests() {}
void
LocaleTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
// Otherwise plain get() is dubious in the TranslatableConfiguration tests
QLocale::setDefault( QLocale( QStringLiteral( "en_US" ) ) );
QVERIFY( ( QLocale().name() == "C" ) || ( QLocale().name() == "en_US" ) );
}
void
LocaleTests::testLanguageModelCount()
{
const auto* m = Calamares::Locale::availableTranslations();
QVERIFY( m );
QVERIFY( m->rowCount( QModelIndex() ) > 1 );
int dutch = m->find( QLocale( "nl_NL" ) );
QVERIFY( dutch > 0 );
QCOMPARE( m->find( "NL" ), dutch );
// must be capitals
QCOMPARE( m->find( "nl" ), -1 );
QCOMPARE( m->find( QLocale( "nl" ) ), dutch );
// Belgium speaks Dutch as well
QCOMPARE( m->find( "BE" ), dutch );
}
void
LocaleTests::testLanguageScripts()
{
const auto* m = Calamares::Locale::availableTranslations();
QVERIFY( m );
// Cursory test that all the locales found have a sensible language,
// and that some specific languages have sensible corresponding data.
//
// This fails on Esperanto (or, if Esperanto is added to Qt, then
// this will pass and the test after the loop will fail.
for ( int i = 0; i < m->rowCount( QModelIndex() ); ++i )
{
const auto& label = m->locale( i );
const auto locale = label.locale();
cDebug() << label.label() << locale;
QVERIFY( locale.language() == QLocale::Greek ? locale.script() == QLocale::GreekScript : true );
QVERIFY( locale.language() == QLocale::Korean ? locale.script() == QLocale::KoreanScript : true );
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
QVERIFY( locale.language() == QLocale::Lithuanian ? locale.country() == QLocale::Lithuania : true );
#else
QVERIFY( locale.language() == QLocale::Lithuanian ? locale.territory() == QLocale::Lithuania : true );
#endif
QVERIFY( locale.language() != QLocale::C );
}
}
void
LocaleTests::testEsperanto()
{
QCOMPARE( QLocale( "eo" ).language(), QLocale::Esperanto );
QCOMPARE( QLocale( QLocale::Esperanto ).language(), QLocale::Esperanto ); // Probably fails on 5.12, too
}
void
LocaleTests::testInterlingue()
{
#if CALAMARES_QT_SUPPORT_INTERLINGUE
// ie was fixed in version ... of Qt
QCOMPARE( QLocale( "ie" ).language(), QLocale::Interlingue );
QCOMPARE( QLocale( QLocale::Interlingue ).language(), QLocale::Interlingue );
#else
// ie / Interlingue is borked (is "ie" even the right name?)
QCOMPARE( QLocale( "ie" ).language(), QLocale::C );
QCOMPARE( QLocale( QLocale::Interlingue ).language(), QLocale::English );
#endif
// "ia" exists (post-war variant of Interlingue)
QCOMPARE( QLocale( "ia" ).language(), QLocale::Interlingua );
// "bork" does not exist
QCOMPARE( QLocale( "bork" ).language(), QLocale::C );
}
static const QStringList&
someLanguages()
{
static QStringList languages { "nl", "de", "da", "nb", "sr@latin", "ar", "ru" };
return languages;
}
/** @brief Check consistency of test data
* Check that all the languages used in testing, are actually enabled
* in Calamares translations.
*/
void
LocaleTests::testTranslatableLanguages()
{
cDebug() << "Translation languages:" << Calamares::Locale::availableLanguages();
for ( const auto& language : someLanguages() )
{
// Could be QVERIFY, but then we don't see what language code fails
QCOMPARE( Calamares::Locale::availableLanguages().contains( language ) ? language : QString(), language );
}
}
/** @brief Test strings with no translations
*/
void
LocaleTests::testTranslatableConfig1()
{
Calamares::Locale::TranslatedString ts0;
QVERIFY( ts0.isEmpty() );
QCOMPARE( ts0.count(), 1 ); // the empty string
Calamares::Locale::TranslatedString ts1( "Hello" );
QCOMPARE( ts1.count(), 1 );
QVERIFY( !ts1.isEmpty() );
QCOMPARE( ts1.get(), QStringLiteral( "Hello" ) );
QCOMPARE( ts1.get( QLocale( "nl" ) ), QStringLiteral( "Hello" ) );
QVariantMap map;
map.insert( "description", "description (no language)" );
Calamares::Locale::TranslatedString ts2( map, "description" );
QCOMPARE( ts2.count(), 1 );
QVERIFY( !ts2.isEmpty() );
QCOMPARE( ts2.get(), QStringLiteral( "description (no language)" ) );
QCOMPARE( ts2.get( QLocale( "nl" ) ), QStringLiteral( "description (no language)" ) );
}
/** @bref Test strings with translations.
*/
void
LocaleTests::testTranslatableConfig2()
{
QVariantMap map;
for ( const auto& language : someLanguages() )
{
map.insert( QString( "description[%1]" ).arg( language ),
QString( "description (language %1)" ).arg( language ) );
if ( language != "nl" )
{
map.insert( QString( "name[%1]" ).arg( language ), QString( "name (language %1)" ).arg( language ) );
}
}
// If there's no untranslated string in the map, it is considered empty
Calamares::Locale::TranslatedString ts0( map, "description" );
QVERIFY( ts0.isEmpty() ); // Because no untranslated string
QCOMPARE( ts0.count(),
someLanguages().count() + 1 ); // But there are entries for the translations, plus an empty string
// expand the map with untranslated entries
map.insert( QString( "description" ), "description (no language)" );
map.insert( QString( "name" ), "name (no language)" );
Calamares::Locale::TranslatedString ts1( map, "description" );
// The +1 is because "" is always also inserted
QCOMPARE( ts1.count(), someLanguages().count() + 1 );
QVERIFY( !ts1.isEmpty() );
QCOMPARE( ts1.get(), QStringLiteral( "description (no language)" ) ); // it wasn't set
QCOMPARE( ts1.get( QLocale( "nl" ) ), QStringLiteral( "description (language nl)" ) );
for ( const auto& language : someLanguages() )
{
// Skip Serbian (latin) because QLocale() constructed with it
// doesn't retain the @latin part.
if ( language == "sr@latin" )
{
continue;
}
// Could be QVERIFY, but then we don't see what language code fails
QCOMPARE( ts1.get( QLocale( language ) ) == QString( "description (language %1)" ).arg( language ) ? language
: QString(),
language );
}
QCOMPARE( ts1.get( QLocale( QLocale::Language::Serbian, QLocale::Script::LatinScript, QLocale::Country::Serbia ) ),
QStringLiteral( "description (language sr@latin)" ) );
Calamares::Locale::TranslatedString ts2( map, "name" );
// We skipped dutch this time
QCOMPARE( ts2.count(), someLanguages().count() );
QVERIFY( !ts2.isEmpty() );
// This key doesn't exist
Calamares::Locale::TranslatedString ts3( map, "front" );
QVERIFY( ts3.isEmpty() );
QCOMPARE( ts3.count(), 1 ); // The empty string
}
void
LocaleTests::testTranslatableConfigContext()
{
using TS = Calamares::Locale::TranslatedString;
const QString original( "Quit" );
TS quitUntranslated( original );
TS quitTranslated( original, metaObject()->className() );
QCOMPARE( quitUntranslated.get(), original );
QCOMPARE( quitTranslated.get(), original );
// Load translation data from QRC
QVERIFY( QFile::exists( ":/lang/localetest_nl.qm" ) );
QTranslator t;
QVERIFY( t.load( QString( ":/lang/localetest_nl" ) ) );
QCoreApplication::installTranslator( &t );
// Translation doesn't affect the one without context
QCOMPARE( quitUntranslated.get(), original );
// But the translation **does** affect this class' context
QCOMPARE( quitTranslated.get(), QStringLiteral( "Ophouden" ) );
QCOMPARE( tr( "Quit" ), QStringLiteral( "Ophouden" ) );
}
void
LocaleTests::testRegions()
{
using namespace Calamares::Locale;
RegionsModel regions;
QVERIFY( regions.rowCount( QModelIndex() ) > 3 ); // Africa, America, Asia
QStringList names;
for ( int i = 0; i < regions.rowCount( QModelIndex() ); ++i )
{
QVariant name = regions.data( regions.index( i ), RegionsModel::NameRole );
QVERIFY( name.isValid() );
QVERIFY( !name.toString().isEmpty() );
names.append( name.toString() );
}
QVERIFY( names.contains( "America" ) );
QVERIFY( !names.contains( "UTC" ) );
}
static void
displayedNames( QAbstractItemModel& model, QStringList& names )
{
names.clear();
for ( int i = 0; i < model.rowCount( QModelIndex() ); ++i )
{
QVariant name = model.data( model.index( i, 0 ), Qt::DisplayRole );
QVERIFY( name.isValid() );
QVERIFY( !name.toString().isEmpty() );
names.append( name.toString() );
}
}
void
LocaleTests::testSimpleZones()
{
using namespace Calamares::Locale;
ZonesModel zones;
QVERIFY( zones.rowCount( QModelIndex() ) > 24 );
QStringList names;
displayedNames( zones, names );
QVERIFY( names.contains( "Amsterdam" ) );
if ( !names.contains( "New York" ) )
{
for ( const auto& s : names )
{
if ( s.startsWith( 'N' ) )
{
cDebug() << s;
}
}
}
QVERIFY( names.contains( "New York" ) );
QVERIFY( !names.contains( "America" ) );
QVERIFY( !names.contains( "New_York" ) );
}
void
LocaleTests::testComplexZones()
{
using namespace Calamares::Locale;
ZonesModel zones;
RegionalZonesModel europe( &zones );
QStringList names;
displayedNames( zones, names );
QVERIFY( names.contains( "New York" ) );
QVERIFY( names.contains( "Prague" ) );
QVERIFY( names.contains( "Abidjan" ) );
// No region set
displayedNames( europe, names );
QVERIFY( names.contains( "New York" ) );
QVERIFY( names.contains( "Prague" ) );
QVERIFY( names.contains( "Abidjan" ) );
// Now filter
europe.setRegion( "Europe" );
displayedNames( europe, names );
QVERIFY( !names.contains( "New York" ) );
QVERIFY( names.contains( "Prague" ) );
QVERIFY( !names.contains( "Abidjan" ) );
europe.setRegion( "America" );
displayedNames( europe, names );
QVERIFY( names.contains( "New York" ) );
QVERIFY( !names.contains( "Prague" ) );
QVERIFY( !names.contains( "Abidjan" ) );
europe.setRegion( "Africa" );
displayedNames( europe, names );
QVERIFY( !names.contains( "New York" ) );
QVERIFY( !names.contains( "Prague" ) );
QVERIFY( names.contains( "Abidjan" ) );
}
void
LocaleTests::testTZLookup()
{
using namespace Calamares::Locale;
ZonesModel zones;
QVERIFY( zones.find( "America", "New_York" ) );
QCOMPARE( zones.find( "America", "New_York" )->zone(), QStringLiteral( "New_York" ) );
QCOMPARE( zones.find( "America", "New_York" )->translated(), QStringLiteral( "New York" ) );
QVERIFY( !zones.find( "Europe", "New_York" ) );
QVERIFY( !zones.find( "America", "New York" ) );
}
void
LocaleTests::testTZIterator()
{
using namespace Calamares::Locale;
const ZonesModel zones;
QVERIFY( zones.find( "Europe", "Rome" ) );
int count = 0;
bool seenRome = false;
bool seenGnome = false;
for ( auto it = zones.begin(); it; ++it )
{
QVERIFY( *it );
QVERIFY( !( *it )->zone().isEmpty() );
seenRome |= ( *it )->zone() == QStringLiteral( "Rome" );
seenGnome |= ( *it )->zone() == QStringLiteral( "Gnome" );
count++;
}
QVERIFY( seenRome );
QVERIFY( !seenGnome );
QCOMPARE( count, zones.rowCount( QModelIndex() ) );
QCOMPARE( zones.data( zones.index( 0 ), ZonesModel::RegionRole ).toString(), QStringLiteral( "Africa" ) );
QCOMPARE( ( *zones.begin() )->zone(), QStringLiteral( "Abidjan" ) );
}
void
LocaleTests::testLocationLookup_data()
{
QTest::addColumn< double >( "latitude" );
QTest::addColumn< double >( "longitude" );
QTest::addColumn< QString >( "name" );
QTest::newRow( "London" ) << 50.0 << 0.0 << QString( "London" );
QTest::newRow( "Tarawa E" ) << 0.0 << 179.0 << QString( "Tarawa" );
QTest::newRow( "Tarawa W" ) << 0.0 << -179.0 << QString( "Tarawa" );
QTest::newRow( "Johannesburg" ) << -26.0 << 28.0 << QString( "Johannesburg" ); // South Africa
QTest::newRow( "Maseru" ) << -29.0 << 27.0 << QString( "Maseru" ); // Lesotho
QTest::newRow( "Windhoek" ) << -22.0 << 17.0 << QString( "Windhoek" ); // Namibia
QTest::newRow( "Port Elisabeth" ) << -33.0 << 25.0 << QString( "Johannesburg" ); // South Africa
QTest::newRow( "Cape Town" ) << -33.0 << 18.0 << QString( "Johannesburg" ); // South Africa
}
void
LocaleTests::testLocationLookup()
{
const Calamares::Locale::ZonesModel zones;
QFETCH( double, latitude );
QFETCH( double, longitude );
QFETCH( QString, name );
const auto* zone = zones.find( latitude, longitude );
QVERIFY( zone );
QCOMPARE( zone->zone(), name );
}
void
LocaleTests::testLocationLookup2()
{
// Official
// ZA -2615+02800 Africa/Johannesburg
// Spot patch
// "ZA -3230+02259 Africa/Johannesburg\n";
const Calamares::Locale::ZonesModel zones;
const auto* zone = zones.find( -26.15, 28.00 );
QCOMPARE( zone->zone(), QString( "Johannesburg" ) );
// The TZ data sources use minutes-and-seconds notation,
// so "2615" is 26 degrees, 15 minutes, and 15 minutes is
// one-quarter of a degree.
QCOMPARE( zone->latitude(), -26.25 );
QCOMPARE( zone->longitude(), 28.00 );
// Elsewhere in South Africa
const auto* altzone = zones.find( -32.0, 22.0 );
QCOMPARE( altzone, zone ); // same pointer
QCOMPARE( altzone->zone(), QString( "Johannesburg" ) );
QCOMPARE( altzone->latitude(), -26.25 );
QCOMPARE( altzone->longitude(), 28.00 );
altzone = zones.find( -29.0, 27.0 );
QCOMPARE( altzone->zone(), QString( "Maseru" ) );
// -2928, that's -29 and 28/60 of a degree, is almost half, but we don't want
// to fall foul of variations in double-precision
QCOMPARE( trunc( altzone->latitude() * 1000.0 ), -29466 );
}
void
LocaleTests::testGSUpdates()
{
Calamares::GlobalStorage gs;
const QString gsKey( "localeConf" );
QCOMPARE( gs.value( gsKey ), QVariant() );
// Insert one
{
Calamares::Locale::insertGS( gs, "LANG", "en_US" );
auto map = gs.value( gsKey ).toMap();
QCOMPARE( map.count(), 1 );
QCOMPARE( map.value( "LANG" ).toString(), QString( "en_US" ) );
}
// Overwrite one
{
Calamares::Locale::insertGS( gs, "LANG", "nl_BE" );
auto map = gs.value( gsKey ).toMap();
QCOMPARE( map.count(), 1 );
QCOMPARE( map.value( "LANG" ).toString(), QString( "nl_BE" ) );
}
// Insert a second value
{
Calamares::Locale::insertGS( gs, "LC_TIME", "UTC" );
auto map = gs.value( gsKey ).toMap();
QCOMPARE( map.count(), 2 );
QCOMPARE( map.value( "LANG" ).toString(), QString( "nl_BE" ) );
QCOMPARE( map.value( "LC_TIME" ).toString(), QString( "UTC" ) );
}
// Overwrite parts
{
QMap< QString, QString > kv;
kv.insert( "LANG", "en_SU" );
kv.insert( "LC_CURRENCY", "rbl" );
// Overwrite one, add one
Calamares::Locale::insertGS( gs, kv, Calamares::Locale::InsertMode::Merge );
auto map = gs.value( gsKey ).toMap();
QCOMPARE( map.count(), 3 );
QCOMPARE( map.value( "LANG" ).toString(), QString( "en_SU" ) );
QCOMPARE( map.value( "LC_TIME" ).toString(), QString( "UTC" ) ); // unchanged
QCOMPARE( map.value( "LC_CURRENCY" ).toString(), QString( "rbl" ) );
}
// Overwrite with clear
{
QMap< QString, QString > kv;
kv.insert( "LANG", "en_US" );
kv.insert( "LC_CURRENCY", "peso" );
// Overwrite one, add one
Calamares::Locale::insertGS( gs, kv, Calamares::Locale::InsertMode::Overwrite );
auto map = gs.value( gsKey ).toMap();
QCOMPARE( map.count(), 2 ); // the rest were cleared
QCOMPARE( map.value( "LANG" ).toString(), QString( "en_US" ) );
QVERIFY( !map.contains( "LC_TIME" ) );
QCOMPARE( map.value( "LC_TIME" ).toString(), QString() ); // removed
QCOMPARE( map.value( "LC_CURRENCY" ).toString(), QString( "peso" ) );
}
}
QTEST_GUILESS_MAIN( LocaleTests )
#include "utils/moc-warnings.h"
#include "Tests.moc"

View File

@@ -0,0 +1,501 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "TimeZone.h"
#include "locale/TranslatableString.h"
#include "utils/Logger.h"
#include "utils/String.h"
#include <QFile>
#include <QRegularExpression>
#include <QString>
static const char TZ_DATA_FILE[] = "/usr/share/zoneinfo/zone.tab";
namespace Calamares
{
namespace Locale
{
class RegionData;
using RegionVector = QVector< RegionData* >;
using ZoneVector = QVector< TimeZoneData* >;
/** @brief Turns a string longitude or latitude notation into a double
*
* This handles strings like "+4230+00131" from zone.tab,
* which is degrees-and-minutes notation, and + means north or east.
*/
static double
getRightGeoLocation( QString str )
{
double sign = 1, num = 0.00;
// Determine sign
if ( str.startsWith( '-' ) )
{
sign = -1;
str.remove( 0, 1 );
}
else if ( str.startsWith( '+' ) )
{
str.remove( 0, 1 );
}
if ( str.length() == 4 || str.length() == 6 )
{
num = str.mid( 0, 2 ).toDouble() + str.mid( 2, 2 ).toDouble() / 60.0;
}
else if ( str.length() == 5 || str.length() == 7 )
{
num = str.mid( 0, 3 ).toDouble() + str.mid( 3, 2 ).toDouble() / 60.0;
}
return sign * num;
}
TimeZoneData::TimeZoneData( const QString& region,
const QString& zone,
const QString& country,
double latitude,
double longitude )
: TranslatableString( zone )
, m_region( region )
, m_country( country )
, m_latitude( latitude )
, m_longitude( longitude )
{
setObjectName( region + '/' + zone );
}
QString
TimeZoneData::translated() const
{
// NOTE: context name must match what's used in zone-extractor.py
return QObject::tr( m_human, "tz_names" );
}
class RegionData : public TranslatableString
{
public:
using TranslatableString::TranslatableString;
QString translated() const override;
};
QString
RegionData::translated() const
{
// NOTE: context name must match what's used in zone-extractor.py
return QObject::tr( m_human, "tz_regions" );
}
static void
loadTZData( RegionVector& regions, ZoneVector& zones, QTextStream& in )
{
while ( !in.atEnd() )
{
QString line = in.readLine().trimmed().split( '#', SplitKeepEmptyParts ).first().trimmed();
if ( line.isEmpty() )
{
continue;
}
QStringList list = line.split( QRegularExpression( "[\t ]" ), SplitSkipEmptyParts );
if ( list.size() < 3 )
{
continue;
}
QStringList timezoneParts = list.at( 2 ).split( '/', SplitSkipEmptyParts );
if ( timezoneParts.size() < 2 )
{
continue;
}
QString region = timezoneParts.first().trimmed();
if ( region.isEmpty() )
{
continue;
}
QString countryCode = list.at( 0 ).trimmed();
if ( countryCode.size() != 2 )
{
continue;
}
timezoneParts.removeFirst();
QString zone = timezoneParts.join( '/' );
if ( zone.length() < 2 )
{
continue;
}
QString position = list.at( 1 );
int cooSplitPos = position.indexOf( QRegularExpression( "[-+]" ), 1 );
double latitude;
double longitude;
if ( cooSplitPos > 0 )
{
latitude = getRightGeoLocation( position.mid( 0, cooSplitPos ) );
longitude = getRightGeoLocation( position.mid( cooSplitPos ) );
}
else
{
continue;
}
// Now we have region, zone, country, lat and longitude
const RegionData* existingRegion = nullptr;
for ( const auto* p : regions )
{
if ( p->key() == region )
{
existingRegion = p;
break;
}
}
if ( !existingRegion )
{
regions.append( new RegionData( region ) );
}
zones.append( new TimeZoneData( region, zone, countryCode, latitude, longitude ) );
}
}
/** @brief Extra, fake, timezones
*
* The timezone locations in zone.tab are not always very useful,
* given Calamares's standard "nearest zone" algorithm: for instance,
* in most locations physically in the country of South Africa,
* Maseru (the capital of Lesotho, and location for timezone Africa/Maseru)
* is closer than Johannesburg (the location for timezone Africa/Johannesburg).
*
* The algorithm picks the wrong place. This is for instance annoying
* when clicking on Cape Town, you get Maseru, and to get Johannesburg
* you need to click somewhere very carefully north of Maserru.
*
* These alternate zones are used to introduce "extra locations"
* into the timezone database, in order to influence the closest-location
* algorithm. Lines are formatted just like in zone.tab: remember the \n
*/
static const char altZones[] =
/* This extra zone is north-east of Karoo National park,
* and means that Western Cape province and a good chunk of
* Northern- and Eastern- Cape provinces get pulled in to Johannesburg.
* Bloemfontein is still closer to Maseru than either correct zone,
* but this is a definite improvement.
*/
"ZA -3230+02259 Africa/Johannesburg\n";
class Private : public QObject
{
Q_OBJECT
public:
RegionVector m_regions;
ZoneVector m_zones; ///< The official timezones and locations
ZoneVector m_altZones; ///< Extra locations for zones
Private()
{
m_regions.reserve( 12 ); // reasonable guess
m_zones.reserve( 452 ); // wc -l /usr/share/zoneinfo/zone.tab
// Load the official timezones
{
QFile file( TZ_DATA_FILE );
if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
QTextStream in( &file );
loadTZData( m_regions, m_zones, in );
}
}
// Load the alternate zones (see documentation at altZones)
{
QTextStream in( altZones );
loadTZData( m_regions, m_altZones, in );
}
std::sort( m_regions.begin(),
m_regions.end(),
[]( const RegionData* lhs, const RegionData* rhs ) { return lhs->key() < rhs->key(); } );
std::sort( m_zones.begin(),
m_zones.end(),
[]( const TimeZoneData* lhs, const TimeZoneData* rhs )
{
if ( lhs->region() == rhs->region() )
{
return lhs->zone() < rhs->zone();
}
return lhs->region() < rhs->region();
} );
for ( auto* z : m_zones )
{
z->setParent( this );
}
}
};
static Private*
privateInstance()
{
static Private* s_p = new Private;
return s_p;
}
RegionsModel::RegionsModel( QObject* parent )
: QAbstractListModel( parent )
, m_private( privateInstance() )
{
}
RegionsModel::~RegionsModel() {}
int
RegionsModel::rowCount( const QModelIndex& ) const
{
return m_private->m_regions.count();
}
QVariant
RegionsModel::data( const QModelIndex& index, int role ) const
{
if ( !index.isValid() || index.row() < 0 || index.row() >= m_private->m_regions.count() )
{
return QVariant();
}
const auto& region = m_private->m_regions[ index.row() ];
if ( role == NameRole )
{
return region->translated();
}
if ( role == KeyRole )
{
return region->key();
}
return QVariant();
}
QHash< int, QByteArray >
RegionsModel::roleNames() const
{
return { { NameRole, "name" }, { KeyRole, "key" } };
}
QString
RegionsModel::translated( const QString& region ) const
{
for ( const auto* p : m_private->m_regions )
{
if ( p->key() == region )
{
return p->translated();
}
}
return region;
}
ZonesModel::ZonesModel( QObject* parent )
: QAbstractListModel( parent )
, m_private( privateInstance() )
{
}
ZonesModel::~ZonesModel() {}
int
ZonesModel::rowCount( const QModelIndex& ) const
{
return m_private->m_zones.count();
}
QVariant
ZonesModel::data( const QModelIndex& index, int role ) const
{
if ( !index.isValid() || index.row() < 0 || index.row() >= m_private->m_zones.count() )
{
return QVariant();
}
const auto* zone = m_private->m_zones[ index.row() ];
switch ( role )
{
case NameRole:
return zone->translated();
case KeyRole:
return zone->key();
case RegionRole:
return zone->region();
default:
return QVariant();
}
}
QHash< int, QByteArray >
ZonesModel::roleNames() const
{
return { { NameRole, "name" }, { KeyRole, "key" } };
}
const TimeZoneData*
ZonesModel::find( const QString& region, const QString& zone ) const
{
for ( const auto* p : m_private->m_zones )
{
if ( p->region() == region && p->zone() == zone )
{
return p;
}
}
return nullptr;
}
STATICTEST const TimeZoneData*
find( double startingDistance,
const ZoneVector& zones,
const std::function< double( const TimeZoneData* ) >& distanceFunc )
{
double smallestDistance = startingDistance;
const TimeZoneData* closest = nullptr;
for ( const auto* zone : zones )
{
double thisDistance = distanceFunc( zone );
if ( thisDistance < smallestDistance )
{
closest = zone;
smallestDistance = thisDistance;
}
}
return closest;
}
const TimeZoneData*
ZonesModel::find( const std::function< double( const TimeZoneData* ) >& distanceFunc ) const
{
const auto* officialZone = Calamares::Locale::find( 1000000.0, m_private->m_zones, distanceFunc );
const auto* altZone = Calamares::Locale::find( distanceFunc( officialZone ), m_private->m_altZones, distanceFunc );
// If nothing was closer than the official zone already was, altZone is
// nullptr; but if there is a spot-patch, then we need to re-find
// the zone by name, since we want to always return pointers into
// m_zones, not into the alternative spots.
return altZone ? find( altZone->region(), altZone->zone() ) : officialZone;
}
const TimeZoneData*
ZonesModel::find( double latitude, double longitude ) const
{
/* This is a somewhat derpy way of finding "closest",
* in that it considers one degree of separation
* either N/S or E/W equal to any other; this obviously
* falls apart at the poles.
*/
auto distance = [ & ]( const TimeZoneData* zone ) -> double
{
// Latitude doesn't wrap around: there is nothing north of 90
double latitudeDifference = abs( zone->latitude() - latitude );
// Longitude **does** wrap around, so consider the case of -178 and 178
// which differ by 4 degrees.
double westerly = qMin( zone->longitude(), longitude );
double easterly = qMax( zone->longitude(), longitude );
double longitudeDifference = 0.0;
if ( westerly < 0.0 && !( easterly < 0.0 ) )
{
// Only if they're different signs can we have wrap-around.
longitudeDifference = qMin( abs( westerly - easterly ), abs( 360.0 + westerly - easterly ) );
}
else
{
longitudeDifference = abs( westerly - easterly );
}
return latitudeDifference + longitudeDifference;
};
return find( distance );
}
QObject*
ZonesModel::lookup( double latitude, double longitude ) const
{
const auto* p = find( latitude, longitude );
if ( !p )
{
p = find( "America", "New_York" );
}
if ( !p )
{
cWarning() << "No zone (not even New York) found, expect crashes.";
}
return const_cast< QObject* >( reinterpret_cast< const QObject* >( p ) );
}
ZonesModel::Iterator::operator bool() const
{
return 0 <= m_index && m_index < m_p->m_zones.count();
}
const TimeZoneData*
ZonesModel::Iterator::operator*() const
{
if ( *this )
{
return m_p->m_zones[ m_index ];
}
return nullptr;
}
RegionalZonesModel::RegionalZonesModel( Calamares::Locale::ZonesModel* source, QObject* parent )
: QSortFilterProxyModel( parent )
, m_private( privateInstance() )
{
setSourceModel( source );
}
RegionalZonesModel::~RegionalZonesModel() {}
void
RegionalZonesModel::setRegion( const QString& r )
{
if ( r != m_region )
{
m_region = r;
invalidateFilter();
emit regionChanged( r );
}
}
bool
RegionalZonesModel::filterAcceptsRow( int sourceRow, const QModelIndex& ) const
{
if ( m_region.isEmpty() )
{
return true;
}
if ( sourceRow < 0 || sourceRow >= m_private->m_zones.count() )
{
return false;
}
const auto& zone = m_private->m_zones[ sourceRow ];
return ( zone->m_region == m_region );
}
} // namespace Locale
} // namespace Calamares
#include "utils/moc-warnings.h"
#include "TimeZone.moc"

View File

@@ -0,0 +1,237 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
/** @file Timezone data and models to go with it
*
* The TimeZoneData class holds information from zone.tab, about
* TZ names and locations (latitude and longitude) for geographic
* lookups.
*
* The RegionModel lists the regions of the world (about 12) and
* ZonesModel lists all the timezones; the RegionalZonesModel provides
* a way to restrict the view of timezones to those of a specific region.
*
*/
#ifndef LOCALE_TIMEZONE_H
#define LOCALE_TIMEZONE_H
#include "DllMacro.h"
#include "locale/TranslatableString.h"
#include <QAbstractListModel>
#include <QObject>
#include <QSortFilterProxyModel>
#include <QVariant>
namespace Calamares
{
namespace Locale
{
class Private;
class RegionalZonesModel;
class ZonesModel;
class DLLEXPORT TimeZoneData : public QObject, TranslatableString
{
friend class RegionalZonesModel;
friend class ZonesModel;
Q_OBJECT
Q_PROPERTY( QString region READ region CONSTANT )
Q_PROPERTY( QString zone READ zone CONSTANT )
Q_PROPERTY( QString name READ translated CONSTANT )
Q_PROPERTY( QString countryCode READ country CONSTANT )
public:
TimeZoneData( const QString& region,
const QString& zone,
const QString& country,
double latitude,
double longitude );
TimeZoneData( const TimeZoneData& ) = delete;
TimeZoneData( TimeZoneData&& ) = delete;
///@brief Returns a translated, human-readable form of region/zone (e.g. "America/New York")
QString translated() const override;
///@brief Returns the region key (e.g. "Europe") with no translation and no human-readable tweaks
QString region() const { return m_region; }
///@brief Returns the zone key (e.g. "New_York") with no translation and no human-readable tweaks
QString zone() const { return key(); }
QString country() const { return m_country; }
double latitude() const { return m_latitude; }
double longitude() const { return m_longitude; }
private:
QString m_region;
QString m_country;
double m_latitude;
double m_longitude;
};
/** @brief The list of timezone regions
*
* The regions are a short list of global areas (Africa, America, India ..)
* which contain zones.
*/
class DLLEXPORT RegionsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
NameRole = Qt::DisplayRole,
KeyRole = Qt::UserRole // So that currentData() will get the key
};
RegionsModel( QObject* parent = nullptr );
~RegionsModel() override;
int rowCount( const QModelIndex& parent ) const override;
QVariant data( const QModelIndex& index, int role ) const override;
QHash< int, QByteArray > roleNames() const override;
public Q_SLOTS:
/** @brief Provides a human-readable version of the region
*
* Returns @p region unchanged if there is no such region
* or no translation for the region's name.
*/
QString translated( const QString& region ) const;
private:
Private* m_private;
};
class DLLEXPORT ZonesModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
NameRole = Qt::DisplayRole,
KeyRole = Qt::UserRole, // So that currentData() will get the key
RegionRole = Qt::UserRole + 1
};
ZonesModel( QObject* parent = nullptr );
~ZonesModel() override;
int rowCount( const QModelIndex& parent ) const override;
QVariant data( const QModelIndex& index, int role ) const override;
QHash< int, QByteArray > roleNames() const override;
/** @brief Iterator for the underlying list of zones
*
* Iterates over all the zones in the model. Operator * may return
* a @c nullptr when the iterator is not valid. Typical usage:
*
* ```
* for( auto it = model.begin(); it; ++it )
* {
* const auto* zonedata = *it;
* ...
* }
*/
class Iterator
{
friend class ZonesModel;
Iterator( const Private* m )
: m_index( 0 )
, m_p( m )
{
}
public:
operator bool() const;
void operator++() { ++m_index; }
const TimeZoneData* operator*() const;
int index() const { return m_index; }
private:
int m_index;
const Private* m_p;
};
Iterator begin() const { return Iterator( m_private ); }
/** @brief Look up TZ data based on an arbitrary distance function
*
* This is a generic method that can define distance in whatever
* coordinate system is wanted; returns the zone with the smallest
* distance. The @p distanceFunc must return "the distance" for
* each zone. It would be polite to return something non-negative.
*
* Note: not a slot, because the parameter isn't moc-able.
*/
const TimeZoneData* find( const std::function< double( const TimeZoneData* ) >& distanceFunc ) const;
public Q_SLOTS:
/** @brief Look up TZ data based on its name.
*
* Returns @c nullptr if not found.
*/
const TimeZoneData* find( const QString& region, const QString& zone ) const;
/** @brief Look up TZ data based on the location.
*
* Returns the nearest zone to the given lat and lon. This is a
* convenience function for calling find(), below, with a standard
* distance function based on the distance between the given
* location (lat and lon) and each zone's given location.
*/
const TimeZoneData* find( double latitude, double longitude ) const;
/** @brief Look up TZ data based on the location.
*
* Returns the nearest zone, or New York. This is non-const for QML
* purposes, but the object should be considered const anyway.
*/
QObject* lookup( double latitude, double longitude ) const;
private:
Private* m_private;
};
class DLLEXPORT RegionalZonesModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY( QString region READ region WRITE setRegion NOTIFY regionChanged )
public:
RegionalZonesModel( ZonesModel* source, QObject* parent = nullptr );
~RegionalZonesModel() override;
bool filterAcceptsRow( int sourceRow, const QModelIndex& sourceParent ) const override;
QString region() const { return m_region; }
public Q_SLOTS:
void setRegion( const QString& r );
signals:
void regionChanged( const QString& );
private:
Private* m_private;
QString m_region;
};
} // namespace Locale
} // namespace Calamares
#endif // LOCALE_TIMEZONE_H

View File

@@ -0,0 +1,118 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "TranslatableConfiguration.h"
#include "TranslationsModel.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <QCoreApplication>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
namespace Calamares
{
namespace Locale
{
TranslatedString::TranslatedString( const QString& key, const char* context )
: m_context( context )
{
m_strings[ QString() ] = key;
}
TranslatedString::TranslatedString( const QString& string )
: TranslatedString( string, nullptr )
{
}
TranslatedString::TranslatedString( const QVariantMap& map, const QString& key, const char* context )
: m_context( context )
{
// Get the un-decorated value for the key
QString value = Calamares::getString( map, key );
m_strings[ QString() ] = value;
for ( auto it = map.constBegin(); it != map.constEnd(); ++it )
{
QString subkey = it.key();
if ( subkey == key )
{
// Already obtained, above
}
else if ( subkey.startsWith( key ) )
{
QRegularExpressionMatch match;
if ( subkey.indexOf( QRegularExpression( "\\[([a-zA-Z_@]*)\\]" ), 0, &match ) > 0 )
{
QString language = match.captured( 1 );
m_strings[ language ] = it.value().toString();
}
}
}
}
QString
TranslatedString::get() const
{
return get( QLocale() );
}
QString
TranslatedString::get( const QLocale& locale ) const
{
// TODO: keep track of special cases like sr@latin and ca@valencia
QString localeName = locale.name();
// Special case, sr@latin doesn't have the @latin reflected in the name
if ( locale.language() == QLocale::Language::Serbian && locale.script() == QLocale::Script::LatinScript )
{
localeName = QStringLiteral( "sr@latin" );
}
if ( m_strings.contains( localeName ) )
{
return m_strings[ localeName ];
}
int index = localeName.indexOf( '@' );
if ( index > 0 )
{
localeName.truncate( index );
if ( m_strings.contains( localeName ) )
{
return m_strings[ localeName ];
}
}
index = localeName.indexOf( '_' );
if ( index > 0 )
{
localeName.truncate( index );
if ( m_strings.contains( localeName ) )
{
return m_strings[ localeName ];
}
}
// If we're given a context to work with, also try the same string in
// the regular translation framework.
const QString& s = m_strings[ QString() ];
if ( m_context )
{
return QCoreApplication::translate( m_context, s.toLatin1().constData() );
}
else
{
return s;
}
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,104 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
/** @file Run-time translation of strings from configuration files
*
* The TranslatedString class provides a way of doing run-time
* lookups of human-readable strings, from data provided in
* the configuration files (*.conf) for Calamares. This acts
* like "normal" translation through tr() calls, as far as the
* user-visible part goes.
*/
#ifndef LOCALE_TRANSLATABLECONFIGURATION_H
#define LOCALE_TRANSLATABLECONFIGURATION_H
#include "DllMacro.h"
#include <QLocale>
#include <QMap>
#include <QVariant>
namespace Calamares
{
namespace Locale
{
/** @brief A human-readable string from a configuration file
*
* The configuration files can contain human-readable strings,
* but those need their own translations and are not supported
* by QObject::tr or anything else.
*/
class DLLEXPORT TranslatedString
{
public:
/** @brief Get all the translations connected to @p key
*
* Gets map[key] as the "untranslated" form, and then all the
* keys of the form <key>[lang] are taken as the translation
* for <lang> of the untranslated form.
*
* If @p context is not a nullptr, then that is taken as an
* indication to **also** use the regular QObject::tr() translation
* mechanism for these strings. It is recommended to pass in
* metaObject()->className() as context (from a QObject based class)
* to give the TranslatedString the same context as other calls
* to tr() within that class.
*
* The @p context, if any, should point to static data; it is
* **not** owned by the TranslatedString.
*/
TranslatedString( const QVariantMap& map, const QString& key, const char* context = nullptr );
/** @brief Not-actually-translated string.
*/
TranslatedString( const QString& string );
/** @brief Proxy for calling QObject::tr()
*
* This is like the two constructors above, with an empty map an a
* non-null context. It will end up calling tr() with that context.
*
* The @p context, if any, should point to static data; it is
* **not** owned by the TranslatedString.
*/
TranslatedString( const QString& key, const char* context );
/// @brief Empty string
TranslatedString()
: TranslatedString( QString() )
{
}
/** @brief How many strings (translations) are there?
*
* This is always at least 1 (for the untranslated string),
* but may be more than 1 even when isEmpty() is true --
* if there is no untranslated version, for instance.
*/
int count() const { return m_strings.count(); }
/** @brief Consider this string empty?
*
* Only the state of the untranslated string is considered,
* so count() may be more than 1 even while the string is empty.
*/
bool isEmpty() const { return m_strings[ QString() ].isEmpty(); }
/// @brief Gets the string in the current locale
QString get() const;
/// @brief Gets the string from the given locale
QString get( const QLocale& ) const;
private:
// Maps locale name to human-readable string, "" is English
QMap< QString, QString > m_strings;
const char* m_context = nullptr;
};
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,77 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "TranslatableString.h"
/** @brief Massage an identifier into a human-readable form
*
* Makes a copy of @p s, caller must free() it.
*/
static char*
munge( const char* s )
{
char* t = strdup( s );
if ( !t )
{
return nullptr;
}
// replace("_"," ") in the Python script
char* p = t;
while ( *p )
{
if ( ( *p ) == '_' )
{
*p = ' ';
}
++p;
}
return t;
}
namespace Calamares
{
namespace Locale
{
TranslatableString::TranslatableString( TranslatableString&& t )
: m_human( nullptr )
, m_key()
{
// My pointers are initialized to nullptr
std::swap( m_human, t.m_human );
std::swap( m_key, t.m_key );
}
TranslatableString::TranslatableString( const TranslatableString& t )
: m_human( t.m_human ? strdup( t.m_human ) : nullptr )
, m_key( t.m_key )
{
}
TranslatableString::TranslatableString( const char* s1 )
: m_human( s1 ? munge( s1 ) : nullptr )
, m_key( s1 ? QString( s1 ) : QString() )
{
}
TranslatableString::TranslatableString( const QString& s )
: m_human( munge( s.toUtf8().constData() ) )
, m_key( s )
{
}
TranslatableString::~TranslatableString()
{
free( m_human );
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,58 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef LOCALE_TRANSLATABLESTRING_H
#define LOCALE_TRANSLATABLESTRING_H
#include <QString>
namespace Calamares
{
namespace Locale
{
/** @brief A pair of strings, one human-readable, one a key
*
* Given an identifier-like string (e.g. "New_York"), makes
* a human-readable version of that and keeps a copy of the
* identifier itself.
*
* This explicitly uses const char* instead of just being
* QPair<QString, QString> because the human-readable part
* may need to be translated through tr(), and that takes a char*
* C-style strings.
*/
class TranslatableString
{
public:
/// @brief An empty pair
TranslatableString() {}
/// @brief Given an identifier, create the pair
explicit TranslatableString( const char* s1 );
explicit TranslatableString( const QString& s );
TranslatableString( TranslatableString&& t );
TranslatableString( const TranslatableString& );
virtual ~TranslatableString();
/// @brief Give the localized human-readable form
virtual QString translated() const = 0;
QString key() const { return m_key; }
bool operator==( const TranslatableString& other ) const { return m_key == other.m_key; }
bool operator<( const TranslatableString& other ) const { return m_key < other.m_key; }
protected:
char* m_human = nullptr;
QString m_key;
};
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,246 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "Translation.h"
#include <memory>
#include <set>
namespace
{
struct TranslationSpecialCase
{
const char* id; // The Calamares ID for the translation
const char** regions;
QLocale::Language language;
QLocale::Script script;
QLocale::Country country;
const char* name; // Native name, if different from Qt
constexpr bool customLocale() const { return language != QLocale::Language::AnyLanguage; }
};
/** @brief Handle special cases of Calamares language names
*
* If a given @p id (e.g. en_US, or sr@latin) has special handling,
* put an entry in this table. The QLocale constants are used when a
* particular @p id needs specific configuration, **if** @p language
* is not @c AnyLanguage. The @p name is used as a human-readable
* native name if the Qt name is not suitable.
*
* Another form of lookup maps a @p language + a region-identifier
* to a @p id, running around Qt's neglect of `@region` variants.
*
* Examples:
* - `sr@latin` needs specific Qt Locale settnigs, but the name is OK
* - Chinese needs a specific name, but the Locale is OK
*/
static const char* serbian_latin_regions[] = { "latin", "latn", nullptr };
static const char* catalan_regions[] = { "valencia", nullptr };
static constexpr const TranslationSpecialCase special_cases[] = {
{ "sr@latin",
serbian_latin_regions,
QLocale::Language::Serbian,
QLocale::Script::LatinScript,
QLocale::Country::Serbia,
nullptr },
// Valencian is a regional variant of Catalan
{ "ca@valencia",
catalan_regions,
QLocale::Language::Catalan,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"Català (València)" },
{ "ca_ES@valencia",
catalan_regions,
QLocale::Language::Catalan,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"Català (València)" },
// Simplified Chinese, but drop the (China) from the name
{ "zh_CN",
nullptr,
QLocale::Language::AnyLanguage,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"简体中文" },
// Traditional Chinese, but drop (Taiwan) from the name
{ "zh_TW",
nullptr,
QLocale::Language::AnyLanguage,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"繁體中文" },
{ "oc",
nullptr,
QLocale::Language::AnyLanguage,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"Lenga d'òc" },
// Luri
{ "bqi",
nullptr,
QLocale::Language::NorthernLuri,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
nullptr },
// Interlingue is mapped to interlingu*a* (up until Qt version ...) because
// the real Language::Interlingue acts like C locale.
{ "ie",
nullptr,
CALAMARES_QT_SUPPORT_INTERLINGUE ? QLocale::Language::Interlingue : QLocale::Language::Interlingua,
QLocale::Script::AnyScript,
QLocale::Country::AnyCountry,
"Interlingue" },
};
static inline bool
lookup_region( const QByteArray& region, const char** regions_list )
{
if ( regions_list )
{
while ( *regions_list )
{
if ( region == *regions_list )
{
return true;
}
regions_list++;
}
}
return false;
}
static QString
specialCaseSystemLanguage()
{
const QByteArray lang_p = qgetenv( "LANG" );
if ( lang_p.isEmpty() )
{
// This will figure out the system language some other way
return {};
}
auto lang_parts = lang_p.split( '@' );
if ( lang_parts.size() != 2 )
{
return {};
}
QLocale locale( QString::fromLatin1( lang_p ) );
auto it
= std::find_if( std::cbegin( special_cases ),
std::cend( special_cases ),
[ language = locale.language(), region = lang_parts[ 1 ] ]( const TranslationSpecialCase& s )
{ return ( s.language == language ) && lookup_region( region, s.regions ); } );
return ( it != std::cend( special_cases ) ) ? QString::fromLatin1( it->id ) : QString();
}
///@brief Country (territory) name for this locale
QString
territoryName( const QLocale& locale )
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
return QLocale::countryToString( locale.country() );
#else
return QLocale::territoryToString( locale.territory() );
#endif
}
QString
nativeTerritoryName( const QLocale& locale )
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
return locale.nativeCountryName();
#else
return locale.nativeTerritoryName();
#endif
}
bool
needsTerritorialDisambiguation( const QLocale& locale )
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
return QLocale::countriesForLanguage( locale.language() ).count() > 1;
#else
std::set<QLocale::Territory> s;
for(const auto & l : QLocale::matchingLocales( locale.language(), QLocale::Script::AnyScript, QLocale::Territory::AnyTerritory ))
{
s.insert(l.territory());
}
return s.size() > 1;
#endif
}
} // namespace
namespace Calamares
{
namespace Locale
{
Translation::Translation( QObject* parent )
: Translation( { specialCaseSystemLanguage() }, LabelFormat::IfNeededWithCountry, parent )
{
}
Translation::Translation( const Id& localeId, LabelFormat format, QObject* parent )
: QObject( parent )
, m_locale( getLocale( localeId ) )
, m_localeId( localeId.name.isEmpty() ? m_locale.name() : localeId.name )
{
auto it = std::find_if( std::cbegin( special_cases ),
std::cend( special_cases ),
[ &localeId ]( const TranslationSpecialCase& s ) { return localeId.name == s.id; } );
const char* name = ( it != std::cend( special_cases ) ) ? it->name : nullptr;
QString longFormat = QObject::tr( "%1 (%2)" );
QString languageName = name ? QString::fromUtf8( name ) : m_locale.nativeLanguageName();
QString englishName = m_locale.languageToString( m_locale.language() );
if ( languageName.isEmpty() )
{
languageName = QString( "* %1 (%2)" ).arg( localeId.name, englishName );
}
bool needsCountryName = ( format == LabelFormat::AlwaysWithCountry )
|| ( !name && localeId.name.contains( '_' ) && needsTerritorialDisambiguation( ( m_locale ) ) );
const QString countryName = needsCountryName ? nativeTerritoryName( m_locale ) : QString();
m_label = needsCountryName ? longFormat.arg( languageName, countryName ) : languageName;
m_englishLabel = needsCountryName ? longFormat.arg( englishName, territoryName( m_locale ) ) : englishName;
}
QLocale
Translation::getLocale( const Id& localeId )
{
const QString& localeName = localeId.name;
if ( localeName.isEmpty() )
{
return QLocale();
}
auto it = std::find_if( std::cbegin( special_cases ),
std::cend( special_cases ),
[ &localeId ]( const TranslationSpecialCase& s ) { return localeId.name == s.id; } );
if ( it != std::cend( special_cases ) && it->customLocale() )
{
return QLocale( it->language, it->script, it->country );
}
return QLocale( localeName );
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,145 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef LOCALE_TRANSLATION_H
#define LOCALE_TRANSLATION_H
#include "utils/Logger.h"
#include <QLocale>
#include <QObject>
#include <QString>
///@brief Define to 1 if the Qt version being used supports Interlingue fully
#if QT_VERSION < QT_VERSION_CHECK( 6, 7, 0 )
#define CALAMARES_QT_SUPPORT_INTERLINGUE 0
#else
#define CALAMARES_QT_SUPPORT_INTERLINGUE 1
#endif
namespace Calamares
{
namespace Locale
{
/**
* @brief Consistent locale (language + country) naming.
*
* Support class to turn locale names (as used by Calamares's
* translation system) into QLocales, and also into consistent
* human-readable text labels.
*
* This handles special-cases in Calamares translations:
* - `sr@latin` is the name which Qt recognizes as `sr@latn`,
* Serbian written with Latin characters (not Cyrillic).
* - `ca@valencia` is the Catalan dialect spoken in Valencia.
* There is no Qt code for it.
*
* (There are more special cases, not documented here)
*/
class DLLEXPORT Translation : public QObject
{
Q_OBJECT
public:
/** @brief Formatting option for label -- add (country) to label. */
enum class LabelFormat
{
AlwaysWithCountry,
IfNeededWithCountry
};
struct Id
{
QString name;
};
/** @brief Empty locale. This uses the system-default locale. */
Translation( QObject* parent = nullptr );
/** @brief Construct from a locale name.
*
* The @p localeName should be one that Qt recognizes, e.g. en_US or ar_EY.
* The @p format determines whether the country name is always present
* in the label (human-readable form) or only if needed for disambiguation.
*/
Translation( const Id& localeId, LabelFormat format = LabelFormat::IfNeededWithCountry, QObject* parent = nullptr );
/** @brief Define a sorting order.
*
* Locales are sorted by their id, which means the ISO 2-letter code + country.
*/
bool operator<( const Translation& other ) const { return m_localeId < other.m_localeId; }
/** @brief Is this locale English?
*
* en_US and en (American English) is defined as English. The Queen's
* English -- proper English -- is relegated to non-English status.
*/
bool isEnglish() const { return m_localeId == QLatin1String( "en_US" ) || m_localeId == QLatin1String( "en" ); }
/** @brief Get the human-readable name for this locale. */
QString label() const { return m_label; }
/** @brief Get the *English* human-readable name for this locale. */
QString englishLabel() const { return m_englishLabel; }
/** @brief Get the Qt locale. */
QLocale locale() const { return m_locale; }
/** @brief Gets the Calamares internal name (code) of the locale.
*
* This is a strongly-typed return to avoid it ending up all over
* the place as a QString.
*/
Id id() const { return { m_localeId }; }
/// @brief Convenience accessor to the language part of the locale
QLocale::Language language() const { return m_locale.language(); }
/// @brief Convenience accessor to the country part (if any) of the locale
QLocale::Country country() const
{
#if QT_VERSION < QT_VERSION_CHECK( 6, 6, 0 )
return m_locale.country();
#else
return m_locale.territory();
#endif
}
/** @brief Get a Qt locale for the given @p localeName
*
* This obeys special cases as described in the class documentation.
*/
static QLocale getLocale( const Id& localeId );
private:
QLocale m_locale;
QString m_localeId; // the locale identifier, e.g. "en_GB"
QString m_label; // the native name of the locale
QString m_englishLabel;
};
static inline QDebug&
operator<<( QDebug& s, const Translation::Id& id )
{
return s << id.name;
}
static inline bool
operator==( const Translation::Id& lhs, const Translation::Id& rhs )
{
return lhs.name == rhs.name;
}
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,155 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Camilo Higuita <milo.h@aol.com>
* SPDX-FileCopyrightText: 2019-2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#include "TranslationsModel.h"
#include "Lookup.h"
#include "CalamaresTranslations.cc" // For the list of translations, generated at build time
namespace Calamares
{
namespace Locale
{
TranslationsModel::TranslationsModel( const QStringList& locales, QObject* parent )
: QAbstractListModel( parent )
, m_localeIds( locales )
{
Q_ASSERT( locales.count() > 0 );
m_locales.reserve( locales.count() );
for ( const auto& l : locales )
{
m_locales.push_back( new Translation( { l }, Translation::LabelFormat::IfNeededWithCountry, this ) );
}
}
TranslationsModel::~TranslationsModel() {}
int
TranslationsModel::rowCount( const QModelIndex& ) const
{
return m_locales.count();
}
QVariant
TranslationsModel::data( const QModelIndex& index, int role ) const
{
if ( ( role != LabelRole ) && ( role != EnglishLabelRole ) )
{
return QVariant();
}
if ( !index.isValid() )
{
return QVariant();
}
const auto& locale = m_locales.at( index.row() );
switch ( role )
{
case LabelRole:
return locale->label();
case EnglishLabelRole:
return locale->englishLabel();
default:
return QVariant();
}
}
QHash< int, QByteArray >
TranslationsModel::roleNames() const
{
return { { LabelRole, "label" }, { EnglishLabelRole, "englishLabel" } };
}
const Translation&
TranslationsModel::locale( int row ) const
{
if ( ( row < 0 ) || ( row >= m_locales.count() ) )
{
for ( const auto& l : m_locales )
{
if ( l->isEnglish() )
{
return *l;
}
}
return *m_locales[ 0 ];
}
return *m_locales[ row ];
}
int
TranslationsModel::find( std::function< bool( const Translation& ) > predicate ) const
{
for ( int row = 0; row < m_locales.count(); ++row )
{
if ( predicate( *m_locales[ row ] ) )
{
return row;
}
}
return -1;
}
int
TranslationsModel::find( std::function< bool( const QLocale& ) > predicate ) const
{
return find( [ & ]( const Translation& l ) { return predicate( l.locale() ); } );
}
int
TranslationsModel::find( const QLocale& locale ) const
{
return find( [ & ]( const Translation& l ) { return locale == l.locale(); } );
}
int
TranslationsModel::find( const QString& countryCode ) const
{
if ( countryCode.length() != 2 )
{
return -1;
}
auto c_l = countryData( countryCode );
int r = find( [ & ]( const Translation& l )
{ return ( l.language() == c_l.second ) && ( l.country() == c_l.first ); } );
if ( r >= 0 )
{
return r;
}
return find( [ & ]( const Translation& l ) { return l.language() == c_l.second; } );
}
int
TranslationsModel::find( const Translation::Id& id ) const
{
return find( [ & ]( const Translation& l ) { return l.id() == id; } );
}
TranslationsModel*
availableTranslations()
{
static TranslationsModel* model = new TranslationsModel( availableLanguageList );
return model;
}
const QStringList&
availableLanguages()
{
return availableLanguageList;
}
} // namespace Locale
} // namespace Calamares

View File

@@ -0,0 +1,94 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Camilo Higuita <milo.h@aol.com>
* SPDX-FileCopyrightText: 2019-2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef LOCALE_TRANSLATIONSMODEL_H
#define LOCALE_TRANSLATIONSMODEL_H
#include "DllMacro.h"
#include "Translation.h"
#include <QAbstractListModel>
#include <QVector>
namespace Calamares
{
namespace Locale
{
class DLLEXPORT TranslationsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum
{
LabelRole = Qt::DisplayRole,
EnglishLabelRole = Qt::UserRole + 1
};
TranslationsModel( const QStringList& locales, QObject* parent = nullptr );
~TranslationsModel() override;
int rowCount( const QModelIndex& parent ) const override;
QVariant data( const QModelIndex& index, int role ) const override;
QHash< int, QByteArray > roleNames() const override;
/** @brief Gets locale information for entry #n
*
* This is the backing data for the model; if @p row is out-of-range,
* returns a reference to en_US.
*/
const Translation& locale( int row ) const;
/// @brief Returns all of the locale Ids (e.g. en_US) put into this model.
const QStringList& localeIds() const { return m_localeIds; }
/** @brief Searches for an item that matches @p predicate
*
* Returns the row number of the first match, or -1 if there isn't one.
*/
int find( std::function< bool( const QLocale& ) > predicate ) const;
int find( std::function< bool( const Translation& ) > predicate ) const;
/// @brief Looks for an item using the same locale, -1 if there isn't one
int find( const QLocale& ) const;
/// @brief Looks for an item that best matches the 2-letter country code
int find( const QString& countryCode ) const;
/// @brief Looks up a translation Id
int find( const Translation::Id& id ) const;
private:
QVector< Translation* > m_locales;
QStringList m_localeIds;
};
/** @brief Returns a model with all available translations.
*
* The translations are set when Calamares is compiled; the list
* of names used can be queried with avalableLanguages().
*
* This model is a singleton and can be shared.
*
* NOTE: While the model is not typed const, it should be. Do not modify.
*/
DLLEXPORT TranslationsModel* availableTranslations();
/** @brief The list of names (e.g. en, pt_BR) of available translations.
*
* The translations are set when Calamares is compiled.
* At CMake-time, the list CALAMARES_TRANSLATION_LANGUAGES
* is used to create the table.
*/
DLLEXPORT const QStringList& availableLanguages();
} // namespace Locale
} // namespace Calamares
#endif

View File

@@ -0,0 +1,480 @@
/* GENERATED FILE DO NOT EDIT
*
* SPDX-FileCopyrightText: 2009 Arthur David Olson
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: CC0-1.0
*
* This file is derived from zone.tab, which has its own copyright statement:
*
* This file is in the public domain, so clarified as of
* 2009-05-17 by Arthur David Olson.
*
* From Paul Eggert (2018-06-27):
* This file is intended as a backward-compatibility aid for older programs.
* New programs should use zone1970.tab. This file is like zone1970.tab (see
* zone1970.tab's comments), but with the following additional restrictions:
*
* 1. This file contains only ASCII characters.
* 2. The first data column contains exactly one country code.
*
*/
/** THIS FILE EXISTS ONLY FOR TRANSLATIONS PURPOSES **/
// *INDENT-OFF*
// clang-format off
/* This returns a reference to local, which is a terrible idea.
* Good thing it's not meant to be compiled.
*/
static const QStringList& tz_regions_table()
{
return QStringList {
QObject::tr("Africa", "tz_regions"),
QObject::tr("America", "tz_regions"),
QObject::tr("Antarctica", "tz_regions"),
QObject::tr("Arctic", "tz_regions"),
QObject::tr("Asia", "tz_regions"),
QObject::tr("Atlantic", "tz_regions"),
QObject::tr("Australia", "tz_regions"),
QObject::tr("Europe", "tz_regions"),
QObject::tr("Indian", "tz_regions"),
QObject::tr("Pacific", "tz_regions"),
QString()
};
}
/* This returns a reference to local, which is a terrible idea.
* Good thing it's not meant to be compiled.
*/
static const QStringList& tz_names_table()
{
return QStringList {
QObject::tr("Abidjan", "tz_names"),
QObject::tr("Accra", "tz_names"),
QObject::tr("Adak", "tz_names"),
QObject::tr("Addis Ababa", "tz_names"),
QObject::tr("Adelaide", "tz_names"),
QObject::tr("Aden", "tz_names"),
QObject::tr("Algiers", "tz_names"),
QObject::tr("Almaty", "tz_names"),
QObject::tr("Amman", "tz_names"),
QObject::tr("Amsterdam", "tz_names"),
QObject::tr("Anadyr", "tz_names"),
QObject::tr("Anchorage", "tz_names"),
QObject::tr("Andorra", "tz_names"),
QObject::tr("Anguilla", "tz_names"),
QObject::tr("Antananarivo", "tz_names"),
QObject::tr("Antigua", "tz_names"),
QObject::tr("Apia", "tz_names"),
QObject::tr("Aqtau", "tz_names"),
QObject::tr("Aqtobe", "tz_names"),
QObject::tr("Araguaina", "tz_names"),
QObject::tr("Argentina/Buenos Aires", "tz_names"),
QObject::tr("Argentina/Catamarca", "tz_names"),
QObject::tr("Argentina/Cordoba", "tz_names"),
QObject::tr("Argentina/Jujuy", "tz_names"),
QObject::tr("Argentina/La Rioja", "tz_names"),
QObject::tr("Argentina/Mendoza", "tz_names"),
QObject::tr("Argentina/Rio Gallegos", "tz_names"),
QObject::tr("Argentina/Salta", "tz_names"),
QObject::tr("Argentina/San Juan", "tz_names"),
QObject::tr("Argentina/San Luis", "tz_names"),
QObject::tr("Argentina/Tucuman", "tz_names"),
QObject::tr("Argentina/Ushuaia", "tz_names"),
QObject::tr("Aruba", "tz_names"),
QObject::tr("Ashgabat", "tz_names"),
QObject::tr("Asmara", "tz_names"),
QObject::tr("Astrakhan", "tz_names"),
QObject::tr("Asuncion", "tz_names"),
QObject::tr("Athens", "tz_names"),
QObject::tr("Atikokan", "tz_names"),
QObject::tr("Atyrau", "tz_names"),
QObject::tr("Auckland", "tz_names"),
QObject::tr("Azores", "tz_names"),
QObject::tr("Baghdad", "tz_names"),
QObject::tr("Bahia", "tz_names"),
QObject::tr("Bahia Banderas", "tz_names"),
QObject::tr("Bahrain", "tz_names"),
QObject::tr("Baku", "tz_names"),
QObject::tr("Bamako", "tz_names"),
QObject::tr("Bangkok", "tz_names"),
QObject::tr("Bangui", "tz_names"),
QObject::tr("Banjul", "tz_names"),
QObject::tr("Barbados", "tz_names"),
QObject::tr("Barnaul", "tz_names"),
QObject::tr("Beirut", "tz_names"),
QObject::tr("Belem", "tz_names"),
QObject::tr("Belgrade", "tz_names"),
QObject::tr("Belize", "tz_names"),
QObject::tr("Berlin", "tz_names"),
QObject::tr("Bermuda", "tz_names"),
QObject::tr("Bishkek", "tz_names"),
QObject::tr("Bissau", "tz_names"),
QObject::tr("Blanc-Sablon", "tz_names"),
QObject::tr("Blantyre", "tz_names"),
QObject::tr("Boa Vista", "tz_names"),
QObject::tr("Bogota", "tz_names"),
QObject::tr("Boise", "tz_names"),
QObject::tr("Bougainville", "tz_names"),
QObject::tr("Bratislava", "tz_names"),
QObject::tr("Brazzaville", "tz_names"),
QObject::tr("Brisbane", "tz_names"),
QObject::tr("Broken Hill", "tz_names"),
QObject::tr("Brunei", "tz_names"),
QObject::tr("Brussels", "tz_names"),
QObject::tr("Bucharest", "tz_names"),
QObject::tr("Budapest", "tz_names"),
QObject::tr("Bujumbura", "tz_names"),
QObject::tr("Busingen", "tz_names"),
QObject::tr("Cairo", "tz_names"),
QObject::tr("Cambridge Bay", "tz_names"),
QObject::tr("Campo Grande", "tz_names"),
QObject::tr("Canary", "tz_names"),
QObject::tr("Cancun", "tz_names"),
QObject::tr("Cape Verde", "tz_names"),
QObject::tr("Caracas", "tz_names"),
QObject::tr("Casablanca", "tz_names"),
QObject::tr("Casey", "tz_names"),
QObject::tr("Cayenne", "tz_names"),
QObject::tr("Cayman", "tz_names"),
QObject::tr("Ceuta", "tz_names"),
QObject::tr("Chagos", "tz_names"),
QObject::tr("Chatham", "tz_names"),
QObject::tr("Chicago", "tz_names"),
QObject::tr("Chihuahua", "tz_names"),
QObject::tr("Chisinau", "tz_names"),
QObject::tr("Chita", "tz_names"),
QObject::tr("Choibalsan", "tz_names"),
QObject::tr("Christmas", "tz_names"),
QObject::tr("Chuuk", "tz_names"),
QObject::tr("Cocos", "tz_names"),
QObject::tr("Colombo", "tz_names"),
QObject::tr("Comoro", "tz_names"),
QObject::tr("Conakry", "tz_names"),
QObject::tr("Copenhagen", "tz_names"),
QObject::tr("Costa Rica", "tz_names"),
QObject::tr("Creston", "tz_names"),
QObject::tr("Cuiaba", "tz_names"),
QObject::tr("Curacao", "tz_names"),
QObject::tr("Currie", "tz_names"),
QObject::tr("Dakar", "tz_names"),
QObject::tr("Damascus", "tz_names"),
QObject::tr("Danmarkshavn", "tz_names"),
QObject::tr("Dar es Salaam", "tz_names"),
QObject::tr("Darwin", "tz_names"),
QObject::tr("Davis", "tz_names"),
QObject::tr("Dawson", "tz_names"),
QObject::tr("Dawson Creek", "tz_names"),
QObject::tr("Denver", "tz_names"),
QObject::tr("Detroit", "tz_names"),
QObject::tr("Dhaka", "tz_names"),
QObject::tr("Dili", "tz_names"),
QObject::tr("Djibouti", "tz_names"),
QObject::tr("Dominica", "tz_names"),
QObject::tr("Douala", "tz_names"),
QObject::tr("Dubai", "tz_names"),
QObject::tr("Dublin", "tz_names"),
QObject::tr("DumontDUrville", "tz_names"),
QObject::tr("Dushanbe", "tz_names"),
QObject::tr("Easter", "tz_names"),
QObject::tr("Edmonton", "tz_names"),
QObject::tr("Efate", "tz_names"),
QObject::tr("Eirunepe", "tz_names"),
QObject::tr("El Aaiun", "tz_names"),
QObject::tr("El Salvador", "tz_names"),
QObject::tr("Enderbury", "tz_names"),
QObject::tr("Eucla", "tz_names"),
QObject::tr("Fakaofo", "tz_names"),
QObject::tr("Famagusta", "tz_names"),
QObject::tr("Faroe", "tz_names"),
QObject::tr("Fiji", "tz_names"),
QObject::tr("Fort Nelson", "tz_names"),
QObject::tr("Fortaleza", "tz_names"),
QObject::tr("Freetown", "tz_names"),
QObject::tr("Funafuti", "tz_names"),
QObject::tr("Gaborone", "tz_names"),
QObject::tr("Galapagos", "tz_names"),
QObject::tr("Gambier", "tz_names"),
QObject::tr("Gaza", "tz_names"),
QObject::tr("Gibraltar", "tz_names"),
QObject::tr("Glace Bay", "tz_names"),
QObject::tr("Godthab", "tz_names"),
QObject::tr("Goose Bay", "tz_names"),
QObject::tr("Grand Turk", "tz_names"),
QObject::tr("Grenada", "tz_names"),
QObject::tr("Guadalcanal", "tz_names"),
QObject::tr("Guadeloupe", "tz_names"),
QObject::tr("Guam", "tz_names"),
QObject::tr("Guatemala", "tz_names"),
QObject::tr("Guayaquil", "tz_names"),
QObject::tr("Guernsey", "tz_names"),
QObject::tr("Guyana", "tz_names"),
QObject::tr("Halifax", "tz_names"),
QObject::tr("Harare", "tz_names"),
QObject::tr("Havana", "tz_names"),
QObject::tr("Hebron", "tz_names"),
QObject::tr("Helsinki", "tz_names"),
QObject::tr("Hermosillo", "tz_names"),
QObject::tr("Ho Chi Minh", "tz_names"),
QObject::tr("Hobart", "tz_names"),
QObject::tr("Hong Kong", "tz_names"),
QObject::tr("Honolulu", "tz_names"),
QObject::tr("Hovd", "tz_names"),
QObject::tr("Indiana/Indianapolis", "tz_names"),
QObject::tr("Indiana/Knox", "tz_names"),
QObject::tr("Indiana/Marengo", "tz_names"),
QObject::tr("Indiana/Petersburg", "tz_names"),
QObject::tr("Indiana/Tell City", "tz_names"),
QObject::tr("Indiana/Vevay", "tz_names"),
QObject::tr("Indiana/Vincennes", "tz_names"),
QObject::tr("Indiana/Winamac", "tz_names"),
QObject::tr("Inuvik", "tz_names"),
QObject::tr("Iqaluit", "tz_names"),
QObject::tr("Irkutsk", "tz_names"),
QObject::tr("Isle of Man", "tz_names"),
QObject::tr("Istanbul", "tz_names"),
QObject::tr("Jakarta", "tz_names"),
QObject::tr("Jamaica", "tz_names"),
QObject::tr("Jayapura", "tz_names"),
QObject::tr("Jersey", "tz_names"),
QObject::tr("Jerusalem", "tz_names"),
QObject::tr("Johannesburg", "tz_names"),
QObject::tr("Juba", "tz_names"),
QObject::tr("Juneau", "tz_names"),
QObject::tr("Kabul", "tz_names"),
QObject::tr("Kaliningrad", "tz_names"),
QObject::tr("Kamchatka", "tz_names"),
QObject::tr("Kampala", "tz_names"),
QObject::tr("Karachi", "tz_names"),
QObject::tr("Kathmandu", "tz_names"),
QObject::tr("Kentucky/Louisville", "tz_names"),
QObject::tr("Kentucky/Monticello", "tz_names"),
QObject::tr("Kerguelen", "tz_names"),
QObject::tr("Khandyga", "tz_names"),
QObject::tr("Khartoum", "tz_names"),
QObject::tr("Kiev", "tz_names"),
QObject::tr("Kigali", "tz_names"),
QObject::tr("Kinshasa", "tz_names"),
QObject::tr("Kiritimati", "tz_names"),
QObject::tr("Kirov", "tz_names"),
QObject::tr("Kolkata", "tz_names"),
QObject::tr("Kosrae", "tz_names"),
QObject::tr("Kralendijk", "tz_names"),
QObject::tr("Krasnoyarsk", "tz_names"),
QObject::tr("Kuala Lumpur", "tz_names"),
QObject::tr("Kuching", "tz_names"),
QObject::tr("Kuwait", "tz_names"),
QObject::tr("Kwajalein", "tz_names"),
QObject::tr("La Paz", "tz_names"),
QObject::tr("Lagos", "tz_names"),
QObject::tr("Libreville", "tz_names"),
QObject::tr("Lima", "tz_names"),
QObject::tr("Lindeman", "tz_names"),
QObject::tr("Lisbon", "tz_names"),
QObject::tr("Ljubljana", "tz_names"),
QObject::tr("Lome", "tz_names"),
QObject::tr("London", "tz_names"),
QObject::tr("Longyearbyen", "tz_names"),
QObject::tr("Lord Howe", "tz_names"),
QObject::tr("Los Angeles", "tz_names"),
QObject::tr("Lower Princes", "tz_names"),
QObject::tr("Luanda", "tz_names"),
QObject::tr("Lubumbashi", "tz_names"),
QObject::tr("Lusaka", "tz_names"),
QObject::tr("Luxembourg", "tz_names"),
QObject::tr("Macau", "tz_names"),
QObject::tr("Maceio", "tz_names"),
QObject::tr("Macquarie", "tz_names"),
QObject::tr("Madeira", "tz_names"),
QObject::tr("Madrid", "tz_names"),
QObject::tr("Magadan", "tz_names"),
QObject::tr("Mahe", "tz_names"),
QObject::tr("Majuro", "tz_names"),
QObject::tr("Makassar", "tz_names"),
QObject::tr("Malabo", "tz_names"),
QObject::tr("Maldives", "tz_names"),
QObject::tr("Malta", "tz_names"),
QObject::tr("Managua", "tz_names"),
QObject::tr("Manaus", "tz_names"),
QObject::tr("Manila", "tz_names"),
QObject::tr("Maputo", "tz_names"),
QObject::tr("Mariehamn", "tz_names"),
QObject::tr("Marigot", "tz_names"),
QObject::tr("Marquesas", "tz_names"),
QObject::tr("Martinique", "tz_names"),
QObject::tr("Maseru", "tz_names"),
QObject::tr("Matamoros", "tz_names"),
QObject::tr("Mauritius", "tz_names"),
QObject::tr("Mawson", "tz_names"),
QObject::tr("Mayotte", "tz_names"),
QObject::tr("Mazatlan", "tz_names"),
QObject::tr("Mbabane", "tz_names"),
QObject::tr("McMurdo", "tz_names"),
QObject::tr("Melbourne", "tz_names"),
QObject::tr("Menominee", "tz_names"),
QObject::tr("Merida", "tz_names"),
QObject::tr("Metlakatla", "tz_names"),
QObject::tr("Mexico City", "tz_names"),
QObject::tr("Midway", "tz_names"),
QObject::tr("Minsk", "tz_names"),
QObject::tr("Miquelon", "tz_names"),
QObject::tr("Mogadishu", "tz_names"),
QObject::tr("Monaco", "tz_names"),
QObject::tr("Moncton", "tz_names"),
QObject::tr("Monrovia", "tz_names"),
QObject::tr("Monterrey", "tz_names"),
QObject::tr("Montevideo", "tz_names"),
QObject::tr("Montserrat", "tz_names"),
QObject::tr("Moscow", "tz_names"),
QObject::tr("Muscat", "tz_names"),
QObject::tr("Nairobi", "tz_names"),
QObject::tr("Nassau", "tz_names"),
QObject::tr("Nauru", "tz_names"),
QObject::tr("Ndjamena", "tz_names"),
QObject::tr("New York", "tz_names"),
QObject::tr("Niamey", "tz_names"),
QObject::tr("Nicosia", "tz_names"),
QObject::tr("Nipigon", "tz_names"),
QObject::tr("Niue", "tz_names"),
QObject::tr("Nome", "tz_names"),
QObject::tr("Norfolk", "tz_names"),
QObject::tr("Noronha", "tz_names"),
QObject::tr("North Dakota/Beulah", "tz_names"),
QObject::tr("North Dakota/Center", "tz_names"),
QObject::tr("North Dakota/New Salem", "tz_names"),
QObject::tr("Nouakchott", "tz_names"),
QObject::tr("Noumea", "tz_names"),
QObject::tr("Novokuznetsk", "tz_names"),
QObject::tr("Novosibirsk", "tz_names"),
QObject::tr("Ojinaga", "tz_names"),
QObject::tr("Omsk", "tz_names"),
QObject::tr("Oral", "tz_names"),
QObject::tr("Oslo", "tz_names"),
QObject::tr("Ouagadougou", "tz_names"),
QObject::tr("Pago Pago", "tz_names"),
QObject::tr("Palau", "tz_names"),
QObject::tr("Palmer", "tz_names"),
QObject::tr("Panama", "tz_names"),
QObject::tr("Pangnirtung", "tz_names"),
QObject::tr("Paramaribo", "tz_names"),
QObject::tr("Paris", "tz_names"),
QObject::tr("Perth", "tz_names"),
QObject::tr("Phnom Penh", "tz_names"),
QObject::tr("Phoenix", "tz_names"),
QObject::tr("Pitcairn", "tz_names"),
QObject::tr("Podgorica", "tz_names"),
QObject::tr("Pohnpei", "tz_names"),
QObject::tr("Pontianak", "tz_names"),
QObject::tr("Port Moresby", "tz_names"),
QObject::tr("Port of Spain", "tz_names"),
QObject::tr("Port-au-Prince", "tz_names"),
QObject::tr("Porto Velho", "tz_names"),
QObject::tr("Porto-Novo", "tz_names"),
QObject::tr("Prague", "tz_names"),
QObject::tr("Puerto Rico", "tz_names"),
QObject::tr("Punta Arenas", "tz_names"),
QObject::tr("Pyongyang", "tz_names"),
QObject::tr("Qatar", "tz_names"),
QObject::tr("Qostanay", "tz_names"),
QObject::tr("Qyzylorda", "tz_names"),
QObject::tr("Rainy River", "tz_names"),
QObject::tr("Rankin Inlet", "tz_names"),
QObject::tr("Rarotonga", "tz_names"),
QObject::tr("Recife", "tz_names"),
QObject::tr("Regina", "tz_names"),
QObject::tr("Resolute", "tz_names"),
QObject::tr("Reunion", "tz_names"),
QObject::tr("Reykjavik", "tz_names"),
QObject::tr("Riga", "tz_names"),
QObject::tr("Rio Branco", "tz_names"),
QObject::tr("Riyadh", "tz_names"),
QObject::tr("Rome", "tz_names"),
QObject::tr("Rothera", "tz_names"),
QObject::tr("Saipan", "tz_names"),
QObject::tr("Sakhalin", "tz_names"),
QObject::tr("Samara", "tz_names"),
QObject::tr("Samarkand", "tz_names"),
QObject::tr("San Marino", "tz_names"),
QObject::tr("Santarem", "tz_names"),
QObject::tr("Santiago", "tz_names"),
QObject::tr("Santo Domingo", "tz_names"),
QObject::tr("Sao Paulo", "tz_names"),
QObject::tr("Sao Tome", "tz_names"),
QObject::tr("Sarajevo", "tz_names"),
QObject::tr("Saratov", "tz_names"),
QObject::tr("Scoresbysund", "tz_names"),
QObject::tr("Seoul", "tz_names"),
QObject::tr("Shanghai", "tz_names"),
QObject::tr("Simferopol", "tz_names"),
QObject::tr("Singapore", "tz_names"),
QObject::tr("Sitka", "tz_names"),
QObject::tr("Skopje", "tz_names"),
QObject::tr("Sofia", "tz_names"),
QObject::tr("South Georgia", "tz_names"),
QObject::tr("Srednekolymsk", "tz_names"),
QObject::tr("St Barthelemy", "tz_names"),
QObject::tr("St Helena", "tz_names"),
QObject::tr("St Johns", "tz_names"),
QObject::tr("St Kitts", "tz_names"),
QObject::tr("St Lucia", "tz_names"),
QObject::tr("St Thomas", "tz_names"),
QObject::tr("St Vincent", "tz_names"),
QObject::tr("Stanley", "tz_names"),
QObject::tr("Stockholm", "tz_names"),
QObject::tr("Swift Current", "tz_names"),
QObject::tr("Sydney", "tz_names"),
QObject::tr("Syowa", "tz_names"),
QObject::tr("Tahiti", "tz_names"),
QObject::tr("Taipei", "tz_names"),
QObject::tr("Tallinn", "tz_names"),
QObject::tr("Tarawa", "tz_names"),
QObject::tr("Tashkent", "tz_names"),
QObject::tr("Tbilisi", "tz_names"),
QObject::tr("Tegucigalpa", "tz_names"),
QObject::tr("Tehran", "tz_names"),
QObject::tr("Thimphu", "tz_names"),
QObject::tr("Thule", "tz_names"),
QObject::tr("Thunder Bay", "tz_names"),
QObject::tr("Tijuana", "tz_names"),
QObject::tr("Tirane", "tz_names"),
QObject::tr("Tokyo", "tz_names"),
QObject::tr("Tomsk", "tz_names"),
QObject::tr("Tongatapu", "tz_names"),
QObject::tr("Toronto", "tz_names"),
QObject::tr("Tortola", "tz_names"),
QObject::tr("Tripoli", "tz_names"),
QObject::tr("Troll", "tz_names"),
QObject::tr("Tunis", "tz_names"),
QObject::tr("Ulaanbaatar", "tz_names"),
QObject::tr("Ulyanovsk", "tz_names"),
QObject::tr("Urumqi", "tz_names"),
QObject::tr("Ust-Nera", "tz_names"),
QObject::tr("Uzhgorod", "tz_names"),
QObject::tr("Vaduz", "tz_names"),
QObject::tr("Vancouver", "tz_names"),
QObject::tr("Vatican", "tz_names"),
QObject::tr("Vienna", "tz_names"),
QObject::tr("Vientiane", "tz_names"),
QObject::tr("Vilnius", "tz_names"),
QObject::tr("Vladivostok", "tz_names"),
QObject::tr("Volgograd", "tz_names"),
QObject::tr("Vostok", "tz_names"),
QObject::tr("Wake", "tz_names"),
QObject::tr("Wallis", "tz_names"),
QObject::tr("Warsaw", "tz_names"),
QObject::tr("Whitehorse", "tz_names"),
QObject::tr("Windhoek", "tz_names"),
QObject::tr("Winnipeg", "tz_names"),
QObject::tr("Yakutat", "tz_names"),
QObject::tr("Yakutsk", "tz_names"),
QObject::tr("Yangon", "tz_names"),
QObject::tr("Yekaterinburg", "tz_names"),
QObject::tr("Yellowknife", "tz_names"),
QObject::tr("Yerevan", "tz_names"),
QObject::tr("Zagreb", "tz_names"),
QObject::tr("Zaporozhye", "tz_names"),
QObject::tr("Zurich", "tz_names"),
QString()
};
}

View File

@@ -0,0 +1,271 @@
#! /usr/bin/env python3
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
"""
Python3 script to scrape some data out of ICU CLDR supplemental data.
To use this script, you must have downloaded the CLDR data, e.g.
http://unicode.org/Public/cldr/35.1/, and extracted the zip file.
Run the script from **inside** the common/ durectory that is created
(or fix the hard-coded path).
The script tries to print C++ code that compiles; if there are encoding
problems, it will print some kind of representation of the problematic
lines.
To avoid having to cross-reference multiple XML files, the script
cheats: it reads the comments as well to get names. So it looks for
pairs of lines like this:
<likelySubtag from="und_BQ" to="pap_Latn_BQ"/>
<!--{ ?; ?; Caribbean Netherlands } => { Papiamento; Latin; Caribbean Netherlands }-->
It extracts the 2-character country code "BQ" from the sub-tag, and
parses the comment to get a language and country name (instead of looking up
"pap" and "BQ" in other tables). This may be considered a hack.
A large collection of exceptions can be found in the two *_mapper tables,
which massage the CLDR names to Qt enum values.
"""
#
### END USAGE
import sys
# These are languages listed in CLDR that don't match
# the enum-values in QLocale::Language.
language_mapper = {
"?" : "AnyLanguage",
"Bangla" : "Bengali",
"Kalaallisut" : "Greenlandic",
"Haitian Creole" : "Haitian",
"Kyrgyz" : "Kirghiz",
"Norwegian Bokmål" : "NorwegianBokmal",
"Tokelau" : "TokelauLanguage",
"Tuvalu" : "TuvaluLanguage",
}
country_mapper = {
"Åland Islands" : "AlandIslands",
"St. Barthélemy" : "SaintBarthelemy",
"Côte dIvoire" : "IvoryCoast",
"Curaçao" : "CuraSao",
"Réunion" : "Reunion",
"São Tomé & Príncipe" : "SaoTomeAndPrincipe",
"Bosnia & Herzegovina" : "BosniaAndHerzegowina",
"Czechia" : "CzechRepublic",
"St. Pierre & Miquelon" : "SaintPierreAndMiquelon",
"Vatican City" : "VaticanCityState",
"South Georgia & South Sandwich Islands" : "SouthGeorgiaAndTheSouthSandwichIslands",
"Timor-Leste" : "EastTimor",
"Wallis & Futuna" : "WallisAndFutunaIslands",
"Myanmar (Burma)" : "Myanmar",
"Svalbard & Jan Mayen" : "SvalbardAndJanMayenIslands",
"St. Martin" : "SaintMartin",
"North Macedonia" : "Macedonia",
"Hong Kong SAR China" : "HongKong",
"Macao SAR China" : "Macau",
"Eurozone" : "AnyCountry", # Not likely for GeoIP
"Caribbean Netherlands" : "Bonaire", # Bonaire, Saba, St.Eustatius
}
class CountryData:
def __init__(self, country_code, language_name, country_name):
"""
Takes a 2-letter country name, and enum names from
QLocale::Language and QLocale::Country. An empty
@p country code is acceptable, for the terminating
entry in the data array (and yields a 0,0 code).
"""
if country_code:
assert len(country_code) == 2
self.country_code = country_code
self.language_enum = language_name
self.country_enum = country_name
else:
self.country_code = ""
self.language_enum = "AnyLanguage"
self.country_enum = "AnyCountry"
def __str__(self):
if self.country_code:
char0 = "'{!s}'".format(self.country_code[0])
char1 = "'{!s}'".format(self.country_code[1])
else:
char0 = "0"
char1 = "0"
return "{!s} QLocale::Language::{!s}, QLocale::Country::{!s}, {!s}, {!s} {!s},".format(
"{",
self.language_enum,
self.country_enum,
char0,
char1,
"}")
# Must match type name below
cpp_classname = "CountryData"
# Must match the output format of __str__ above
cpp_declaration = """
struct CountryData
{
QLocale::Language l;
QLocale::Country c;
char cc1;
char cc2;
};
"""
def extricate_subtags(l1, l2):
"""
Given two lines @p l1 and @p l2 which are the <likelySubtag> element-line
and the comment-line underneath it, return a CountryData for them,
or None if the two lines are not relevant (e.g. not the right subtag from,
or 3-letter country codes.
"""
if 'from="und_' not in l1:
return
if '{ ?; ?;' not in l2:
return
# This is extremely crude "parsing" which chops up the string
# by delimiter and then extracts some substring.
l1_parts = l1.split("und_")
l2_parts = l2.split(";")
l1_first_quote = l1_parts[1].find('"')
l1_code = l1_parts[1][:l1_first_quote]
if len(l1_code) != 2:
return
l2_brace = l2_parts[2].find("{")
l2_language = l2_parts[2][l2_brace+1:].strip()
l2_brace = l2_parts[2].find("}")
l2_country = l2_parts[2][:l2_brace-1].strip()
# Handle mapped cases
l2_language = language_mapper.get(l2_language, l2_language)
l2_language = l2_language.replace(" ", "")
# Handle mapped cases and then do a bunch of standard replacements.
l2_country = country_mapper.get(l2_country, l2_country)
l2_country = l2_country.replace(" ", "").replace("-", "").replace(".","").replace("&","And")
return CountryData(l1_code, l2_language, l2_country)
def read_subtags_file():
"""
Returns a list of CountryData objects from the likelySubtags file.
"""
data = []
with open("supplemental/likelySubtags.xml", "rt", encoding="UTF-8") as f:
l1 = "a line"
while l1:
l1 = f.readline()
if '<likelySubtag from="und_' not in l1:
continue
l2 = f.readline()
if l1:
assert "likelySubtag" in l1, l1;
assert "<!--" in l2, l2;
data.append(extricate_subtags(l1, l2))
data.append(CountryData("", None, None))
return [c for c in data if c is not None]
cpp_header_comment = """/* GENERATED FILE DO NOT EDIT
*
* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 1991-2019 Unicode, Inc.
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: CC0
*
* This file is derived from CLDR data from Unicode, Inc. Applicable terms
* are listed at http://unicode.org/copyright.html , of which the most
* important are:
*
* A. Unicode Copyright
* 1. Copyright © 1991-2019 Unicode, Inc. All rights reserved.
* B. Definitions
* Unicode Data Files ("DATA FILES") include all data files under the directories:
* https://www.unicode.org/Public/
* C. Terms of Use
* 1. Certain documents and files on this website contain a legend indicating
* that "Modification is permitted." Any person is hereby authorized,
* without fee, to modify such documents and files to create derivative
* works conforming to the Unicode® Standard, subject to Terms and
* Conditions herein.
* 2. Any person is hereby authorized, without fee, to view, use, reproduce,
* and distribute all documents and files, subject to the Terms and
* Conditions herein.
*/
// BEGIN Generated from CLDR data
// *INDENT-OFF*
// clang-format off
"""
cpp_footer_comment = """
// END Generated from CLDR data
"""
def make_identifier(classname):
"""
Given a class name (e.g. CountryData) return an identifer
for the data-table for that class.
"""
identifier = [ classname[0].lower() ]
for c in classname[1:]:
if c.isupper():
identifier.extend(["_", c.lower()])
else:
identifier.append(c)
return "".join(identifier)
def export_class(cls, data):
"""
Given a @p cls and a list of @p data objects from that class,
print (to stdout) a C++ file for that data.
"""
identifier = make_identifier(cls.cpp_classname)
with open("{!s}_p.cpp".format(cls.cpp_classname), "wt", encoding="UTF-8") as f:
f.write(cpp_header_comment)
f.write(cls.cpp_declaration)
f.write("\nstatic constexpr int const {!s}_size = {!s};\n".format(
identifier,
len(data)))
f.write("\nstatic const {!s} {!s}_table[] = {!s}\n".format(
cls.cpp_classname,
identifier,
"{"))
for d in data:
f.write(str(d))
f.write("\n")
f.write("};\n\n");
f.write("static_assert( (sizeof({!s}_table) / sizeof({!s})) == {!s}_size, \"Table size mismatch for {!s}\" );\n\n".format(
identifier,
cls.cpp_classname,
identifier,
cls.cpp_classname))
f.write(cpp_footer_comment)
if __name__ == "__main__":
export_class(CountryData, read_subtags_file())

View File

@@ -0,0 +1,83 @@
#! /usr/bin/env python3
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
"""
Python3 script to scrape some data out of zoneinfo/zone.tab.
To use this script, you must have a zone.tab in a standard location,
/usr/share/zoneinfo/zone.tab (this is usual on FreeBSD and Linux).
Prints out a few tables of zone names for use in translations.
"""
def scrape_file(file, regionset, zoneset):
for line in file.readlines():
if line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) < 3:
continue
zoneid = parts[2]
if not "/" in zoneid:
continue
region, zone = zoneid.split("/", 1)
zone = zone.strip().replace("_", " ")
regionset.add(region)
assert(zone not in zoneset)
zoneset.add(zone)
def write_set(file, label, set):
file.write("/* This returns a reference to local, which is a terrible idea.\n * Good thing it's not meant to be compiled.\n */\n")
# Note {{ is an escaped { for Python string formatting
file.write("static const QStringList& {!s}_table()\n{{\n\treturn QStringList {{\n".format(label))
for x in sorted(set):
file.write("""\t\tQObject::tr("{!s}", "{!s}"),\n""".format(x, label))
file.write("\t\tQString()\n\t};\n}\n\n")
cpp_header_comment = """/* GENERATED FILE DO NOT EDIT
*
* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2009 Arthur David Olson
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: CC0-1.0
*
* This file is derived from zone.tab, which has its own copyright statement:
*
* This file is in the public domain, so clarified as of
* 2009-05-17 by Arthur David Olson.
*
* From Paul Eggert (2018-06-27):
* This file is intended as a backward-compatibility aid for older programs.
* New programs should use zone1970.tab. This file is like zone1970.tab (see
* zone1970.tab's comments), but with the following additional restrictions:
*
* 1. This file contains only ASCII characters.
* 2. The first data column contains exactly one country code.
*
*/
/** THIS FILE EXISTS ONLY FOR TRANSLATIONS PURPOSES **/
// *INDENT-OFF*
// clang-format off
"""
if __name__ == "__main__":
regions=set()
zones=set()
with open("/usr/share/zoneinfo/zone.tab", "r") as f:
scrape_file(f, regions, zones)
with open("ZoneData_p.cpp", "w") as f:
f.write(cpp_header_comment)
write_set(f, "tz_regions", regions)
write_set(f, "tz_names", zones)

View File

@@ -0,0 +1,29 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*
*/
#ifndef MODULESYSTEM_ACTIONS_H
#define MODULESYSTEM_ACTIONS_H
namespace Calamares
{
namespace ModuleSystem
{
enum class Action : char
{
Show,
Exec
};
} // namespace ModuleSystem
} // namespace Calamares
#endif

View File

@@ -0,0 +1,135 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "Config.h"
#include "Preset.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
namespace Calamares
{
namespace ModuleSystem
{
class Config::Private
{
public:
std::unique_ptr< Presets > m_presets;
};
Config::Config( QObject* parent )
: QObject( parent )
, d( std::make_unique< Private >() )
{
}
Config::~Config() {}
bool
Config::isEditable( const QString& fieldName ) const
{
if ( m_unlocked )
{
return true;
}
if ( d && d->m_presets )
{
return d->m_presets->isEditable( fieldName );
}
else
{
cWarning() << "Checking isEditable, but no presets are configured.";
}
return true;
}
Config::ApplyPresets::ApplyPresets( Calamares::ModuleSystem::Config& c, const QVariantMap& configurationMap )
: m_c( c )
, m_bogus( true )
, m_map( Calamares::getSubMap( configurationMap, "presets", m_bogus ) )
{
c.m_unlocked = true;
if ( !c.d->m_presets )
{
c.d->m_presets = std::make_unique< Presets >();
}
}
Config::ApplyPresets::~ApplyPresets()
{
m_c.m_unlocked = false;
// Check that there's no **settings** (from the configuration map)
// that have not been consumed by apply() -- if they are there,
// that means the configuration map specifies things that the
// Config object does not expect.
bool haveWarned = false;
for ( const auto& k : m_map.keys() )
{
if ( !m_c.d->m_presets->find( k ).isValid() )
{
if ( !haveWarned )
{
cWarning() << "Preset configuration contains unused keys";
haveWarned = true;
}
cDebug() << Logger::SubEntry << "Unused key" << k;
}
}
}
Config::ApplyPresets&
Config::ApplyPresets::apply( const char* fieldName )
{
const auto prop = m_c.property( fieldName );
if ( !prop.isValid() )
{
cWarning() << "Applying invalid property" << fieldName;
return *this;
}
const QString key( fieldName );
if ( key.isEmpty() )
{
cWarning() << "Applying empty field";
return *this;
}
if ( m_c.d->m_presets->find( key ).isValid() )
{
cWarning() << "Applying duplicate property" << fieldName;
return *this;
}
if ( m_map.contains( key ) )
{
// Key has an explicit setting
QVariantMap m = Calamares::getSubMap( m_map, key, m_bogus );
QVariant value = m[ "value" ];
bool editable = Calamares::getBool( m, "editable", true );
if ( value.isValid() )
{
m_c.setProperty( fieldName, value );
}
m_c.d->m_presets->append( PresetField { key, value, editable } );
}
else
{
// There is no setting, but since we apply() this field,
// we do know about it; put in a preset so that looking
// it up won't complani.
m_c.d->m_presets->append( PresetField { key, QVariant(), true } );
}
return *this;
}
} // namespace ModuleSystem
} // namespace Calamares

View File

@@ -0,0 +1,147 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef CALAMARES_MODULESYSTEM_CONFIG_H
#define CALAMARES_MODULESYSTEM_CONFIG_H
#include "DllMacro.h"
#include <QObject>
#include <QStringList>
#include <QVariantMap>
#include <memory>
namespace Calamares
{
namespace ModuleSystem
{
/** @brief Base class for Config-objects
*
* This centralizes the things every Config-object should
* do and provides one source of preset-data. A Config-object
* for a module can **optionally** inherit from this class
* to get presets-support.
*
* TODO:3.3 This is not optional
* TODO:3.3 Put consistent i18n for Configurations in here too
*/
class DLLEXPORT Config : public QObject
{
Q_OBJECT
public:
Config( QObject* parent = nullptr );
~Config() override;
/** @brief Set the configuration from the config file
*
* Subclasses must implement this to load configuration data;
* that subclass **should** also call loadPresets() with the
* same map, to pick up the "presets" key consistently.
*/
virtual void setConfigurationMap( const QVariantMap& ) = 0;
public Q_SLOTS:
/** @brief Checks if a @p fieldName is editable according to presets
*
* If the field is named as a preset, **and** the field is set
* to not-editable, returns @c false. Otherwise, return @c true.
* Calling this with an unknown field (one for which no presets
* are accepted) will print a warning and return @c true.
*
* @see CONFIG_PREVENT_EDITING
*
* Most setters will call isEditable() to check if the field should
* be editable. Do not count on the setter not being called: the
* UI might not have set the field to fixed / constant / not-editable
* and then you can have the setter called by changes in the UI.
*
* To prevent the UI from changing **and** to make sure that the UI
* reflects the unchanged value (rather than the changed value it
* sent to the Config object), use CONFIG_PREVENT_EDITING, like so:
*
* CONFIG_PREVENT_EDITING( type, "propertyName" );
*
* The ; is necessary. <type> is the type of the property; for instance
* QString. The name of the property is a (constant) string. The
* macro will return (out of the setter it is used in) if the field
* is not editable, and will send a notification event with the old
* value as soon as the event loop resumes.
*/
bool isEditable( const QString& fieldName ) const;
protected:
friend class ApplyPresets;
/** @brief "Builder" class for presets
*
* Derived classes should instantiate this (with themselves,
* and the whole configuration map that is passed to
* setConfigurationMap()) and then call .apply() to apply
* the presets specified in the configuration to the **named**
* QObject properties.
*/
class ApplyPresets
{
public:
/** @brief Create a preset-applier for this config
*
* The @p configurationMap should be the one passed in to
* setConfigurationMap() . Presets are extracted from the
* standard key *presets* and can be applied to the configuration
* with apply() or operator<<.
*/
ApplyPresets( Config& c, const QVariantMap& configurationMap );
~ApplyPresets();
/** @brief Add a preset for the given @p fieldName
*
* This checks for preset-entries in the configuration map that was
* passed in to the constructor.
*/
ApplyPresets& apply( const char* fieldName );
/** @brief Alternate way of writing apply()
*/
ApplyPresets& operator<<( const char* fieldName ) { return apply( fieldName ); }
private:
Config& m_c;
bool m_bogus = true;
const QVariantMap m_map;
};
private:
class Private;
std::unique_ptr< Private > d;
bool m_unlocked = false;
};
} // namespace ModuleSystem
} // namespace Calamares
/// @see Config::isEditable()
//
// This needs to be a macro, because Q_ARG() is a macro that stringifies
// the type name.
#define CONFIG_PREVENT_EDITING( type, fieldName ) \
do \
{ \
if ( !isEditable( QStringLiteral( fieldName ) ) ) \
{ \
auto prop = property( fieldName ); \
const auto& metaobject = metaObject(); \
auto metaprop = metaobject->property( metaobject->indexOfProperty( fieldName ) ); \
if ( metaprop.hasNotifySignal() ) \
{ \
metaprop.notifySignal().invoke( this, Qt::QueuedConnection, Q_ARG( type, prop.value< type >() ) ); \
} \
return; \
} \
} while ( 0 )
#endif

View File

@@ -0,0 +1,171 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#include "Descriptor.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
namespace Calamares
{
namespace ModuleSystem
{
const NamedEnumTable< Type >&
typeNames()
{
// *INDENT-OFF*
// clang-format off
static const NamedEnumTable< Type > table{
{ QStringLiteral( "job" ), Type::Job },
{ QStringLiteral( "view" ), Type::View },
{ QStringLiteral( "viewmodule" ), Type::View },
{ QStringLiteral( "jobmodule" ), Type::Job }
};
// *INDENT-ON*
// clang-format on
return table;
}
const NamedEnumTable< Interface >&
interfaceNames()
{
// *INDENT-OFF*
// clang-format off
static const NamedEnumTable< Interface > table {
{ QStringLiteral("process"), Interface::Process },
{ QStringLiteral("qtplugin"), Interface::QtPlugin },
{ QStringLiteral("python"), Interface::Python },
};
// *INDENT-ON*
// clang-format on
return table;
}
Descriptor::Descriptor() {}
Descriptor
Descriptor::fromDescriptorData( const QVariantMap& moduleDesc, const QString& descriptorPath )
{
Descriptor d;
Logger::Once o;
{
bool typeOk = false;
QString typeValue = moduleDesc.value( "type" ).toString();
Type t = typeNames().find( typeValue, typeOk );
if ( !typeOk )
{
if ( o )
{
cWarning() << o << "Descriptor file" << descriptorPath;
}
cWarning() << o << "Module descriptor contains invalid *type*" << typeValue;
}
bool interfaceOk = false;
QString interfaceValue = moduleDesc.value( "interface" ).toString();
Interface i = interfaceNames().find( interfaceValue, interfaceOk );
if ( !interfaceOk )
{
if ( o )
{
cWarning() << o << "Descriptor file" << descriptorPath;
}
cWarning() << o << "Module descriptor contains invalid *interface*" << interfaceValue;
}
d.m_name = moduleDesc.value( "name" ).toString();
if ( typeOk && interfaceOk && !d.m_name.isEmpty() )
{
d.m_type = t;
d.m_interface = i;
d.m_isValid = true;
}
}
if ( !d.m_isValid )
{
return d;
}
d.m_isEmergeny = Calamares::getBool( moduleDesc, "emergency", false );
d.m_hasConfig = !Calamares::getBool( moduleDesc, "noconfig", false ); // Inverted logic during load
d.m_requiredModules = Calamares::getStringList( moduleDesc, "requiredModules" );
d.m_weight = int( Calamares::getInteger( moduleDesc, "weight", -1 ) );
QStringList consumedKeys { "type", "interface", "name", "emergency", "noconfig", "requiredModules", "weight" };
switch ( d.interface() )
{
case Interface::QtPlugin:
d.m_script = Calamares::getString( moduleDesc, "load" );
consumedKeys << "load";
break;
case Interface::Python:
d.m_script = Calamares::getString( moduleDesc, "script" );
if ( d.m_script.isEmpty() )
{
if ( o )
{
cWarning() << o << "Descriptor file" << descriptorPath;
}
cWarning() << o << "Module descriptor contains no *script*" << d.name();
d.m_isValid = false;
}
consumedKeys << "script";
break;
case Interface::Process:
d.m_script = Calamares::getString( moduleDesc, "command" );
d.m_processTimeout = int( Calamares::getInteger( moduleDesc, "timeout", 30 ) );
d.m_processChroot = Calamares::getBool( moduleDesc, "chroot", false );
if ( d.m_processTimeout < 0 )
{
d.m_processTimeout = 0;
}
if ( d.m_script.isEmpty() )
{
if ( o )
{
cWarning() << o << "Descriptor file" << descriptorPath;
}
cWarning() << o << "Module descriptor contains no *script*" << d.name();
d.m_isValid = false;
}
consumedKeys << "command"
<< "timeout"
<< "chroot";
break;
}
if ( !d.m_isValid )
{
return d;
}
QStringList superfluousKeys;
for ( auto kv = moduleDesc.keyBegin(); kv != moduleDesc.keyEnd(); ++kv )
{
if ( !consumedKeys.contains( *kv ) )
{
superfluousKeys << *kv;
}
}
if ( !superfluousKeys.isEmpty() )
{
if ( o )
{
cWarning() << o << "Descriptor file" << descriptorPath;
}
cWarning() << o << "Module descriptor contains extra keys:" << Logger::DebugList( superfluousKeys );
d.m_isValid = false;
}
return d;
}
} // namespace ModuleSystem
} // namespace Calamares

Some files were not shown because too many files have changed in this diff Show More