mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 02:41:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b29f06a0ab | ||
|
|
9ec0d39592 | ||
|
|
c55f9c2251 | ||
|
|
d2675f15d1 | ||
|
|
5371c85362 | ||
|
|
0b9b05ed85 | ||
|
|
546b9c621d |
@@ -0,0 +1,65 @@
|
|||||||
|
name: clang-tidy fix
|
||||||
|
|
||||||
|
# Makes the missing-include fix that /bot fix-tidy (pr-tidy-bot.yml) posts on a
|
||||||
|
# pull request: scripts/run_clang_tidy.sh --fix on the merge commit the
|
||||||
|
# clang-tidy check runs on, uploaded as the clang-tidy-fix artifact.
|
||||||
|
#
|
||||||
|
# Every push to a PR starts this workflow, and its job skips on that first
|
||||||
|
# attempt. The bot re-runs it on request. A re-run keeps the pull_request
|
||||||
|
# context, so the PR's code runs with a read-only token, no secrets and the
|
||||||
|
# PR's own cache, never main's, whoever asked for it.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- release/*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fix:
|
||||||
|
if: github.run_attempt > 1
|
||||||
|
# A hosted runner, so nothing the PR's code leaves behind outlives the job.
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
lfs: 'false'
|
||||||
|
# The PR merge commit plus its first parent, the base it is diffed against.
|
||||||
|
fetch-depth: 2
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
# Parsing needs the dependency headers, not a build of this PR's deps/, so
|
||||||
|
# a PR that changes deps/ falls back to the newest cache main has.
|
||||||
|
- name: Restore cached deps
|
||||||
|
uses: actions/cache/restore@v6
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
|
||||||
|
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
|
||||||
|
restore-keys: linux-clang-cache-orcaslicer_deps-build-
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
|
- name: Apt-Install Dependencies
|
||||||
|
uses: ./.github/actions/apt-install-deps
|
||||||
|
|
||||||
|
# The status says whether findings remain that --fix cannot fix.
|
||||||
|
- name: Fix the changed lines
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RUNNER_TEMP/clang-tidy-fix"
|
||||||
|
status=0
|
||||||
|
scripts/run_clang_tidy.sh --base HEAD^1 --yes --fix || status=$?
|
||||||
|
echo "$status" > "$RUNNER_TEMP/clang-tidy-fix/status"
|
||||||
|
git -c core.quotePath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ \
|
||||||
|
> "$RUNNER_TEMP/clang-tidy-fix/fix.patch"
|
||||||
|
|
||||||
|
- name: Upload the fix
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: clang-tidy-fix
|
||||||
|
path: ${{ runner.temp }}/clang-tidy-fix/
|
||||||
|
retention-days: 7
|
||||||
|
# A later re-run of the same run replaces the earlier fix.
|
||||||
|
overwrite: true
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
name: PR clang-tidy Bot
|
||||||
|
|
||||||
|
# Helps a pull request past the clang-tidy check.
|
||||||
|
#
|
||||||
|
# When the check fails on a PR's changes, a comment asks the author to fix it,
|
||||||
|
# either with scripts/run_clang_tidy.sh --fix or by commenting:
|
||||||
|
# /bot fix-tidy
|
||||||
|
#
|
||||||
|
# That command, from the PR's author or an account with write access, re-runs
|
||||||
|
# the PR's clang-tidy fix run (clang_tidy_fix.yml), which makes the fix in the
|
||||||
|
# PR's own context. When it finishes, the fix is posted as one review: each
|
||||||
|
# include whose place falls on a line the PR's diff shows is a suggestion GitHub
|
||||||
|
# can commit in one click, and the review's body holds the whole patch with a
|
||||||
|
# git apply command. Findings --fix cannot fix, such as a using in a header,
|
||||||
|
# are left for the check to report.
|
||||||
|
#
|
||||||
|
# No code from the PR runs here. Nothing is pushed, and the patch is only
|
||||||
|
# posted when every line it adds is an #include in a C++ file the PR changes.
|
||||||
|
#
|
||||||
|
# workflow_run matches the workflows by their name: keep "clang-tidy" and
|
||||||
|
# "clang-tidy fix" in step with clang_tidy.yml and clang_tidy_fix.yml, and
|
||||||
|
# TIDY_STEP below with the clang-tidy step's name in clang_tidy.yml.
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- clang-tidy
|
||||||
|
- clang-tidy fix
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
request:
|
||||||
|
# Skips the job unless a PR comment mentions the command.
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'issue_comment'
|
||||||
|
&& github.event.issue.pull_request != null
|
||||||
|
&& contains(github.event.comment.body, '/bot fix-tidy')
|
||||||
|
permissions:
|
||||||
|
actions: write # re-run the fix run
|
||||||
|
pull-requests: write # react to and answer the comment, which is the PR's
|
||||||
|
issues: write # feedback comment + reactions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Re-run the PR's clang-tidy fix
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
const FIX_WORKFLOW = 'clang_tidy_fix.yml';
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const issue = context.payload.issue;
|
||||||
|
const comment = context.payload.comment;
|
||||||
|
|
||||||
|
// Ignores a comment whose sender is not its author.
|
||||||
|
if (context.payload.sender.login !== comment.user.login || comment.user.type !== 'User') {
|
||||||
|
core.info('Ignoring a comment not written by its sender, or by a bot.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandLine = (comment.body || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => /^\/bot\s+fix-tidy\b/i.test(line));
|
||||||
|
if (!commandLine) {
|
||||||
|
core.info('No /bot fix-tidy command found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commenter = comment.user.login;
|
||||||
|
// Answers only the PR's author and maintainers, so the command cannot be used to spam.
|
||||||
|
if (commenter !== issue.user.login) {
|
||||||
|
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter });
|
||||||
|
if (!['admin', 'write'].includes(access.permission)) {
|
||||||
|
core.info(`Ignoring /bot fix-tidy from @${commenter}: not the PR's author and no write access.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const react = (content) =>
|
||||||
|
github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content });
|
||||||
|
async function refuse(reason) {
|
||||||
|
await react('-1');
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n@${commenter} ${reason}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
await react('eyes');
|
||||||
|
|
||||||
|
if (!/^\/bot\s+fix-tidy$/i.test(commandLine)) {
|
||||||
|
return refuse('`/bot fix-tidy` takes no arguments.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue.number });
|
||||||
|
if (pr.state !== 'open') {
|
||||||
|
return refuse('the PR is not open.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha = pr.head.sha.slice(0, 10);
|
||||||
|
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||||
|
owner, repo, workflow_id: FIX_WORKFLOW, event: 'pull_request', head_sha: pr.head.sha, per_page: 1
|
||||||
|
});
|
||||||
|
const run = runs[0];
|
||||||
|
// A PR last pushed before clang_tidy_fix.yml existed has no run to re-run.
|
||||||
|
if (!run) {
|
||||||
|
return refuse(`there is no clang-tidy fix run for ${sha}, the PR's latest commit. If you just pushed it, ` +
|
||||||
|
'try again in a minute. Otherwise, push a commit or merge the base branch to start one, then comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
if (run.status !== 'completed') {
|
||||||
|
// The first attempt is the one every push starts, and it makes no fix.
|
||||||
|
return refuse(run.run_attempt > 1
|
||||||
|
? `a clang-tidy fix for ${sha} is already [running](${run.html_url}). I'll post it when it finishes.`
|
||||||
|
: `the PR's checks on ${sha} are still starting. Comment \`/bot fix-tidy\` again in a minute.`);
|
||||||
|
}
|
||||||
|
// GitHub refuses to re-run a run created over 30 days ago.
|
||||||
|
try {
|
||||||
|
await github.rest.actions.reRunWorkflow({ owner, repo, run_id: run.id });
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Re-running ${run.html_url} failed: ${error.message}`);
|
||||||
|
return refuse(`GitHub won't re-run the [clang-tidy fix run](${run.html_url}) for ${sha} (${error.message}). ` +
|
||||||
|
'Push a commit or merge the base branch to start a new one, then comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
core.info(`Re-ran ${run.html_url}`);
|
||||||
|
|
||||||
|
suggest:
|
||||||
|
# The fix run's re-runs only. Its first attempt makes no fix.
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'workflow_run'
|
||||||
|
&& github.event.workflow_run.name == 'clang-tidy fix'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
&& github.event.workflow_run.run_attempt > 1
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-suggest-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
actions: read # download the fix
|
||||||
|
contents: read # check out the PR head
|
||||||
|
pull-requests: write # the review
|
||||||
|
issues: write # feedback comment + reactions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Find the PR and who asked
|
||||||
|
id: find
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
const CPP = /^(src|tests)\/.+\.(cpp|cc|cxx|hpp|h|hxx)$/;
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const run = context.payload.workflow_run;
|
||||||
|
|
||||||
|
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||||
|
const { data: prs } = await github.rest.pulls.list({
|
||||||
|
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||||
|
});
|
||||||
|
const pr = prs[0];
|
||||||
|
if (!pr) {
|
||||||
|
core.info('No open PR for this run.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The latest /bot fix-tidy comment is the one this run answers.
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||||
|
const request = comments.reverse().find((c) => c.user.type === 'User' && /^\s*\/bot\s+fix-tidy\s*$/im.test(c.body || ''));
|
||||||
|
const mention = request ? `@${request.user.login} ` : '';
|
||||||
|
async function refuse(reason) {
|
||||||
|
if (request) {
|
||||||
|
await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: request.id, content: '-1' });
|
||||||
|
}
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: `${MARKER}\n${mention}${reason}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pr.head.sha !== run.head_sha) {
|
||||||
|
return refuse('the PR changed while I was making its clang-tidy fix. Comment `/bot fix-tidy` again.');
|
||||||
|
}
|
||||||
|
if (run.conclusion !== 'success') {
|
||||||
|
return refuse(`I couldn't make the clang-tidy fix. See the [run log](${run.html_url}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The right-side lines of each changed C++ file's diff: the lines a
|
||||||
|
// review comment can be placed on. A file too large for GitHub to
|
||||||
|
// show its diff has none, and gets no suggestions.
|
||||||
|
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
|
||||||
|
const ranges = {};
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.status === 'removed' || !CPP.test(file.filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranges[file.filename] = [...(file.patch || '').matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)]
|
||||||
|
.map((m) => [Number(m[1]), Number(m[1]) + Number(m[2] ?? 1) - 1])
|
||||||
|
.filter(([first, last]) => last >= first);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput('pr', pr.number);
|
||||||
|
core.setOutput('head-sha', pr.head.sha);
|
||||||
|
core.setOutput('head-repo', run.head_repository.full_name);
|
||||||
|
core.setOutput('ranges', JSON.stringify(ranges));
|
||||||
|
core.setOutput('mention', mention);
|
||||||
|
core.setOutput('request-id', request ? request.id : '');
|
||||||
|
|
||||||
|
- name: Download the fix
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: clang-tidy-fix
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
path: ${{ runner.temp }}/clang-tidy-fix
|
||||||
|
|
||||||
|
# The PR's own files, only to apply the patch to. Nothing in them is run.
|
||||||
|
- name: Checkout the PR head
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.find.outputs.head-repo }}
|
||||||
|
ref: ${{ steps.find.outputs.head-sha }}
|
||||||
|
path: head
|
||||||
|
fetch-depth: 1
|
||||||
|
lfs: 'false'
|
||||||
|
persist-credentials: false
|
||||||
|
sparse-checkout: |
|
||||||
|
src
|
||||||
|
tests
|
||||||
|
|
||||||
|
# The fix was made on the PR's merge commit, so it is applied to the head
|
||||||
|
# and diffed again, giving line numbers in the PR's own files.
|
||||||
|
- name: Check the fix and place its suggestions
|
||||||
|
id: place
|
||||||
|
if: steps.find.outputs.pr != ''
|
||||||
|
env:
|
||||||
|
RANGES: ${{ steps.find.outputs.ranges }}
|
||||||
|
run: |
|
||||||
|
python3 -I - "$RUNNER_TEMP/clang-tidy-fix/fix.patch" head "$RUNNER_TEMP" <<'EOF'
|
||||||
|
import json, os, re, subprocess, sys
|
||||||
|
|
||||||
|
patch_path, head, out = sys.argv[1:]
|
||||||
|
ranges = json.loads(os.environ["RANGES"])
|
||||||
|
|
||||||
|
def output(**values):
|
||||||
|
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
|
||||||
|
for key, value in values.items():
|
||||||
|
f.write(f"{key}={value}\n")
|
||||||
|
|
||||||
|
patch = open(patch_path, encoding="utf-8").read()
|
||||||
|
if not patch.strip():
|
||||||
|
output(result="empty")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Every line the patch adds must be an #include or blank, in a C++ file the
|
||||||
|
# PR changes, and it may not remove, create, rename or change the mode of
|
||||||
|
# anything.
|
||||||
|
include = re.compile(r'\s*#\s*include\s*(<[\w./+-]+>|"[\w./+-]+")\s*')
|
||||||
|
header = re.compile(r"(index [0-9a-f]+\.\.[0-9a-f]+( \d+)?|@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*)")
|
||||||
|
problems = []
|
||||||
|
path = None
|
||||||
|
for line in patch.splitlines():
|
||||||
|
if line.startswith("diff --git "):
|
||||||
|
match = re.fullmatch(r"diff --git a/(.+) b/\1", line)
|
||||||
|
path = match.group(1) if match else None
|
||||||
|
if path not in ranges:
|
||||||
|
problems.append(f"{line}: not a C++ file the PR changes")
|
||||||
|
# git appends a tab to the --- and +++ lines of a path that contains a space.
|
||||||
|
elif line.removesuffix("\t") in (f"--- a/{path}", f"+++ b/{path}") or header.fullmatch(line):
|
||||||
|
pass
|
||||||
|
elif line.startswith(" ") or line == "\\ No newline at end of file":
|
||||||
|
pass
|
||||||
|
elif line.startswith("+") and (include.fullmatch(line[1:]) or not line[1:].strip()):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
problems.append(f"{path}: {line}")
|
||||||
|
if problems:
|
||||||
|
for problem in problems:
|
||||||
|
print(f"::error::Refused patch line {problem}")
|
||||||
|
output(result="rejected")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if subprocess.run(["git", "-C", head, "apply", "--index", os.path.abspath(patch_path)]).returncode != 0:
|
||||||
|
output(result="apply-failed")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
git = ["git", "-C", head, "-c", "core.quotePath=false", "diff", "--cached", "--no-color", "--no-ext-diff",
|
||||||
|
"--src-prefix=a/", "--dst-prefix=b/"]
|
||||||
|
with open(os.path.join(out, "head.patch"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(subprocess.run(git, check=True, capture_output=True, encoding="utf-8").stdout)
|
||||||
|
|
||||||
|
# Each run of added lines becomes one suggestion on the line it goes after
|
||||||
|
# (before, at the top of a file): that line, replaced by itself and the
|
||||||
|
# includes. It can only sit on a line the PR's diff shows.
|
||||||
|
hunks = []
|
||||||
|
path = None
|
||||||
|
for line in subprocess.run(git + ["-U0"], check=True, capture_output=True, encoding="utf-8").stdout.splitlines():
|
||||||
|
if line.startswith("diff --git "):
|
||||||
|
path = re.fullmatch(r"diff --git a/(.+) b/\1", line).group(1)
|
||||||
|
elif line.startswith("@@ "):
|
||||||
|
hunks.append((path, int(re.match(r"@@ -(\d+)", line).group(1)), []))
|
||||||
|
elif hunks and line.startswith("+") and not line.startswith("+++ "):
|
||||||
|
hunks[-1][2].append(line[1:])
|
||||||
|
|
||||||
|
suggestions = []
|
||||||
|
total = suggested = 0
|
||||||
|
for path, after, added in hunks:
|
||||||
|
count = sum(bool(include.fullmatch(line)) for line in added)
|
||||||
|
total += count
|
||||||
|
original = subprocess.run(["git", "-C", head, "show", f"HEAD:{path}"], check=True, capture_output=True,
|
||||||
|
encoding="utf-8").stdout.splitlines()
|
||||||
|
line = max(after, 1)
|
||||||
|
if line > len(original) or not any(first <= line <= last for first, last in ranges[path]):
|
||||||
|
continue
|
||||||
|
body = "\n".join([original[line - 1], *added] if after else [*added, original[0]])
|
||||||
|
# A fence longer than any run of backticks in the line it repeats.
|
||||||
|
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", body)])
|
||||||
|
suggestions.append({"path": path, "line": line, "side": "RIGHT",
|
||||||
|
"body": f"{fence}suggestion\n{body}\n{fence}"})
|
||||||
|
suggested += count
|
||||||
|
with open(os.path.join(out, "suggestions.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(suggestions, f)
|
||||||
|
output(result="ok", includes=total, suggested=suggested)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Post the fix
|
||||||
|
if: always() && steps.find.outputs.pr != ''
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
env:
|
||||||
|
PR: ${{ steps.find.outputs.pr }}
|
||||||
|
MENTION: ${{ steps.find.outputs.mention }}
|
||||||
|
REQUEST_ID: ${{ steps.find.outputs.request-id }}
|
||||||
|
HEAD_SHA: ${{ steps.find.outputs.head-sha }}
|
||||||
|
RESULT: ${{ steps.place.outputs.result }}
|
||||||
|
INCLUDES: ${{ steps.place.outputs.includes }}
|
||||||
|
SUGGESTED: ${{ steps.place.outputs.suggested }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const MARKER = '<!-- pr-tidy-bot -->';
|
||||||
|
// GitHub's limit on a review body is 65536 characters.
|
||||||
|
const MAX_PATCH = 60000;
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const { PR, MENTION, REQUEST_ID, HEAD_SHA, RESULT, INCLUDES, SUGGESTED, RUNNER_TEMP } = process.env;
|
||||||
|
const issue_number = Number(PR);
|
||||||
|
const sha = HEAD_SHA.slice(0, 10);
|
||||||
|
const fixUrl = context.payload.workflow_run.html_url;
|
||||||
|
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||||
|
|
||||||
|
const react = (content) => REQUEST_ID
|
||||||
|
? github.rest.reactions.createForIssueComment({ owner, repo, comment_id: Number(REQUEST_ID), content })
|
||||||
|
: Promise.resolve();
|
||||||
|
async function reply(body, content) {
|
||||||
|
await react(content);
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number, body: `${MARKER}\n${MENTION}${body}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = (...parts) => fs.readFileSync(path.join(RUNNER_TEMP, ...parts), 'utf8');
|
||||||
|
const passes = RESULT && read('clang-tidy-fix', 'status').trim() === '0';
|
||||||
|
|
||||||
|
if (RESULT === 'empty') {
|
||||||
|
return passes
|
||||||
|
? reply(`clang-tidy passes on ${sha}. There is nothing to fix.`, '+1')
|
||||||
|
: reply(`clang-tidy has no automatic fix for what it reports on ${sha}. The findings, in the [fix run's log](${fixUrl}), need a manual edit.`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT === 'rejected') {
|
||||||
|
return reply(`the clang-tidy fix changes more than includes, so I didn't post it. See the [run log](${runUrl}).`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT === 'apply-failed') {
|
||||||
|
return reply(`the clang-tidy fix doesn't apply to ${sha}. Merge the base branch into the PR, then comment \`/bot fix-tidy\` again.`, '-1');
|
||||||
|
}
|
||||||
|
if (RESULT !== 'ok') {
|
||||||
|
return reply(`something went wrong. See the [run log](${runUrl}).`, '-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = read('head.patch');
|
||||||
|
const suggestions = JSON.parse(read('suggestions.json'));
|
||||||
|
|
||||||
|
const one = INCLUDES === '1';
|
||||||
|
const includes = one ? '1 missing include' : `${INCLUDES} missing includes`;
|
||||||
|
const inline = SUGGESTED === '0'
|
||||||
|
? (one ? 'It goes on a line this PR does not change, so I can\'t suggest it inline.'
|
||||||
|
: 'They go on lines this PR does not change, so I can\'t suggest them inline.')
|
||||||
|
: SUGGESTED === INCLUDES
|
||||||
|
? (one ? 'It is suggested inline below.' : 'Each is suggested inline below.')
|
||||||
|
: `${SUGGESTED} of them are suggested inline below; the rest go on lines this PR does not change.`;
|
||||||
|
const remaining = passes
|
||||||
|
? `With ${one ? 'it' : 'them'}, clang-tidy passes.`
|
||||||
|
: `Some findings remain that need a manual edit; see the [fix run's log](${fixUrl}).`;
|
||||||
|
// A fence longer than any run of backticks in the patch's context lines.
|
||||||
|
const fence = '`'.repeat(Math.max(3, ...[...patch.matchAll(/`+/g)].map((m) => m[0].length + 1)));
|
||||||
|
const apply = patch.length > MAX_PATCH
|
||||||
|
? `The full patch is too large to show here. Download \`fix.patch\` from the [fix run](${fixUrl}) and run \`git apply fix.patch\`.`
|
||||||
|
: `To apply ${one ? 'it' : 'all of them'}, run this in your checkout of the PR branch:\n\n${fence}sh\ngit apply <<'EOF'\n${patch}EOF\n${fence}`;
|
||||||
|
const body = (withSuggestions) => `${MARKER}\n${MENTION}clang-tidy's fix for ${sha} adds ${includes}. ` +
|
||||||
|
`${withSuggestions ? `${inline} ` : ''}${remaining}\n\n${apply}`;
|
||||||
|
|
||||||
|
const review = { owner, repo, pull_number: issue_number, commit_id: HEAD_SHA, event: 'COMMENT' };
|
||||||
|
try {
|
||||||
|
await github.rest.pulls.createReview({ ...review, body: body(true), comments: suggestions });
|
||||||
|
} catch (error) {
|
||||||
|
// A suggestion GitHub will not place still leaves the patch.
|
||||||
|
core.warning(`Posting the suggestions failed (${error.message}); posting the patch alone.`);
|
||||||
|
await github.rest.pulls.createReview({ ...review, body: body(false) });
|
||||||
|
}
|
||||||
|
await react('+1');
|
||||||
|
|
||||||
|
nudge:
|
||||||
|
if: >-
|
||||||
|
github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||||
|
&& github.event_name == 'workflow_run'
|
||||||
|
&& github.event.workflow_run.name == 'clang-tidy'
|
||||||
|
&& github.event.workflow_run.event == 'pull_request'
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-nudge-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
actions: read # the check's steps
|
||||||
|
pull-requests: write # comment on the PR
|
||||||
|
issues: write # the comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Ask for a fix when the check fails
|
||||||
|
uses: actions/github-script@v9
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const MARKER = '<!-- pr-tidy-bot-nudge -->';
|
||||||
|
// The clang-tidy step's name in clang_tidy.yml. A failure in any other
|
||||||
|
// step is the CI's, not the PR's.
|
||||||
|
const TIDY_STEP = 'Run clang-tidy on the changed lines';
|
||||||
|
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const run = context.payload.workflow_run;
|
||||||
|
|
||||||
|
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
|
||||||
|
const { data: prs } = await github.rest.pulls.list({
|
||||||
|
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
|
||||||
|
});
|
||||||
|
const pr = prs.find((p) => p.head.sha === run.head_sha);
|
||||||
|
if (!pr) {
|
||||||
|
core.info('No open PR at this run\'s commit; a newer run will answer.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: run.id });
|
||||||
|
const failed = jobs.some((job) => (job.steps || []).some((step) => step.name === TIDY_STEP && step.conclusion === 'failure'));
|
||||||
|
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
|
||||||
|
const previous = comments.find((c) => c.user.type === 'Bot' && (c.body || '').startsWith(MARKER));
|
||||||
|
|
||||||
|
if (!failed) {
|
||||||
|
// Removes the request once the check passes.
|
||||||
|
if (previous && run.conclusion === 'success') {
|
||||||
|
await github.rest.issues.deleteComment({ owner, repo, comment_id: previous.id });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = `${MARKER}\n@${pr.user.login} the clang-tidy check found problems in this PR's changes ` +
|
||||||
|
`at ${run.head_sha.slice(0, 10)} ([log](${run.html_url})). Missing includes can be fixed either way:\n\n` +
|
||||||
|
'- run `scripts/run_clang_tidy.sh --fix` (`scripts\\run_clang_tidy.ps1 -Fix` on Windows) and push the result, or\n' +
|
||||||
|
'- comment `/bot fix-tidy`, and I\'ll suggest the fix here for you to apply.\n\n' +
|
||||||
|
'Other findings, such as a `using` in a header, need a manual edit.';
|
||||||
|
// Edits the earlier request instead of posting another, so each push does not notify again.
|
||||||
|
if (previous) {
|
||||||
|
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Qidi",
|
"name": "Qidi",
|
||||||
"version": "02.04.00.16",
|
"version": "02.04.00.17",
|
||||||
"force_update": "0",
|
"force_update": "0",
|
||||||
"description": "Qidi configurations",
|
"description": "Qidi configurations",
|
||||||
"machine_model_list": [
|
"machine_model_list": [
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"extruder_clearance_height_to_lid": "120",
|
"extruder_clearance_height_to_lid": "120",
|
||||||
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
||||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||||
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||||
"thumbnails_format": "PNG",
|
"thumbnails_format": "PNG",
|
||||||
"default_filament_profile": [
|
"default_filament_profile": [
|
||||||
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
"default_filament_profile": [
|
"default_filament_profile": [
|
||||||
"Generic PLA @Qidi"
|
"Generic PLA @Qidi"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||||
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
||||||
"scan_first_layer": "0"
|
"scan_first_layer": "0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"210x210/COLPIC",
|
"210x210/COLPIC",
|
||||||
"110x110/PNG"
|
"110x110/PNG"
|
||||||
],
|
],
|
||||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||||
"scan_first_layer": "0"
|
"scan_first_layer": "0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ DockPanel::DockPanel(wxWindow* parent,
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed)
|
CloseHandler on_destroyed,
|
||||||
: WebPanel(parent, web_hosting::orca_bridge_script())
|
const wxString& profile)
|
||||||
|
: WebPanel(parent, web_hosting::orca_bridge_script(), profile)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, m_on_message(std::move(on_message))
|
||||||
, m_on_close(std::move(on_close))
|
, m_on_close(std::move(on_close))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
class wxWindow;
|
class wxWindow;
|
||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
@@ -27,7 +29,8 @@ public:
|
|||||||
const std::string& html,
|
const std::string& html,
|
||||||
MessageHandler on_message,
|
MessageHandler on_message,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed);
|
CloseHandler on_destroyed,
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
~DockPanel() override;
|
~DockPanel() override;
|
||||||
|
|
||||||
// Main thread only.
|
// Main thread only.
|
||||||
|
|||||||
@@ -3232,9 +3232,16 @@ bool GUI_App::on_init_inner()
|
|||||||
#ifdef _MSW_DARK_MODE
|
#ifdef _MSW_DARK_MODE
|
||||||
|
|
||||||
#ifndef __WINDOWS__
|
#ifndef __WINDOWS__
|
||||||
|
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
|
||||||
|
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
|
||||||
|
const bool saved_dark_mode = dark_mode();
|
||||||
wxSystemAppearance app = wxSystemSettings::GetAppearance();
|
wxSystemAppearance app = wxSystemSettings::GetAppearance();
|
||||||
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
|
||||||
GUI::wxGetApp().app_config->save();
|
GUI::wxGetApp().app_config->save();
|
||||||
|
if (dark_mode() != saved_dark_mode) {
|
||||||
|
init_label_colours();
|
||||||
|
Update_dark_mode_flag();
|
||||||
|
}
|
||||||
#endif // __APPLE__
|
#endif // __APPLE__
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ WebDialog::WebDialog(wxWindow* parent,
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style)
|
long wx_style,
|
||||||
|
const wxString& profile)
|
||||||
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
|
||||||
, m_html(html)
|
, m_html(html)
|
||||||
, m_on_message(std::move(on_message))
|
, m_on_message(std::move(on_message))
|
||||||
@@ -37,7 +38,7 @@ WebDialog::WebDialog(wxWindow* parent,
|
|||||||
{
|
{
|
||||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||||
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240), profile);
|
||||||
|
|
||||||
// Paint the window/webview in the themed background so there is no white
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public:
|
|||||||
SubmitHandler on_submit,
|
SubmitHandler on_submit,
|
||||||
CloseHandler on_close,
|
CloseHandler on_close,
|
||||||
CloseHandler on_destroyed,
|
CloseHandler on_destroyed,
|
||||||
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
|
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER,
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
~WebDialog() override;
|
~WebDialog() override;
|
||||||
|
|
||||||
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
static void post_message(WebDialog* dialog, const nlohmann::json& data);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile)
|
||||||
: wxPanel(parent, wxID_ANY)
|
: wxPanel(parent, wxID_ANY)
|
||||||
{
|
{
|
||||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
@@ -25,7 +25,7 @@ WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
|
|||||||
SetSizer(sizer);
|
SetSizer(sizer);
|
||||||
|
|
||||||
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
||||||
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
|
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url(), profile);
|
||||||
m_browser->SetBackgroundColour(GetBackgroundColour());
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
|
#include <wx/string.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -22,7 +23,7 @@ namespace Slic3r { namespace GUI {
|
|||||||
class WebPanel : public wxPanel
|
class WebPanel : public wxPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WebPanel(wxWindow* parent, const char* bridge_script);
|
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/Utils/MacDarkMode.hpp"
|
#include "slic3r/Utils/MacDarkMode.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include <wx/chartype.h>
|
||||||
#include <wx/setup.h>
|
#include <wx/setup.h>
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
@@ -72,6 +75,26 @@ webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Private storage root for an isolated (plugin) profile. Plugin pages must not reach the shared
|
||||||
|
// default store's cookies/session (e.g. the OrcaCloud login), so each profile gets its own dir.
|
||||||
|
// macOS isolates with an in-memory store instead (see CreateWebView), so this is unused there.
|
||||||
|
#if !defined(__WXOSX__)
|
||||||
|
static wxString isolated_profile_path(const wxString& profile)
|
||||||
|
{
|
||||||
|
wxString safe;
|
||||||
|
safe.reserve(profile.size());
|
||||||
|
for (wxString::const_iterator it = profile.begin(); it != profile.end(); ++it) {
|
||||||
|
const wxChar c = *it;
|
||||||
|
const bool keep = (c >= wxT('a') && c <= wxT('z')) || (c >= wxT('A') && c <= wxT('Z')) ||
|
||||||
|
(c >= wxT('0') && c <= wxT('9')) || c == wxT('.') || c == wxT('-') || c == wxT('_');
|
||||||
|
safe += keep ? c : wxT('_');
|
||||||
|
}
|
||||||
|
if (safe.empty())
|
||||||
|
safe = wxT("default");
|
||||||
|
return wxString::FromUTF8((boost::filesystem::path(Slic3r::data_dir()) / "web_profiles" / safe.ToStdString()).string());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __WIN32__
|
#ifdef __WIN32__
|
||||||
// Run Download and Install in another thread so we don't block the UI thread
|
// Run Download and Install in another thread so we don't block the UI thread
|
||||||
DWORD DownloadAndInstallWV2RT() {
|
DWORD DownloadAndInstallWV2RT() {
|
||||||
@@ -135,6 +158,9 @@ DWORD DownloadAndInstallWV2RT() {
|
|||||||
class WebViewEdge : public wxWebViewEdge
|
class WebViewEdge : public wxWebViewEdge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
WebViewEdge() = default;
|
||||||
|
explicit WebViewEdge(const wxWebViewConfiguration& config) : wxWebViewEdge(config) {}
|
||||||
|
|
||||||
bool SetUserAgent(const wxString &userAgent) override
|
bool SetUserAgent(const wxString &userAgent) override
|
||||||
{
|
{
|
||||||
bool dark = userAgent.Contains("dark");
|
bool dark = userAgent.Contains("dark");
|
||||||
@@ -213,6 +239,11 @@ public:
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explicit WebViewWebKit(const wxWebViewConfiguration& config)
|
||||||
|
: wxWebViewWebKit(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
~WebViewWebKit() override
|
~WebViewWebKit() override
|
||||||
{
|
{
|
||||||
RemoveScriptMessageHandler("wx");
|
RemoveScriptMessageHandler("wx");
|
||||||
@@ -284,7 +315,7 @@ static WebViewRef *webview_ref(wxWebView *webView)
|
|||||||
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxString const & profile)
|
||||||
{
|
{
|
||||||
#if wxUSE_WEBVIEW_EDGE
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
// Check if a fixed version of edge is present in
|
// Check if a fixed version of edge is present in
|
||||||
@@ -304,12 +335,36 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
|
|||||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
|
||||||
|
|
||||||
|
const bool isolated = !profile.IsEmpty();
|
||||||
|
|
||||||
#ifdef __WIN32__
|
#ifdef __WIN32__
|
||||||
wxWebView* webView = new WebViewEdge;
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendEdge);
|
||||||
|
config.SetDataPath(isolated_profile_path(profile));
|
||||||
|
webView = new WebViewEdge(config);
|
||||||
|
} else {
|
||||||
|
webView = new WebViewEdge;
|
||||||
|
}
|
||||||
#elif defined(__WXOSX__)
|
#elif defined(__WXOSX__)
|
||||||
wxWebView* webView = new WebViewWebKit;
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
// wx has no SetDataPath on macOS; give the view its own in-memory store instead.
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||||
|
Slic3r::GUI::WKWebViewConfiguration_setNonPersistentDataStore(config.GetNativeConfiguration());
|
||||||
|
webView = new WebViewWebKit(config);
|
||||||
|
} else {
|
||||||
|
webView = new WebViewWebKit;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
auto webView = wxWebView::New();
|
wxWebView* webView = nullptr;
|
||||||
|
if (isolated) {
|
||||||
|
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
|
||||||
|
config.SetDataPath(isolated_profile_path(profile));
|
||||||
|
webView = wxWebView::New(config);
|
||||||
|
} else {
|
||||||
|
webView = wxWebView::New();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (webView) {
|
if (webView) {
|
||||||
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
|
|||||||
class WebView
|
class WebView
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url);
|
// `profile` empty -> the shared default backend store used by all trusted UI.
|
||||||
|
// `profile` non-empty -> a private store keyed by `profile`, so this view shares no
|
||||||
|
// cookies/storage/session with the shared store or any other profile. Used for
|
||||||
|
// plugin-supplied (untrusted) content.
|
||||||
|
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url, wxString const &profile = wxEmptyString);
|
||||||
#if wxUSE_WEBVIEW_EDGE
|
#if wxUSE_WEBVIEW_EDGE
|
||||||
static bool CheckWebViewRuntime();
|
static bool CheckWebViewRuntime();
|
||||||
static bool DownloadAndInstallWebViewRuntime();
|
static bool DownloadAndInstallWebViewRuntime();
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
|
|||||||
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size,
|
const wxSize& dialog_size,
|
||||||
const wxSize& min_size)
|
const wxSize& min_size,
|
||||||
|
const wxString& profile)
|
||||||
{
|
{
|
||||||
SetTitle(title);
|
SetTitle(title);
|
||||||
SetMinSize(FromDIP(min_size));
|
SetMinSize(FromDIP(min_size));
|
||||||
@@ -181,7 +182,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
|
|||||||
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
// Created empty and loaded below, after the user scripts are added, so the first load already has them.
|
||||||
m_browser = WebView::CreateWebView(this, wxEmptyString);
|
m_browser = WebView::CreateWebView(this, wxEmptyString, profile);
|
||||||
if (m_browser == nullptr) {
|
if (m_browser == nullptr) {
|
||||||
wxLogError("Could not init m_browser");
|
wxLogError("Could not init m_browser");
|
||||||
delete topsizer;
|
delete topsizer;
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ public:
|
|||||||
bool create_webview(const std::string& resource_path,
|
bool create_webview(const std::string& resource_path,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxSize& dialog_size = wxSize(820, 660),
|
const wxSize& dialog_size = wxSize(820, 660),
|
||||||
const wxSize& min_size = wxSize(640, 640));
|
const wxSize& min_size = wxSize(640, 640),
|
||||||
|
const wxString& profile = wxEmptyString);
|
||||||
|
|
||||||
void load_url(const wxString& url);
|
void load_url(const wxString& url);
|
||||||
bool run_script(const wxString& script);
|
bool run_script(const wxString& script);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ extern double mac_max_scaling_factor();
|
|||||||
extern void set_miniaturizable(void * window);
|
extern void set_miniaturizable(void * window);
|
||||||
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
|
||||||
void WKWebView_setTransparentBackground(void * web);
|
void WKWebView_setTransparentBackground(void * web);
|
||||||
|
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration);
|
||||||
void WKWebView_force_display(void * web);
|
void WKWebView_force_display(void * web);
|
||||||
void set_tag_when_enter_full_screen(bool isfullscreen);
|
void set_tag_when_enter_full_screen(bool isfullscreen);
|
||||||
void set_title_colour_after_set_title(void * window);
|
void set_title_colour_after_set_title(void * window);
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ void WKWebView_setTransparentBackground(void * web)
|
|||||||
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give a WKWebViewConfiguration its own in-memory website data store, so the view's cookies and
|
||||||
|
// storage neither come from nor leak into the shared default store. Used to isolate plugin web views.
|
||||||
|
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration)
|
||||||
|
{
|
||||||
|
WKWebViewConfiguration * config = (WKWebViewConfiguration*)configuration;
|
||||||
|
if (config)
|
||||||
|
config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
|
||||||
|
}
|
||||||
|
|
||||||
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
|
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
|
||||||
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
|
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
|
||||||
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
|
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
|
||||||
|
|||||||
@@ -390,7 +390,8 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
|
|
||||||
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
|
||||||
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
|
||||||
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
|
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE,
|
||||||
|
wxString::FromUTF8(plugin_key));
|
||||||
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
UiRegistry::instance().bind(new_id, dlg, plugin_key);
|
||||||
if (modal) {
|
if (modal) {
|
||||||
dlg->ShowModal();
|
dlg->ShowModal();
|
||||||
@@ -487,7 +488,7 @@ py::object ui_create_dock_panel(const std::string& html, const std::string& titl
|
|||||||
|
|
||||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||||
std::move(on_destroyed));
|
std::move(on_destroyed), wxString::FromUTF8(plugin_key));
|
||||||
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
wxSize(w, h), [panel]() { panel->fire_close(); });
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||||
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS,
|
||||||
|
wxString::FromUTF8(capability ? capability->identity().plugin_key : std::string()))
|
||||||
, m_cap(std::move(capability))
|
, m_cap(std::move(capability))
|
||||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user